```
-("Telepített alkalmazások" = "Installed applications")
-
-No other template changes needed — the `{{range .Stacks}}` loop just iterates over fewer items.
-
-### Edge case
-
-If no user apps are deployed yet (fresh install), the section still shows protected infra stacks (traefik, cloudflared, felhom-controller, filebrowser) — this is reassuring to the customer.
-
----
-
-## Task 3: FileBrowser — show URL + allow restart on protected stacks
-
-### Current behavior
-
-Protected stacks show only "Védett rendszerkomponens" badge with no actions or URL link, on both the dashboard compact list and the Alkalmazások detail cards.
-
-### New behavior
-
-For protected stacks that are **operational** (running):
-- Show the subdomain URL link if configured (e.g., `files.demo-felhom.eu ↗`)
-- Show "Újraindítás" (Restart) button
-- Keep "Védett" badge
-- NO stop/start/delete/update buttons (still protected from destructive actions)
-
-### The subdomain problem
-
-FileBrowser is deployed by `docker-setup.sh` as infrastructure — it may NOT have a `.felhom.yml` metadata file with the `subdomain` field set. Without it, the controller doesn't know FileBrowser's subdomain.
-
-**Solution**: Add `.felhom.yml` creation to the `install_filebrowser()` function in `scripts/docker-setup.sh`.
-
-For the demo node, create it manually after this deploy:
-
-```bash
-sudo tee /opt/docker/stacks/filebrowser/.felhom.yml << 'EOF'
-display_name: Filebrowser
-slug: filebrowser
-description: Fájlkezelő a külső merevlemezhez
-subdomain: files
-category: storage
-app_info:
- tagline: Web-alapú fájlkezelő a külső merevlemezhez
- use_cases:
- - Fájlok böngészése és letöltése a külső HDD-ről
- - Médiafájlok megosztása családtagokkal
- - Dokumentumok feltöltése és kezelése
- first_steps:
- - Nyisd meg a files.DOMAIN címet a böngészőben
- - Jelentkezz be az admin fiókkal
- - Tallózd a /srv mappákat
- prerequisites:
- - Külső HDD csatlakoztatva és felcsatolva
-EOF
-```
-
-### Template changes — stacks.html (Alkalmazások page)
-
-Update the protected stack actions section in the detail card:
+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}}
{{end}}
-{{else if not .Deployed}}
- ...existing deploy button...
-```
-
-The subdomain URL is already shown above the actions for all stacks — it uses `{{if .Meta.Subdomain}}`. If `.felhom.yml` has `subdomain: files`, it should render automatically. Verify this is not gated behind `{{if not .Protected}}` — if it is, remove that guard.
-
-### Template changes — dashboard.html (Vezérlőpult page)
-
-In the compact stack list, update the protected stack section similarly:
-
-```html
-{{if .Protected}}
-
{{end}}
{{else if not .Deployed}}
```
-### Stack action handler check
+**dashboard.html**: Same `data-href` fix for the compact card:
-In `internal/stacks/manager.go`, check if the action handler blocks restart on protected stacks. If there's a guard like:
-
-```go
-if stack.Protected {
- return fmt.Errorf("cannot perform action on protected stack")
-}
+Change from:
+```html
+
+```
+To:
+```html
+
```
-Change it to only block destructive actions:
+### Fix — FileBrowser .felhom.yml (resources)
-```go
-if stack.Protected && action != "restart" {
- return fmt.Errorf("cannot %s protected stack %s", action, name)
-}
+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
```
-**CRITICAL**: Only `restart` is allowed. `stop`, `start`, `update`, `delete` must remain blocked for protected stacks.
-
-### docker-setup.sh changes
-
-In `install_filebrowser()`, add `.felhom.yml` creation after the docker-compose.yml:
+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
-# Create .felhom.yml metadata
-cat > "${FILEBROWSER_DIR}/.felhom.yml" << 'METAEOF'
-display_name: Filebrowser
-slug: filebrowser
-description: Fájlkezelő a külső merevlemezhez
-subdomain: files
-category: storage
-app_info:
- tagline: Web-alapú fájlkezelő a külső merevlemezhez
- use_cases:
- - Fájlok böngészése és letöltése a külső HDD-ről
- - Médiafájlok megosztása családtagokkal
- - Dokumentumok feltöltése és kezelése
- first_steps:
- - Nyisd meg a files.DOMAIN címet a böngészőben
- - Jelentkezz be az admin fiókkal
- - Tallózd a /srv mappákat
- prerequisites:
- - Külső HDD csatlakoztatva és felcsatolva
-METAEOF
+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: MariaDB validation fix
-1. Fix `ValidateDump()` in `internal/backup/dbdump.go` — scan first 10 lines for header
+### 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: Dashboard deployed-only
-1. Filter stacks in `dashboardHandler()` in `handlers.go`
-2. Update heading in `dashboard.html` to "Telepített alkalmazások"
+### 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: Protected stack UX
-1. Update `stacks.html` protected section — add restart button, ensure URL link not gated
-2. Update `dashboard.html` protected section — same (compact form)
-3. Check `internal/stacks/manager.go` — allow restart on protected stacks
-4. Add `.felhom.yml` creation to `install_filebrowser()` in `scripts/docker-setup.sh`
+### 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: Build, deploy, verify
-1. Build v0.4.6
-2. Deploy to demo node (sync full docker-compose.yml)
-3. Manually create `/opt/docker/stacks/filebrowser/.felhom.yml` on demo node
-4. Trigger backup → verify MariaDB green validation
-5. Check dashboard shows only deployed apps
-6. Check FileBrowser shows URL and restart button
-
-### Step 5: Documentation
+### Step 4: Documentation
1. Update CONTEXT.md, README
2. Bump version
@@ -277,25 +290,25 @@ METAEOF
## Files to modify
```
-internal/backup/dbdump.go — fix ValidateDump() header check
-internal/web/handlers.go — filter deployed-only in dashboardHandler()
-internal/web/templates/dashboard.html — heading + protected stack restart
-internal/web/templates/stacks.html — protected stack: ensure URL visible + restart button
-scripts/docker-setup.sh — create .felhom.yml in install_filebrowser()
-internal/stacks/manager.go — allow restart action on protected stacks
+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
-- [ ] MariaDB (romm) shows green validation badge with table count on backup page
-- [ ] Dashboard heading is "Telepített alkalmazások"
-- [ ] Dashboard shows only deployed + protected apps (no "Nincs telepítve" entries)
-- [ ] Dashboard stat cards still show correct total (52 apps)
-- [ ] FileBrowser shows `files.demo-felhom.eu ↗` link on Alkalmazások page
-- [ ] FileBrowser shows "Újraindítás" button on both pages
-- [ ] Restart works on FileBrowser
-- [ ] Other protected stacks also show restart when operational
-- [ ] Stop/delete/update still blocked for all protected stacks
-- [ ] No regressions on backup page, app detail pages, deploy flow
\ No newline at end of file
+- [ ] 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
\ No newline at end of file