# 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}}
Másolat másik meghajtóra (felhasználói adatok):
``` **Replace with:** ```html2. mentés — másolat másik meghajtóra:
``` **Find** (line 214–216): ```html