405 lines
15 KiB
Markdown
405 lines
15 KiB
Markdown
# 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}}
|
||
<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):
|
||
```html
|
||
{{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:**
|
||
```html
|
||
{{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:
|
||
|
||
```html
|
||
<!-- 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):
|
||
```html
|
||
<button class="btn btn-sm btn-outline" onclick="triggerAllCrossDrive(this)">Összes HDD mentés futtatása most</button>
|
||
```
|
||
|
||
**Replace with:**
|
||
```html
|
||
<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):
|
||
|
||
```html
|
||
{{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):
|
||
|
||
```html
|
||
{{if .AlreadyDeployed}}
|
||
<div class="deploy-cross-drive">
|
||
...
|
||
</div>
|
||
{{end}}
|
||
```
|
||
|
||
### 4b: Update section text for generality
|
||
|
||
**Find** (line 109):
|
||
```html
|
||
<p style="font-weight:500;margin-bottom:1rem">Másolat másik meghajtóra (felhasználói adatok):</p>
|
||
```
|
||
|
||
**Replace with:**
|
||
```html
|
||
<p style="font-weight:500;margin-bottom:1rem">2. mentés — másolat másik meghajtóra:</p>
|
||
```
|
||
|
||
**Find** (line 214–216):
|
||
```html
|
||
<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:**
|
||
```html
|
||
<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)
|
||
|
||
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
|