# TASK.md — v0.4.7: Protected Stack Detail Pages + Backup Page Caching > Version bump: **v0.4.7** > Scope: UX fix + performance fix --- ## Task 1: Protected stacks — enable detail page + click-through ### Problem Protected stacks (filebrowser, traefik, cloudflared, felhom-controller) are excluded from the app detail page in two ways: 1. **No click-through**: `data-href="/apps/{{.Meta.Slug}}"` is gated behind `{{if not .Protected}}` on both `dashboard.html` and `stacks.html` 2. **No "Részletek" button**: The protected section in `stacks.html` only shows "Védett rendszerkomponens" badge + restart button — no "Részletek" link 3. **Detail page works**: Manually navigating to `/apps/filebrowser` renders fine — so the handler and template already support it ### Fix — Templates **stacks.html**: Add `data-href` for protected stacks that have a slug, and add "Részletek" button: Change the card opening div from: ```html
``` To: ```html
``` This lets any stack with a slug (including protected ones with `.felhom.yml`) be clickable. Stacks without `.felhom.yml` (no slug) won't have the click handler — which is correct. In the protected actions section, add "Részletek" link: ```html {{if .Protected}} Védett rendszerkomponens {{if isOperational .State}} {{end}} {{if .Meta.Slug}} Részletek {{end}} {{else if not .Deployed}} ``` **dashboard.html**: Same `data-href` fix for the compact card: Change from: ```html
``` To: ```html
``` ### Fix — FileBrowser .felhom.yml (resources) The manually created `.felhom.yml` on the demo node is missing `resources`. Update it to include: ```yaml resources: mem_request: "128M" mem_limit: "256M" pi_compatible: true needs_hdd: true ``` Also add this to the `.felhom.yml` created by `install_filebrowser()` in `scripts/docker-setup.sh`. **Manual fix for demo node** (run after deploy): ```bash cat >> /opt/docker/stacks/filebrowser/.felhom.yml << 'EOF' resources: mem_request: "128M" mem_limit: "256M" pi_compatible: true needs_hdd: true EOF ``` ### Verification - Clicking FileBrowser card on Alkalmazások page opens `/apps/filebrowser` detail page - "Részletek" button appears next to "Újraindítás" on FileBrowser - Detail page shows memory badges (~128M, HDD szükséges, Pi kompatibilis) - App info section shows use cases, first steps, prerequisites - Other protected stacks without `.felhom.yml` (traefik, cloudflared) don't show "Részletek" (no slug) --- ## Task 2: Backup page — cache expensive data ### Problem `GetFullStatus()` is called synchronously on every page load of `/backups` and runs three blocking subprocess calls: 1. `m.restic.Stats()` — executes `restic stats --json` + `restic snapshots --json` (~2-3 seconds) 2. `ListDumpFiles()` — directory listing (fast, ~1ms) 3. `DiscoverDatabases()` — `docker ps` + `docker inspect` per container (~0.5s) Total: 3-4 seconds per page load. This is unacceptable for a dashboard page. ### Solution: Background cache with periodic refresh Add a cached `FullBackupStatus` to the Manager that refreshes periodically via the scheduler, instead of computing on every page load. #### New fields in Manager: ```go type Manager struct { // ... existing fields ... mu sync.Mutex lastDBDump *DBDumpStatus lastBackup *BackupStatus running bool snapshotHistory []SnapshotRecord // Cached status for page rendering cachedStatus *FullBackupStatus cacheTime time.Time } ``` #### New method: `RefreshCache()` ```go // RefreshCache updates the cached full status. Called by scheduler every 5 minutes // and after each backup run. func (m *Manager) RefreshCache(nextDBDump, nextBackup time.Time) { // Same logic as current GetFullStatus() — run restic stats, list dump files, discover DBs status := &FullBackupStatus{ ... } // ... all the expensive calls ... m.mu.Lock() m.cachedStatus = status m.cacheTime = time.Now() m.mu.Unlock() m.logger.Printf("[INFO] Backup status cache refreshed") } ``` #### Modified `GetFullStatus()`: read from cache ```go // GetFullStatus returns the cached backup status for page rendering. // Returns instantly — no subprocess calls. func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupStatus { m.mu.Lock() defer m.mu.Unlock() if m.cachedStatus != nil { // Update dynamic fields that don't need subprocess calls m.cachedStatus.Running = m.running m.cachedStatus.NextDBDump = nextDBDump m.cachedStatus.NextBackup = nextBackup m.cachedStatus.LastDBDump = m.lastDBDump m.cachedStatus.LastBackup = m.lastBackup // Update snapshot history m.cachedStatus.SnapshotHistory = make([]SnapshotRecord, len(m.snapshotHistory)) copy(m.cachedStatus.SnapshotHistory, m.snapshotHistory) return m.cachedStatus } // No cache yet — return a minimal status (first page load before cache is populated) return &FullBackupStatus{ Enabled: m.cfg.Backup.Enabled, Running: m.running, LastDBDump: m.lastDBDump, LastBackup: m.lastBackup, DBDumpSchedule: m.cfg.Backup.DBDumpSchedule, ResticSchedule: m.cfg.Backup.ResticSchedule, PruneSchedule: m.cfg.Backup.PruneSchedule, NextDBDump: nextDBDump, NextBackup: nextBackup, Retention: m.cfg.Backup.Retention, RepoPath: m.cfg.Backup.ResticRepo, LastCheckTime: m.lastCheckTime, LastCheckOK: m.lastCheckOK, BackupPaths: []string{ m.cfg.Paths.StacksDir, m.cfg.Paths.DBDumpDir, "/opt/docker/felhom-controller/controller.yaml", }, } } ``` #### Scheduler integration (in `main.go`): Register a periodic job that refreshes the backup cache: ```go if cfg.Backup.Enabled && backupMgr != nil { // ... existing daily jobs ... // Cache refresh: every 5 minutes sched.Every("backup-cache", 5*time.Minute, func(ctx context.Context) error { nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule) nextBackup := scheduler.NextDailyRun(cfg.Backup.ResticSchedule) backupMgr.RefreshCache(nextDBDump, nextBackup) return nil }) } ``` #### Refresh after backup completion: At the end of `RunFullBackup()` and `RunBackup()`, call `RefreshCache()` so the page shows updated data immediately after a backup: ```go func (m *Manager) RunFullBackup(ctx context.Context) error { // ... existing logic ... // Refresh cache after backup completes m.RefreshCache( scheduler.NextDailyRun(m.cfg.Backup.DBDumpSchedule), scheduler.NextDailyRun(m.cfg.Backup.ResticSchedule), ) return nil // or the backup error } ``` **Note**: `RefreshCache()` needs to import `scheduler.NextDailyRun`. To avoid a circular import (backup → scheduler → backup), either: - Pass the next run times as parameters (already the pattern used) - Make `NextDailyRun` a standalone utility function that both packages can import - Or just call `RefreshCache` from `main.go` via a callback Simplest approach: `RefreshCache` takes `nextDBDump, nextBackup time.Time` params (same as `GetFullStatus`). The scheduler job and the post-backup refresh both compute the times before calling. #### Initial cache population on startup: In `main.go`, after scheduler starts, trigger initial cache refresh in a goroutine (don't block startup): ```go if cfg.Backup.Enabled && backupMgr != nil { go func() { nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule) nextBackup := scheduler.NextDailyRun(cfg.Backup.ResticSchedule) backupMgr.RefreshCache(nextDBDump, nextBackup) }() } ``` ### Result - Page load: instant (reads cached struct) - Cache refresh: every 5 minutes in background (user never waits) - After manual backup: cache refreshes immediately - First page load after startup: may show minimal data for a few seconds until goroutine completes ### Cache staleness indicator (optional) Add `CacheTime time.Time` to `FullBackupStatus`. The template can optionally show "Utolsó frissítés: X perccel ezelőtt" at the bottom of the page in a muted font. Not critical, but helpful for debugging. --- ## Implementation order ### Step 1: Protected stack detail pages 1. Fix `data-href` gating in `stacks.html` and `dashboard.html` — use `{{if .Meta.Slug}}` instead of `{{if not .Protected}}` 2. Add "Részletek" button to protected stack section in `stacks.html` 3. Update `install_filebrowser()` in `docker-setup.sh` — add `resources` to `.felhom.yml` ### Step 2: Backup page caching 1. Add `cachedStatus`, `cacheTime` fields to `Manager` 2. Create `RefreshCache()` method 3. Modify `GetFullStatus()` to read from cache 4. Register `backup-cache` scheduler job in `main.go` 5. Call `RefreshCache()` at end of `RunFullBackup()` and `RunBackup()` 6. Add initial cache goroutine in `main.go` ### Step 3: Build, deploy, verify 1. Build v0.4.7 2. Deploy to demo node 3. Update `/opt/docker/stacks/filebrowser/.felhom.yml` on demo node (add resources) 4. Verify FileBrowser card is clickable → detail page with memory badges 5. Verify backup page loads instantly 6. Trigger manual backup → verify page updates after completion ### Step 4: Documentation 1. Update CONTEXT.md, README 2. Bump version --- ## Files to modify ``` internal/backup/backup.go — add cachedStatus, RefreshCache(), modify GetFullStatus() internal/web/templates/stacks.html — fix data-href gating, add Részletek button internal/web/templates/dashboard.html — fix data-href gating scripts/docker-setup.sh — add resources to filebrowser .felhom.yml cmd/controller/main.go — register backup-cache job, initial goroutine ``` --- ## Verification checklist - [ ] FileBrowser card on Alkalmazások page is clickable → opens `/apps/filebrowser` - [ ] FileBrowser has "Részletek" button next to "Újraindítás" - [ ] FileBrowser detail page shows ~128M / HDD szükséges / Pi kompatibilis badges - [ ] FileBrowser detail page shows use cases, first steps, prerequisites - [ ] Traefik/cloudflared cards do NOT show "Részletek" (no .felhom.yml/slug) - [ ] Felhom-controller card does NOT show "Részletek" (no .felhom.yml) - [ ] Backup page loads in <500ms (instant, cached) - [ ] Backup page shows correct data after initial cache population - [ ] Manual backup → page shows updated data after completion - [ ] Cache refreshes every 5 minutes (check logs for "[INFO] Backup status cache refreshed") - [ ] No regressions on dashboard, app detail pages, deploy flow