# TASK.md — Tier 2 for All Apps + Status Dot Update (v0.12.9) ## Prompt (copy-paste this into Claude Code) ``` Read TASK.md for the full plan. Apply all code changes described, then build and deploy. After all fixes are done: 1. Run `go build ./...` and `go vet ./...` from the controller/ directory — fix any errors 2. Update CHANGELOG.md with a new entry at the top (session 46, v0.12.9) 3. Commit, build, and deploy following the workflow in CLAUDE.md ``` --- ## Context and Goals Currently Tier 2 (cross-drive backup) is only available for apps with HDD data (Immich, Paperless-ngx, etc.). Apps without HDD data (Mealie, Gokapi, etc.) cannot configure Tier 2 at all — the config section is hidden, the code rejects empty mounts, and the UI shows them as "auto" (gray dot). **Problem:** These apps have only 1 tier of protection. If the primary drive fails, their DB and config are lost. The customer should be able to configure Tier 2 for ANY app. **Changes in this version:** 1. **Tier 2 for all apps** — Remove all HDD-only gates. Non-HDD apps back up config + DB dumps to the secondary drive (small, but protects against drive failure). 2. **Status dot update** — Remove "auto" (gray). All apps start as yellow (1 tier only). Green requires 2+ tiers with successful backups. 3. **Tier 3 placeholder** — Show a disabled "3. mentés" row in the UI (future: remote backup). 4. **Deploy page** — Show cross-drive config form for ALL deployed apps, not just HDD ones. **What non-HDD apps back up in Tier 2:** - App with DB (e.g., Mealie): `_config/` + `_db/mealie_postgres.sql` - App without DB (e.g., Gokapi): `_config/` only - Small files — seconds to rsync/restic, but provides drive-failure protection. --- ## Fix 1: Remove empty-mounts gate from RunAppBackup **File:** `internal/backup/crossdrive.go` In `RunAppBackup()`, the code currently errors out when no HDD mounts exist (lines 98–103): ```go // CURRENT CODE — DELETE these 4 lines: mounts := r.stackProvider.GetStackHDDMounts(stackName) if len(mounts) == 0 { r.updateStatus(stackName, "error", "no HDD data paths found for this app", time.Since(start), "") return fmt.Errorf("no HDD data paths found for %s", stackName) } ``` **Replace with:** ```go // Resolve HDD mounts for this app (may be empty for config-only apps) mounts := r.stackProvider.GetStackHDDMounts(stackName) ``` **Why this works:** The rest of the function already handles empty mounts correctly: - Safety overlap check: empty loop = no overlap → passes - `runRsyncBackup`: mount loop doesn't execute, but DB + config copy still runs - `runResticBackup`: no mount paths appended, but config dir + DB dump dir still included - Size calculation: destDir exists and can be measured even without mount data --- ## Fix 2: Update status dot logic + remove HasHDDData gates from handlers.go **File:** `internal/web/handlers.go` ### 2a: Update `AppBackupRow` struct comments In the `AppBackupRow` struct, update the Tier 2 comment: ```go // Tier 2: Cross-drive backup (configurable for all apps) ``` (Remove the old "(only for apps with HDD data)" comment.) ### 2b: Rewrite `buildAppBackupRows` status + Tier2 section Replace the current status + Tier2 block (lines 605–672): **CURRENT CODE:** ```go // Default status = auto (no user data, just config) row.Status = "auto" row.StatusText = "Automatikus mentés" if app.HasHDDData { cfg, hasCfg := crossConfigs[app.StackName] // ... full Tier2 block ... } ``` **REPLACE WITH:** ```go // Status dot — start as yellow (1 tier only) row.Status = "yellow" row.StatusText = "Csak helyi mentés (1 szint)" cfg, hasCfg := crossConfigs[app.StackName] if !hasCfg || cfg == nil || !cfg.Enabled { // Only Tier 1 — no second copy row.Tier2Configured = false } else { row.Tier2Configured = true row.Tier2Method = cfg.Method row.Tier2MethodLabel = cfg.Method // "rsync" or "restic" row.Tier2Browsable = cfg.Method == "rsync" row.Tier2Dest = destLabels[cfg.DestinationPath] if row.Tier2Dest == "" { row.Tier2Dest = cfg.DestinationPath } switch cfg.Schedule { case "daily": row.Tier2Schedule = "Naponta" case "weekly": row.Tier2Schedule = "Hetente" default: row.Tier2Schedule = cfg.Schedule } if cfg.LastRun != "" { if t, err := time.Parse(time.RFC3339, cfg.LastRun); err == nil { row.Tier2LastRun = t.In(loc).Format("01-02 15:04") } } row.Tier2LastStatus = cfg.LastStatus row.Tier2LastError = cfg.LastError row.Tier2SizeHuman = cfg.LastSizeHuman switch cfg.LastStatus { case "ok": row.Tier2StatusBadge = "Sikeres" row.Status = "green" row.StatusText = "Mentés rendben" case "error": row.Tier2StatusBadge = "Hiba" // Status stays yellow row.StatusText = "Utolsó mentés sikertelen" case "running": row.Tier2StatusBadge = "Fut..." default: row.Tier2StatusBadge = "—" // Tier2 configured but never run — stay yellow } // Destination health check — can downgrade green to yellow/red if cfg.DestinationPath != "" { if err := s.crossDriveRunner.ValidateDestination(cfg.DestinationPath); err != nil { if strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "not writable") { row.Status = "red" row.StatusText = "Mentési cél nem elérhető" } else if row.Status != "red" { row.Status = "yellow" row.StatusText = "Figyelmeztetés" } row.Warnings = append(row.Warnings, err.Error()) } } } ``` Note: `s.crossDriveRunner` is used instead of `s.crossDrive` — verify the field name in `server.go` and use whatever the actual field is called. (The current code at the old line 657 shows `s.crossDriveRunner.ValidateDestination`.) ### 2c: Remove HasHDDData gate from cross-drive summary builder In `backupsHandler`, the cross-drive summary loop (around line 409) has: ```go for _, app := range fullStatus.AppDataInfo { if !app.HasHDDData { continue } ``` **Remove** the `if !app.HasHDDData { continue }` — all apps participate in cross-drive summary. ### 2d: Update the top-level warning logic The "NoUserDataBackupWarning" check (around line 473) uses `HasHDDData` — **keep this as-is**. This warning is specifically about user data (photos, documents) being at risk, which only applies to HDD apps. The status dot change already incentivizes Tier 2 for all apps. --- ## Fix 3: Update backup page template **File:** `internal/web/templates/backups.html` ### 3a: Remove HasHDDData gate on Tier 2 row **Find** (around line 284): ```html {{if .HasHDDData}}
2. mentés ``` **Remove** the `{{if .HasHDDData}}` opening and its matching `{{end}}` (around line 313). The Tier 2 row should always be shown for all apps. ### 3b: Update header meta badges **Find** (around line 256–261): ```html {{if .HasHDDData}} {{if .StorageLabel}}{{.StorageLabel}}{{end}} {{.HDDSizeHuman}} {{else}} Auto {{end}} ``` **Replace with:** ```html {{if .HasHDDData}} {{if .StorageLabel}}{{.StorageLabel}}{{end}} {{.HDDSizeHuman}} {{else}} Konfig{{if .HasDB}} + DB{{end}} {{end}} ``` This shows what type of data the app has (instead of meaningless "Auto"). ### 3c: Add Tier 3 placeholder row After the Tier 2 `
` (the closing div of the tier-2 backup-layer-row), add: ```html
3. mentés Hamarosan távoli (offsite) B2 / S3 / SFTP — hamarosan elérhető
``` ### 3d: Update "Run all" button text **Find** (around line 328): ```html ``` **Replace with:** ```html ``` --- ## Fix 4: Deploy page — show cross-drive config for all deployed apps **File:** `internal/web/templates/deploy.html` ### 4a: Remove StorageInfo gate from cross-drive section The cross-drive backup config section is currently double-gated (lines 95–220): ```html {{if .AlreadyDeployed}} {{if .StorageInfo}} ← THIS IS THE GATE — remove it
...
{{end}} ← remove matching end {{end}} ``` **Change to** (keep only the AlreadyDeployed gate): ```html {{if .AlreadyDeployed}}
...
{{end}} ``` ### 4b: Update section text for generality **Find** (line 109): ```html

Másolat másik meghajtóra (felhasználói adatok):

``` **Replace with:** ```html

2. mentés — másolat másik meghajtóra:

``` **Find** (line 214–216): ```html
A cél meghajtó legyen más fizikai eszköz, mint az alkalmazás adattárolója.
``` **Replace with:** ```html
A cél meghajtó legyen más fizikai eszköz a meghibásodás elleni védelem érdekében.
``` --- ## Summary of all changes | Fix | What | File(s) | |-----|------|---------| | 1 | Remove `len(mounts) == 0` error gate | `crossdrive.go` | | 2a | Update `AppBackupRow` Tier2 comment | `handlers.go` | | 2b | Rewrite status + Tier2 block (remove HasHDDData gate, new dot logic) | `handlers.go` | | 2c | Remove HasHDDData gate from cross-drive summary | `handlers.go` | | 3a | Remove `{{if .HasHDDData}}` around Tier 2 row | `backups.html` | | 3b | Update meta badges ("Auto" → "Konfig + DB") | `backups.html` | | 3c | Add Tier 3 placeholder row | `backups.html` | | 3d | Rename "Összes HDD mentés" → "Összes 2. mentés" | `backups.html` | | 4a | Remove `{{if .StorageInfo}}` gate from cross-drive section | `deploy.html` | | 4b | Update cross-drive section text for generality | `deploy.html` | ## Files to modify (4) 1. `internal/backup/crossdrive.go` — Fix 1 2. `internal/web/handlers.go` — Fix 2a + 2b + 2c 3. `internal/web/templates/backups.html` — Fix 3a + 3b + 3c + 3d 4. `internal/web/templates/deploy.html` — Fix 4a + 4b ## Status dot logic after fix | Dot color | Meaning | |-----------|---------| | Green | 2+ tiers with successful backups + destination healthy | | Yellow | Only 1 tier, or Tier 2 failing, or Tier 2 configured but never run | | Red | Tier 2 destination blocked/inaccessible | **"auto" (gray) is removed.** Every app now shows yellow or better. ## Architecture after fix ``` Per-app Tier 2 availability: ┌──────────────────────────────────────────────────────────┐ │ App type │ Tier 1 │ Tier 2 (new) │ │───────────────────│───────────────────│──────────────────│ │ HDD + DB │ Config+DB+Data │ Config+DB+Data │ │ HDD, no DB │ Config+Data │ Config+Data │ │ DB, no HDD │ Config+DB │ Config+DB (new!) │ │ Config only │ Config │ Config (new!) │ └──────────────────────────────────────────────────────────┘ UI per-app display after fix: ┌─────────────────────────────────────────────────────────────┐ │ 🟢 Immich Külső tárhely (hdd_1) 63.9 MB │ │ 1. mentés Auto helyi 02-18 03:00 ✓ DB+Konfig+Adatok │ │ 2. mentés rsync → hdd_1 Naponta Sikeres 📁 │ │ 3. mentés Hamarosan távoli (offsite) │ ├─────────────────────────────────────────────────────────────┤ │ 🟡 Mealie Konfig + DB │ │ 1. mentés Auto helyi 02-18 03:00 ✓ DB+Konfig │ │ 2. mentés ✓ 1. mentés auto ⚠ Nincs 2. másolat │ │ 3. mentés Hamarosan távoli (offsite) │ ├─────────────────────────────────────────────────────────────┤ │ 🟡 Gokapi Konfig │ │ 1. mentés Auto helyi 02-18 03:00 ✓ Konfig │ │ 2. mentés ⚠ Nincs 2. másolat [Beállítás →] │ │ 3. mentés Hamarosan távoli (offsite) │ └─────────────────────────────────────────────────────────────┘ ``` ## Post-fix checklist - [ ] `go build ./...` passes - [ ] `go vet ./...` passes - [ ] Verify no references to old "auto" status remain in handlers.go - [ ] Verify no template references to removed `{{if .HasHDDData}}` gate on Tier 2 - [ ] Update `CHANGELOG.md` — session 46, version **v0.12.9**: - Tier 2 cross-drive backup now configurable for ALL apps (not just HDD apps) - Non-HDD apps back up config + DB dumps to secondary drive - Status dot: removed "auto" (gray) — all apps start yellow, green requires 2+ tiers - Tier 3 placeholder row shown in UI - Deploy page: cross-drive config form visible for all deployed apps - Updated button text "Összes 2. mentés futtatása most" - [ ] Commit, build on 192.168.0.180, deploy on 192.168.0.162 - [ ] Verify with `docker ps` and `docker logs` - [ ] After deploy, verify: - Immich: green dot (Tier 2 configured + successful backup) - Mealie: yellow dot with "Csak helyi mentés (1 szint)" - Mealie: Tier 2 row shown with "⚠ Nincs 2. másolat" + "Beállítás →" link - Mealie deploy page: cross-drive config form visible - Configure Tier 2 for Mealie → run manual backup → verify dot turns green - Tier 3 placeholder row shown for all apps (grayed out "Hamarosan") - Gokapi: yellow dot, Tier 2 configurable