diff --git a/TASK.md b/TASK.md index 9029123..1b9be42 100644 --- a/TASK.md +++ b/TASK.md @@ -1,274 +1,287 @@ -# TASK.md — v0.4.6: MariaDB Validation Fix + Dashboard & Protected Stack UX +# TASK.md — v0.4.7: Protected Stack Detail Pages + Backup Page Caching -> Version bump: **v0.4.6** -> Scope: Bug fix + 2 UI improvements +> Version bump: **v0.4.7** +> Scope: UX fix + performance fix --- -## Overview +## Task 1: Protected stacks — enable detail page + click-through -Three items: +### Problem -1. **Bugfix**: MariaDB dump validation false positive — header check fails because MariaDB 11.4+ prepends a sandbox comment before the dump header -2. **UI**: Dashboard should only show deployed apps (not 47 "Nincs telepítve" entries) -3. **UI**: Protected stacks (especially FileBrowser) should show subdomain URL + allow restart +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 -## Task 1: Fix MariaDB dump validation false positive +### Fix — Templates -### Root cause - -MariaDB 11.4+ prepends a sandbox directive before the header comment: - -```sql -/*M!999999\- enable the sandbox mode */ --- MariaDB dump 10.19-11.4.10-MariaDB, for debian-linux-gnu (x86_64) -``` - -The validation code checks only the first line for `-- MariaDB dump` or `-- MySQL dump`, but line 1 is now `/*M!999999...*/`. So the header check fails with "MariaDB dump missing comment header". - -### Fix - -In `ValidateDump()` (file: `internal/backup/dbdump.go`), change the header check to scan the **first 10 lines** (not just line 1) for the expected pattern. Also accept `/*` and `/*!` lines as valid preamble. - -For MariaDB/MySQL dumps, valid header patterns (any of these in the first 10 lines): -- `-- MariaDB dump` -- `-- MySQL dump` -- `-- mysqldump` - -For PostgreSQL dumps, valid header patterns: -- `-- PostgreSQL database dump` - -### Implementation - -Find the header validation logic in `ValidateDump()`. Replace the "first line must be" check with a loop over the first 10 lines: - -```go -// Check header — scan first 10 lines for expected dump header -scanner := bufio.NewScanner(file) -headerFound := false -linesChecked := 0 -for scanner.Scan() && linesChecked < 10 { - line := scanner.Text() - linesChecked++ - switch dbType { - case DBTypeMariaDB: - if strings.HasPrefix(line, "-- MariaDB dump") || - strings.HasPrefix(line, "-- MySQL dump") || - strings.HasPrefix(line, "-- mysqldump") { - headerFound = true - } - case DBTypePostgres: - if strings.HasPrefix(line, "-- PostgreSQL database dump") { - headerFound = true - } - } - if headerFound { - break - } -} -``` - -### Verification - -After deploying, trigger a manual backup ("Mentés most") and check the Adatbázisok table on the backup page. The romm MariaDB entry should show a green validation badge with table count instead of "Hiba". - ---- - -## Task 2: Dashboard — show only deployed apps - -### Current behavior - -The "Alkalmazások állapota" section on the Vezérlőpult page shows **all** apps (deployed + not deployed), meaning 47+ "Nincs telepítve / Telepítés" entries clutter the dashboard. - -### New behavior - -Only show **deployed** apps (including protected infra stacks) on the dashboard. Non-deployed apps remain accessible via the Alkalmazások page. - -### Implementation - -In `dashboardHandler()` (`internal/web/handlers.go`), filter the stack list before passing to the template: - -```go -// Filter to deployed-only for dashboard -stackList := s.stackMgr.GetStacks() -var deployedStacks []stacks.Stack -for _, st := range stackList { - if st.Deployed || st.Protected { - deployedStacks = append(deployedStacks, st) - } -} -``` - -Pass `deployedStacks` as `data["Stacks"]` to the template instead of the full `stackList`. - -**Important**: The `TotalCount` stat card should still show the total count of all apps (deployed + not deployed), not just the filtered list. Keep using `len(stackList)` for that. - -### Template change - -In `dashboard.html`, update the section heading: +**stacks.html**: Add `data-href` for protected stacks that have a slug, and add "Részletek" button: +Change the card opening div from: ```html -

Telepített alkalmazások

+
+``` +To: +```html +
``` -("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}} Védett rendszerkomponens {{if isOperational .State}} {{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}} - Védett - {{if isOperational .State}} - + {{if .Meta.Slug}} + Részletek {{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