11 KiB
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:
- No click-through:
data-href="/apps/{{.Meta.Slug}}"is gated behind{{if not .Protected}}on bothdashboard.htmlandstacks.html - No "Részletek" button: The protected section in
stacks.htmlonly shows "Védett rendszerkomponens" badge + restart button — no "Részletek" link - Detail page works: Manually navigating to
/apps/filebrowserrenders 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:
<div class="stack-detail-card ..." {{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
To:
<div class="stack-detail-card ..." {{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
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:
{{if .Protected}}
<span class="badge badge-protected">Védett rendszerkomponens</span>
{{if isOperational .State}}
<button class="btn btn-warning" onclick="stackAction('{{.Name}}', 'restart')">Újraindítás</button>
{{end}}
{{if .Meta.Slug}}
<a href="/apps/{{.Meta.Slug}}" class="btn btn-outline">Részletek</a>
{{end}}
{{else if not .Deployed}}
dashboard.html: Same data-href fix for the compact card:
Change from:
<div class="stack-card ..." {{if not .Protected}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
To:
<div class="stack-card ..." {{if .Meta.Slug}} data-href="/apps/{{.Meta.Slug}}"{{end}}>
Fix — FileBrowser .felhom.yml (resources)
The manually created .felhom.yml on the demo node is missing resources. Update it to include:
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):
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/filebrowserdetail 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:
m.restic.Stats()— executesrestic stats --json+restic snapshots --json(~2-3 seconds)ListDumpFiles()— directory listing (fast, ~1ms)DiscoverDatabases()—docker ps+docker inspectper 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:
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()
// 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
// 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:
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:
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
NextDailyRuna standalone utility function that both packages can import - Or just call
RefreshCachefrommain.govia 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):
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
- Fix
data-hrefgating instacks.htmlanddashboard.html— use{{if .Meta.Slug}}instead of{{if not .Protected}} - Add "Részletek" button to protected stack section in
stacks.html - Update
install_filebrowser()indocker-setup.sh— addresourcesto.felhom.yml
Step 2: Backup page caching
- Add
cachedStatus,cacheTimefields toManager - Create
RefreshCache()method - Modify
GetFullStatus()to read from cache - Register
backup-cachescheduler job inmain.go - Call
RefreshCache()at end ofRunFullBackup()andRunBackup() - Add initial cache goroutine in
main.go
Step 3: Build, deploy, verify
- Build v0.4.7
- Deploy to demo node
- Update
/opt/docker/stacks/filebrowser/.felhom.ymlon demo node (add resources) - Verify FileBrowser card is clickable → detail page with memory badges
- Verify backup page loads instantly
- Trigger manual backup → verify page updates after completion
Step 4: Documentation
- Update CONTEXT.md, README
- 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