R-51: aggregateState's mixed branch returned StateRunning ("partial"), so a stack whose
MAIN container was dead behind live helpers alerted on nothing — immich-server sat Exited
for 18 h, 100 % unreachable, no banner and no app_start_failed (audit F4). New
StateDegraded: a DOWN member whose docker restart policy is always/unless-stopped is a
fault (degraded, a down state); no/on-failure is a finished one-shot and stays benign; an
unreadable policy fails CLOSED. The unhealthy/restarting/paused/unknown exclusions are
byte-identical — folding unhealthy into down is the flapping fix-3 avoided.
R-52: new internal/bootrecon — one bounded start-once sweep at startup (2 attempts, 30 s
apart) for apps an interrupted boot left behind, inside the 90 s boot grace so a success
is silent and a failure still alerts. A zero-container stack is NEVER touched: the UI's
Stop is compose down, so a deliberate stop survives a reboot.
Both features carry a production-path wiring test (the v0.154.0 / v0.91.0 inert-seam
class). The main() assertion is an AST walk, not strings.Contains — the substring version
passed its own red-proof, because a commented-out call still contains the string.
Red-proofs run and restored: mix branch reverted -> "running" on the immich fixture;
boot hook commented out -> wiring test fails; zero-container gate dropped -> the
user-stopped app gets started.
NOTE: controller/cmd/controller/ is matched by .gitignore's `controller` entry, so new
files there need `git add -f` (and ripgrep silently skips main.go without --no-ignore).
- inframeta.go: static display-only map (name/description/Linked) + infraMeta template func;
filebrowser is the ONLY Linked stack (files.<domain> Megnyitás on the dashboard row;
the stacks card already links via Subdomains)
- generic infra icon: embedded /static/infra-logo.svg (Lucide-style server, monochrome)
wired through the app-row FallbackIcon slot + the stacks-card onerror chain
- dashboard rows + app cards: infra name + description + existing Védett chip
- guarded WRONG outcome: no customer link for cloudflared/traefik — render tests count
exactly ONE https:// link; red-proven (Linked:true on cloudflared → test FAILS)
Found during the D0 click-through: OffboxTarget.LastRun persists as an
RFC3339 string (settings.json), but backups.html passed it to timeAgo
(expects time.Time), so GET /backups returned 500 on any node where an
off-box backup had ever run. Pre-existing since v0.93.0 (NAS Part B),
unrelated to the re-skin. New timeAgoStr funcmap helper parses the
string (falls back to the raw value); also v2-token fix for the offbox
error hint (var(--danger) fallback hex) and the dead stat-running/
stat-stopped classes on the offbox status card.
Traefik only publishes a route to a healthy container, so an unhealthy deployed app
returns 404 at its URL though the container runs — previously shown only as 'Nem
egészséges' with no hint the URL is dead. New routeUnpublished() funcmap helper +
a distinct indicator on the dashboard and stacks cards (gated on .Deployed). Tests:
routeUnpublished across all states, real templateFS parses with the funcmap, and the
card guard renders the indicator only for deployed+unhealthy.
Deploy API now returns immediately after validation + config save.
docker compose up -d runs in a background goroutine so the UI shows
progress during image pulls instead of blocking for 30-60s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New /backups page with full backup system visibility:
- Status overview cards (local/remote backup, DB count, repo size)
- Schedule section with next-run times and retention policy
- Database table with type, size, validation (table count), status
- Snapshot history table with per-snapshot stats
- Repository info card with paths, integrity status, remote placeholder
- "Mentés most" button with auto-refresh polling
- Empty state when backup not configured
Backend: SnapshotRecord history (ring buffer), DumpValidation,
ListDumpFiles, ListSnapshots, GetFullStatus, restic check tracking.
Server accepts scheduler for next-run time calculation.
Sidebar nav updated with 3rd item, dashboard backup card title clickable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add filter bar (Mind/Futó/Leállítva/Telepíthető) to Alkalmazások page with
URL-based filter persistence. Dashboard stat cards are now clickable links
that navigate to the filtered stacks view.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2 (Monitoring & Health):
- Central job scheduler replacing ad-hoc goroutines (internal/scheduler)
- CPU usage collector via /proc/stat background sampling (internal/system/cpu_linux.go)
- Temperature reading from /sys/class/thermal + /host/sys (Docker mount)
- Load average from /proc/loadavg
- Healthchecks.io-compatible HTTP pinger (internal/monitor/pinger.go)
- System health checks: disk, memory, CPU, temp, Docker, protected containers (internal/monitor/healthcheck.go)
Phase 3 (Backups):
- Database auto-discovery via docker ps + docker inspect (internal/backup/dbdump.go)
- Database dumping via docker exec (pg_dump / mariadb-dump) with atomic writes
- Restic backup integration with auto-password generation (internal/backup/restic.go)
- Backup orchestrator: DB dumps + restic snapshots + weekly prune (internal/backup/backup.go)
- Manual backup trigger via dashboard button and POST /api/backup/run
Dashboard UI:
- CPU usage bar with load average display
- Temperature with colored indicator dot
- Backup status card with last run time, DB count, repo stats
- "Mentés most" button for manual backup trigger
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Migrate all 7 HTML templates + CSS from Go string constants to individual
go:embed files in internal/web/templates/ (templates.go: 2150→35 lines)
- Split server.go into auth.go, handlers.go, funcmap.go (server.go: 540→120 lines)
- Rename controller subdomain from dashboard.* to felhom.* in Traefik labels
- Update documentation (CLAUDE.md, README.md, CONTEXT.md)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>