15 KiB
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:
- 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).
- Status dot update — Remove "auto" (gray). All apps start as yellow (1 tier only). Green requires 2+ tiers with successful backups.
- Tier 3 placeholder — Show a disabled "3. mentés" row in the UI (future: remote backup).
- 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):
// 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:
// 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 runsrunResticBackup: 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:
// 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:
// 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:
// 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:
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):
{{if .HasHDDData}}
<div class="backup-layer-row">
<span class="tier-label">2. mentés</span>
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):
{{if .HasHDDData}}
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
{{else}}
<span class="meta-badge">Auto</span>
{{end}}
Replace with:
{{if .HasHDDData}}
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="mono app-backup-size" style="font-size:.8rem">{{.HDDSizeHuman}}</span>
{{else}}
<span class="meta-badge">Konfig{{if .HasDB}} + DB{{end}}</span>
{{end}}
This shows what type of data the app has (instead of meaningless "Auto").
3c: Add Tier 3 placeholder row
After the Tier 2 </div> (the closing div of the tier-2 backup-layer-row), add:
<!-- Tier 3: Remote backup (future) -->
<div class="backup-layer-row" style="opacity:.5">
<span class="tier-label">3. mentés</span>
<span class="layer-badge" style="background:var(--bg-tertiary);color:var(--text-muted)">Hamarosan</span>
<span class="tier-location">távoli (offsite)</span>
<span class="tier-contents" style="font-style:normal;color:var(--text-muted)">B2 / S3 / SFTP — hamarosan elérhető</span>
</div>
3d: Update "Run all" button text
Find (around line 328):
<button class="btn btn-sm btn-outline" onclick="triggerAllCrossDrive(this)">Összes HDD mentés futtatása most</button>
Replace with:
<button class="btn btn-sm btn-outline" onclick="triggerAllCrossDrive(this)">Összes 2. mentés futtatása most</button>
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):
{{if .AlreadyDeployed}}
{{if .StorageInfo}} ← THIS IS THE GATE — remove it
<div class="deploy-cross-drive">
...
</div>
{{end}} ← remove matching end
{{end}}
Change to (keep only the AlreadyDeployed gate):
{{if .AlreadyDeployed}}
<div class="deploy-cross-drive">
...
</div>
{{end}}
4b: Update section text for generality
Find (line 109):
<p style="font-weight:500;margin-bottom:1rem">Másolat másik meghajtóra (felhasználói adatok):</p>
Replace with:
<p style="font-weight:500;margin-bottom:1rem">2. mentés — másolat másik meghajtóra:</p>
Find (line 214–216):
<div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)">
A cél meghajtó legyen más fizikai eszköz, mint az alkalmazás adattárolója.
</div>
Replace with:
<div class="form-hint" style="margin-top:.75rem;color:var(--text-muted)">
A cél meghajtó legyen más fizikai eszköz a meghibásodás elleni védelem érdekében.
</div>
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)
internal/backup/crossdrive.go— Fix 1internal/web/handlers.go— Fix 2a + 2b + 2cinternal/web/templates/backups.html— Fix 3a + 3b + 3c + 3dinternal/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 ./...passesgo 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 psanddocker 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