Files
deploy-felhom-compose/TASK.md
T

15 KiB
Raw Blame History

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 98103):

// 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 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:

	// 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 605672):

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 HasHDDDatakeep 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 256261):

                {{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 95220):

    {{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 214216):

        <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)

  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