From e002d712cfc75a0302506886db104f582d4d8b6c Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 17 Feb 2026 17:47:21 +0100 Subject: [PATCH] =?UTF-8?q?Backup=20Page=20Overhaul=20=E2=80=94=20Unified?= =?UTF-8?q?=20App=20Backup=20Status=20&=20Bug=20Fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TASK.md | 856 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 688 insertions(+), 168 deletions(-) diff --git a/TASK.md b/TASK.md index 22cb746..402c6e0 100644 --- a/TASK.md +++ b/TASK.md @@ -1,204 +1,724 @@ -# TASK — Simplify Backup Page "Alkalmazás adatok" Section +# TASK: Backup Page Overhaul — Unified App Backup Status & Bug Fixes -## Goal +## Summary -Replace the current complex app data section on the backup page (checkboxes, paths, docker volumes, save button) with a simple, read-only status list of all deployed apps. +Overhaul the "Biztonsági mentés" page to show a single unified per-app backup overview with expandable detail rows, fix critical bugs (duplicate apps, misleading errors, dead toggles), and implement sequential backup chaining so cross-drive backups run after the nightly backup completes. -## Current state (v0.11.9) +--- -The backup page shows an "Alkalmazás adatok" section with: -- Checkboxes per app to toggle backup on/off -- Full HDD path listings with sizes -- Docker volume names with "(nem mentett)" labels -- DB dump status per app -- A "Mentés" (save) button +## Bug Fixes (Critical) -**Problems:** -- Too much detail for the customer — confusing -- Docker volume names shown as "nem mentett" can mislead users into thinking DB data isn't backed up (it is, via DB dump) -- Backup enable/disable should be configured on the app's own settings page, not here -- The section should be informational only on this page +### Bug 1: Duplicate unconfigured apps on every page load -## Desired state +**Root cause:** `GetFullStatus()` returns a pointer to `m.cachedStatus`. The `backupsHandler()` then appends to `UnconfiguredApps` and `CrossDriveSummary` on that cached object. Every page load appends again → 3 apps × 3 loads = 9 entries. -A clean, read-only list of ALL deployed apps (including those without user data) showing a simple status per app: +**File:** `internal/backup/backup.go` — `GetFullStatus()` -``` -┌────────────────────────────────────────────────────────┐ -│ Alkalmazás adatok │ -│ Az alkalmazások felhasználói adatainak mentési │ -│ állapota. Beállítás az alkalmazás oldalán. │ -│ │ -│ Immich Külső tárhely (hdd_1) ● Aktív │ -│ Paperless-ngx Külső tárhely (hdd_p.) ○ Inaktív │ -│ Gokapi — N/A │ -│ Mealie — N/A │ -│ RoMM Külső tárhely (hdd_p.) ○ Inaktív │ -└────────────────────────────────────────────────────────┘ +**Fix:** Return a **deep copy** of the cached status. Specifically, `CrossDriveSummary`, `UnconfiguredApps`, `CrossDriveWarnings`, and `AppDataInfo` slices must not share backing arrays with the cache. + +```go +func (m *Manager) GetFullStatus(nextDBDump, nextBackup time.Time) *FullBackupStatus { + m.mu.Lock() + defer m.mu.Unlock() + + if m.cachedStatus != nil { + // Deep copy to prevent callers from mutating cached slices + status := *m.cachedStatus + status.AppDataInfo = make([]AppBackupInfo, len(m.cachedStatus.AppDataInfo)) + copy(status.AppDataInfo, m.cachedStatus.AppDataInfo) + status.CrossDriveSummary = nil // rebuilt by handler + status.UnconfiguredApps = nil // rebuilt by handler + status.CrossDriveWarnings = nil // rebuilt by handler + // ... (keep existing dynamic field updates on &status) + return &status + } + // ... (rest unchanged) +} ``` -Each app name links to its deploy/settings page where backup can be configured. +**Alternative (simpler):** Don't populate `CrossDriveSummary`/`UnconfiguredApps` in the returned status at all — move that logic into a separate method or let `backupsHandler` build them from `AppDataInfo` + settings. This is cleaner architecturally since the cache shouldn't hold UI-assembly data. -**Status indicators (no emoji, just styled text/dots):** -- `● Aktív` — green dot + text — backup enabled for this app (has HDD data and toggle is on) -- `○ Inaktív` — gray dot + text — has HDD data but backup not enabled -- `— N/A` — muted dash + text — no user data folder, backup not applicable (this is fine, not a warning) +**Recommendation:** Alternative approach. The handler already builds this data; the cache should only hold the expensive-to-compute parts (app discovery, restic stats, snapshot history). -**Optional:** For apps with "Aktív" status, show the data size in muted text next to the storage label (e.g., `47.9 MB`). +### Bug 2: Misleading error message for non-mount-point destinations -## Changes +**Root cause:** When cross-drive destination is configured to `/mnt/hdd_placeholder`, `IsMountPoint()` returns `false` (it's a directory on the system SSD, not a separate mount point). The code then shows: *"A cél tárhely (/mnt/hdd_placeholder) nem elérhető! Ellenőrizd a meghajtó csatlakozását."* — suggesting a disconnected drive, which is incorrect. -### 1. `backups.html` — Replace Section 4 +**File:** `internal/web/handlers.go` — `deployHandler()` (line ~13818), `backupsHandler()` (line ~14036) -Replace everything from `` through the closing `{{end}}` (lines 16199-16247) with: +**Current logic:** +```go +if !system.IsMountPoint(path) || !system.IsWritable(path) { + // "nem elérhető! Ellenőrizd a meghajtó csatlakozását." +} +``` + +**Fix:** Replace the single check with tiered validation: + +```go +func validateBackupDestination(path string) (warning string, blocked bool) { + // 1. Path doesn't exist at all + if _, err := os.Stat(path); os.IsNotExist(err) { + return "A cél tárhely (" + path + ") nem létezik!", true + } + // 2. Path exists but not writable + if !system.IsWritable(path) { + return "A cél tárhely (" + path + ") nem írható! Ellenőrizd a jogosultságokat.", true + } + // 3. Path is on the system drive (not a separate mount point) + if !system.IsMountPoint(path) { + return "A cél tárhely (" + path + ") a rendszermeghajtón van. " + + "Meghajtóhiba esetén az eredeti adat és a mentés is elveszhet. " + + "Külső meghajtó használata javasolt.", false // warning, not blocked + } + // 4. All good + return "", false +} +``` + +- `blocked = true` → red alert, backup should not run +- `blocked = false, warning != ""` → yellow alert, backup can run but user is informed +- No warning → green + +### Bug 3: Dead `BackupEnabled` toggle + +**Root cause:** The `BackupEnabled` flag per app (toggled via `settingsAppBackupHandler`) saves to `settings.json` but nothing reads it to actually include/exclude apps from any backup process. There is no Backrest deployment; the nightly restic backup is a cron-driven script that doesn't consult settings.json. + +**Fix:** Remove the `settingsAppBackupHandler` and the bulk toggle form from the backup page. This toggle is replaced by the new unified per-app status rows (see Architecture section below). The concept of "include this app in nightly backup" becomes implicit: if the app has a DB, it gets dumped; if it has Docker volumes, they're in the restic paths; if it has HDD data, cross-drive must be explicitly configured. + +**Files to modify:** +- `internal/web/handlers.go` — remove `settingsAppBackupHandler()` +- `internal/web/server.go` — remove route registration for `POST /settings/app-backup` +- `internal/web/templates/backups.html` — remove the bulk toggle form +- `internal/settings/settings.go` — deprecate `SetAppBackupBulk()`, `IsAppBackupEnabled()` (keep getters for migration, remove after one version) + +--- + +## Architecture Changes + +### 1. Backup Coverage Model + +Each deployed app has up to 3 backup layers: + +| Layer | What | How | Needs user config? | +|-------|------|-----|-------------------| +| **DB dump** | PostgreSQL/MariaDB databases | `backup-db-dump.sh` via systemd timer | No (auto-discovered) | +| **Docker volumes** | App configs, state, SQLite DBs | Nightly restic snapshot of named volumes | No (automatic for all deployed apps) | +| **User data (HDD)** | Photos, documents, media files | Cross-drive rsync/restic to second drive | **Yes** — requires second drive + config | + +**Status colors per app:** + +| Color | Meaning | +|-------|---------| +| **Green** | All applicable backup layers are configured and last run was successful | +| **Yellow** | Warning: last backup run failed/stale, OR destination drive has low space, OR backup is to system drive | +| **Red** | Critical: app has HDD user data but no cross-drive backup configured, OR backup destination unreachable/disconnected | +| **Gray/Auto** | App has no user-configurable backup needs (DB + volumes only, fully automatic) | + +**Logic for computing status per app:** + +``` +if app.HasHDDData: + if no cross-drive configured: + → RED ("Felhasználói adatokról nincs mentés") + elif destination unreachable/disconnected: + → RED ("Mentési cél nem elérhető") + elif last cross-drive run failed: + → YELLOW ("Utolsó mentés sikertelen") + elif destination is system drive: + → YELLOW ("Mentés a rendszermeghajtóra — külső meghajtó javasolt") + elif destination drive >85% full: + → YELLOW ("Mentési meghajtó majdnem megtelt") + else: + → GREEN +else: + → GREEN/AUTO (no user action needed) + +# Additionally, cross-cutting checks: +if app.HasDB and last DB dump for this app failed: + → YELLOW (even if user data is fine) +``` + +### 2. Unified Per-App Backup Rows (UI) + +**Replace** the current two sections ("Alkalmazás adatok" + "Másolatok másik meghajtóra") with a **single section** containing one expandable row per deployed app. + +#### Collapsed row (default): + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ ● Immich Külső tárhely (hdd_1) 92 MB ▶ Részletek │ +│ ● Paperless-ngx Külső tárhely (hdd_1) 12 MB ▶ Részletek │ +│ ● Gokapi Auto ▶ Részletek │ +│ ● Mealie Auto ▶ Részletek │ +│ ● RomM Külső tárhely (hdd_1) 340 MB ▶ Részletek │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +- `●` = color indicator (green/yellow/red) +- Storage label + size only shown for apps with HDD data +- "Auto" badge for apps with only automatic backups +- `▶ Részletek` = expand button (triangle/chevron) + +#### Expanded row (after clicking): + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ ● Immich Külső tárhely (hdd_1) 92 MB ▼ Részletek │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ Adatbázis mentés Auto Utolsó: 02-17 02:31 │ │ +│ │ Docker kötetek Auto Utolsó: 02-17 03:02 │ │ +│ │ Felhasználói adatok rsync → Külső HDD (hdd_1) │ │ +│ │ Ütemezés: Naponta │ │ +│ │ Utolsó: 02-17 03:35 Sikeres │ │ +│ │ [Beállítás] [Futtatás most] │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +├──────────────────────────────────────────────────────────────────────┤ +│ ● Gokapi Auto ▼ Részletek │ +│ ┌────────────────────────────────────────────────────────────────┐ │ +│ │ Adatbázis mentés — (nincs adatbázis) │ │ +│ │ Docker kötetek Auto Utolsó: 02-17 03:02 │ │ +│ │ Felhasználói adatok — (nincs HDD adat) │ │ +│ └────────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**For apps with HDD data but NO cross-drive configured (RED status):** + +``` +│ Felhasználói adatok ⚠ Nincs beállítva │ +│ Mentés beállítása az alkalmazás │ +│ oldalán: [Immich beállítások →] │ +``` + +**For apps with HDD data and destination is system drive (YELLOW):** + +``` +│ Felhasználói adatok rsync → Rendszer SSD (hdd_placeholder) │ +│ ⚠ Rendszermeghajtóra mentés — │ +│ meghajtóhiba esetén mindkét másolat │ +│ elveszhet. Külső meghajtó javasolt. │ +``` + +#### No second drive at all — top-level warning + +If zero apps have cross-drive configured AND at least one app has HDD data, show a prominent card above the app list: + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ ⚠ Felhasználói adatokról nincs biztonsági mentés │ +│ │ +│ A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg │ +│ csak egy példányban léteznek. Külső meghajtó csatlakoztatásával │ +│ biztonsági másolat készíthető a 3-2-1 szabály szerint. │ +│ │ +│ [Meghajtó beállítása →] │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +### 3. Sequential Backup Chaining + +**Current flow:** +``` +02:30 systemd timer → backup-db-dump.sh (DB dumps) +03:00 cron/scheduler → restic backup (Docker volumes + DB dumps) +03:30 independent scheduler → cross-drive jobs (own schedule) +``` + +**New flow:** +``` +02:30 systemd timer → backup-db-dump.sh (DB dumps) +03:00 scheduler → restic backup (Docker volumes + DB dumps) + ↓ on completion (success or fail) + scheduler → cross-drive jobs (for apps with daily schedule) + (weekly jobs: only triggered on configured day, e.g., Sunday) +``` + +**Implementation:** + +The controller's main scheduler loop already triggers DB dump and restic backup in sequence. After the restic backup completes, add: + +```go +// After restic backup completes +if crossDriveRunner != nil { + schedule := "daily" + if time.Now().Weekday() == time.Sunday { + // Also run weekly jobs on Sunday + crossDriveRunner.RunAllScheduled(ctx, "weekly") + } + crossDriveRunner.RunAllScheduled(ctx, schedule) +} +``` + +**User-facing schedule options on deploy page (cross-drive config):** + +| Option | Meaning | +|--------|---------| +| Naponta | Runs every night after nightly backup completes | +| Hetente (vasárnap) | Runs only on Sunday night after nightly backup | + +**Remove** the "Csak kézi indítás" (manual only) schedule option. If a user doesn't want automated cross-drive, they can disable the toggle. Manual trigger ("Futtatás most") remains available regardless. + +**Weekly + daily DB consistency:** + +When the user selects "Hetente" for cross-drive backup, show an informational note: + +``` +ℹ Heti mentés esetén visszaállításkor az adatbázis is a mentés napjára + áll vissza a konzisztencia érdekében. A mentés napja és a visszaállítás + között keletkezett adatbázis-változások elvesznek (max. 7 nap). +``` + +**Restore implication:** When restoring user data from a weekly cross-drive snapshot, the restore process should also restore the DB dump from the same date (matching by timestamp). This ensures DB ↔ file consistency. + +### 4. Cross-Drive Destination Validation + +**Replace** the current binary `IsMountPoint || !IsWritable` check with tiered validation. + +**New function:** `internal/system/mounts_linux.go` + +```go +type DestinationHealth struct { + Exists bool + Writable bool + MountPoint bool // different device from parent + SystemDrive bool // on same device as / + UsedPercent float64 // disk usage percentage + FreeGB float64 + Warning string // human-readable warning (Hungarian) + Blocked bool // if true, backup should not run + Severity string // "ok", "warning", "critical" +} + +func CheckBackupDestination(path string) DestinationHealth { ... } +``` + +**Validation tiers:** + +| Condition | Severity | Blocked? | Message | +|-----------|----------|----------|---------| +| Path doesn't exist | critical | yes | "A cél tárhely nem létezik" | +| Path not writable | critical | yes | "A cél tárhely nem írható" | +| Same block device as source | critical | yes | "A forrás és a cél azonos meghajtón van" | +| Path is on system drive (/) | warning | no | "Mentés a rendszermeghajtóra — meghajtóhiba esetén mindkét másolat elveszhet" | +| Destination >90% full | warning | no | "A mentési meghajtó majdnem megtelt (X% használt)" | +| Destination >95% full | critical | yes | "A mentési meghajtó megtelt (X%)" | +| Mount point, writable, healthy | ok | no | "" | + +**Same block device detection:** + +```go +func isSameBlockDevice(pathA, pathB string) bool { + var statA, statB syscall.Stat_t + if err := syscall.Stat(pathA, &statA); err != nil { + return false + } + if err := syscall.Stat(pathB, &statB); err != nil { + return false + } + return statA.Dev == statB.Dev +} +``` + +This replaces the current `IsMountPoint()` check in the destination dropdown filtering and the health warning logic. + +### 5. Drive Disconnection Notifications + +**Trigger:** During the nightly backup chain, before running cross-drive jobs, check all configured destinations. If any are unreachable: + +```go +for _, app := range appsWithCrossDrive { + health := system.CheckBackupDestination(app.DestinationPath) + if health.Blocked { + notifier.NotifyBackupFailed( + "Mentési meghajtó nem elérhető", + fmt.Sprintf("%s mentési célja (%s) nem elérhető: %s", + app.DisplayName, app.DestinationPath, health.Warning), + ) + } +} +``` + +**Also check on controller startup** (in `main.go` boot sequence) — if a configured destination is unreachable, log a warning and set the status. The backup page will show it immediately. + +**Also check in the 15-minute hub report** — include destination health in the status payload so the hub dashboard shows drive problems centrally. + +--- + +## Implementation Steps + +### Step 1: Fix duplicate apps bug (Bug 1) + +**Files:** +- `internal/backup/backup.go` — `GetFullStatus()`: return deep copy with nil cross-drive/unconfigured slices +- `internal/web/handlers.go` — `backupsHandler()`: verify no mutation of cached data + +**Test:** Load backup page 5 times → unconfigured apps count stays consistent. + +### Step 2: Fix destination validation (Bug 2 + Architecture item 4) + +**Files:** +- `internal/system/mounts_linux.go` — add `CheckBackupDestination()`, `isSameBlockDevice()` +- `internal/system/mounts_other.go` — add stubs +- `internal/web/handlers.go` — `deployHandler()`: replace `IsMountPoint || !IsWritable` with `CheckBackupDestination()` +- `internal/web/handlers.go` — `backupsHandler()`: same replacement for cross-drive warnings +- `internal/web/templates/deploy.html` — update warning display to handle `warning` vs `critical` severity + +**Test:** +- Configure cross-drive to `hdd_placeholder` → shows yellow warning about system drive, backup still allowed +- Configure cross-drive to `hdd_1` (external) → green, no warning +- Disconnect external drive → shows red "nem elérhető" +- Configure source and dest on same drive → blocked + +### Step 3: Remove dead BackupEnabled toggle (Bug 3) + +**Files:** +- `internal/web/handlers.go` — remove `settingsAppBackupHandler()` +- `internal/web/server.go` — remove `POST /settings/app-backup` route +- `internal/web/templates/backups.html` — remove bulk toggle form, remove "Aktív/Inaktív" badges from old section + +**Test:** Backup page loads without toggle form. No 500 errors. + +### Step 4: Unified per-app backup rows (Architecture item 2) + +**New struct:** + +```go +// AppBackupRow holds all backup information for one app, used by the template. +type AppBackupRow struct { + StackName string + DisplayName string + Status string // "green", "yellow", "red", "auto" + StatusText string // short Hungarian text for the indicator tooltip + + // Storage info (HDD apps only) + HasHDDData bool + StorageLabel string + HDDSizeHuman string + + // Backup layers + DBBackup *BackupLayerInfo // nil if app has no DB + VolumeBackup *BackupLayerInfo // always present for deployed apps + UserDataBackup *BackupLayerInfo // nil if app has no HDD data + + // Warnings (shown in expanded view) + Warnings []string +} + +type BackupLayerInfo struct { + Type string // "db", "volume", "userdata" + Label string // "Adatbázis mentés", "Docker kötetek", "Felhasználói adatok" + Auto bool // true if no user config needed + Configured bool // true if backup is set up for this layer + Method string // "restic", "rsync", "" (for auto) + Destination string // label of destination (for cross-drive) + Schedule string // "Naponta", "Hetente", "" (for auto) + LastRun string // formatted timestamp + LastStatus string // "ok", "error", "running", "" + LastError string // error message if failed + StatusBadge string // "Sikeres", "Hiba", "Fut...", "Auto", "—" + ConfigURL string // link to configure (deploy page) +} +``` + +**Files:** +- `internal/web/handlers.go` — new `buildAppBackupRows()` function, called from `backupsHandler()` +- `internal/web/templates/backups.html` — replace both sections with unified expandable rows +- `internal/web/templates/style.css` — new styles for expandable rows, status indicators, layer detail grid + +**Template structure (backups.html):** ```html - -{{if .Backup.AppDataInfo}}
-

Alkalmazás adatok

-

Az alkalmazások felhasználói adatainak mentési állapota. Beállítás az alkalmazás oldalán.

-
- {{range .Backup.AppDataInfo}} -
-
- {{.DisplayName}} -
- {{if .HasHDDData}} - {{if .StorageLabel}}{{.StorageLabel}}{{end}} - {{if .BackupEnabled}} - {{.HDDSizeHuman}} - Aktív - {{else}} - Inaktív - {{end}} - {{else}} - N/A - {{end}} -
-
-
- {{end}} +

Alkalmazások mentési állapota

+ + {{if .NoUserDataBackupWarning}} +
+ Felhasználói adatokról nincs biztonsági mentés. + A szerveren tárolt fotók, dokumentumok és egyéb fájlok jelenleg + csak egy példányban léteznek. + Meghajtó beállítása
+ {{end}} + + {{range .AppBackupRows}} +
+
+ + {{.DisplayName}} +
+ {{if .HasHDDData}} + {{if .StorageLabel}}{{.StorageLabel}}{{end}} + {{.HDDSizeHuman}} + {{else}} + Auto + {{end}} +
+ +
+ +
+ {{end}} +
+``` + +**JS for expand/collapse:** + +```javascript +function toggleBackupDetail(header) { + const detail = header.nextElementSibling; + const icon = header.querySelector('.expand-icon'); + if (detail.style.display === 'none') { + detail.style.display = 'block'; + icon.textContent = '▼'; + } else { + detail.style.display = 'none'; + icon.textContent = '▶'; + } +} +``` + +**Test:** +- All deployed apps appear in list +- Apps with HDD data show storage label + size +- Apps without HDD data show "Auto" badge +- Expanding shows correct layer info +- Red dot for unconfigured user data +- Green dot for fully covered apps + +### Step 5: Sequential backup chaining (Architecture item 3) + +**Files:** +- `cmd/controller/main.go` — modify nightly scheduler to chain cross-drive after restic +- `internal/backup/crossdrive.go` — `RunAllScheduled()` already exists, verify it handles "daily" and "weekly" correctly +- `internal/web/templates/deploy.html` — update schedule dropdown (remove "Csak kézi indítás", add weekly consistency note) + +**Current scheduler code (conceptual location in main.go):** + +```go +// In the nightly backup goroutine +go func() { + // ... wait for scheduled time ... + + // Phase 1: DB dumps + backupMgr.RunDBDump(ctx) + + // Phase 2: Restic snapshot + backupMgr.RunBackup(ctx) + + // Phase 3: Cross-drive (NEW — chained) + if crossDriveRunner != nil { + crossDriveRunner.RunAllScheduled(ctx, "daily") + if isWeeklyTriggerDay() { // e.g., Sunday + crossDriveRunner.RunAllScheduled(ctx, "weekly") + } + } +}() +``` + +**Deploy page schedule dropdown update:** + +```html + + + + +``` + +**Test:** +- Set app to daily → cross-drive runs after nightly backup every night +- Set app to weekly → cross-drive runs only on Sunday night +- Manual trigger still works regardless of schedule + +### Step 6: Drive disconnection detection & notification (Architecture item 5) + +**Files:** +- `cmd/controller/main.go` — startup drive check +- `internal/backup/crossdrive.go` — pre-run destination check, notification on failure +- `internal/web/handlers.go` — `backupsHandler()`: include destination health in row data + +**Startup check (main.go):** + +```go +// After settings are loaded, check all configured cross-drive destinations +crossConfigs := sett.GetAllCrossDriveConfigs() +for appName, cfg := range crossConfigs { + if cfg == nil || !cfg.Enabled || cfg.DestinationPath == "" { + continue + } + health := system.CheckBackupDestination(cfg.DestinationPath) + if health.Blocked { + logger.Printf("[WARN] Backup destination for %s unreachable: %s (%s)", + appName, cfg.DestinationPath, health.Warning) + } +} +``` + +**Pre-run check (crossdrive.go RunAppBackup):** + +```go +func (r *CrossDriveRunner) RunAppBackup(ctx context.Context, stackName string) error { + cfg := r.sett.GetCrossDriveConfig(stackName) + // ... existing checks ... + + // NEW: Tiered destination validation + health := system.CheckBackupDestination(cfg.DestinationPath) + if health.Blocked { + r.sett.UpdateCrossDriveStatus(stackName, "error", health.Warning, 0, "") + // Send notification + if r.notifier != nil { + r.notifier.NotifyBackupFailed( + "Mentési meghajtó nem elérhető", + fmt.Sprintf("%s mentési célja (%s): %s", stackName, cfg.DestinationPath, health.Warning), + ) + } + return fmt.Errorf("destination blocked: %s", health.Warning) + } + // ... proceed with backup ... +} +``` + +**Hub report inclusion:** + +Add destination health summary to the 15-minute hub status report payload so the central dashboard can show drive problems. + +**Test:** +- Disconnect external USB drive → backup page shows red status, notification sent +- Reconnect → status recovers on next page load +- Controller restart with disconnected drive → warning in logs + +### Step 7: Deploy page updates + +**Files:** +- `internal/web/templates/deploy.html` — update cross-drive section +- `internal/web/handlers.go` — `deployHandler()`: use `CheckBackupDestination()` for warnings + +**Changes:** +- Remove "Csak kézi indítás" schedule option +- Add weekly consistency note (shown/hidden via JS based on dropdown) +- Replace flat error message with tiered warning/critical display +- Add warning on app deploy page if app has HDD data but no cross-drive configured: + +```html +{{if and .StorageInfo (not .CrossDriveConfig)}} +
+ Az alkalmazás felhasználói adatairól nincs biztonsági mentés. + Állítsd be alább, vagy csatlakoztass egy + külső meghajtót.
{{end}} ``` -**Key differences from current:** -- No `
`, no checkboxes, no submit button -- No HDD paths, no Docker volume names, no DB dump notes -- No "app-backup-notice" about Docker volumes -- App name is a link to its deploy page -- Simple status indicator per app +### Step 8: Version bump & testing -### 2. `style.css` — Update styles +- Update CHANGELOG.md, CONTEXT.md +- Bump version (suggest 0.12.0 — this is a significant feature change) +- Build + deploy to demo-felhom.eu +- Full test cycle: + 1. Backup page loads without duplicates ✓ + 2. All deployed apps appear in unified list ✓ + 3. Expandable rows show correct layer info ✓ + 4. Apps without HDD data show "Auto" ✓ + 5. Immich (HDD data, cross-drive configured) → green ✓ + 6. Paperless/RomM (HDD data, no cross-drive) → red ✓ + 7. Configure cross-drive to hdd_placeholder → yellow warning ✓ + 8. Configure cross-drive to hdd_1 → green ✓ + 9. Cross-drive runs after nightly backup (check logs) ✓ + 10. Disconnect external drive → red on backup page, notification sent ✓ + 11. Manual trigger still works ✓ -Keep existing `.app-backup-list` and `.app-backup-item` styles. Add/modify: +--- -```css -/* App backup status list (read-only on backup page) */ -.app-backup-name-link { - font-weight: 500; - font-size: .9rem; - color: var(--text-primary); - text-decoration: none; -} +## Files Summary -.app-backup-name-link:hover { - color: var(--accent-blue); - text-decoration: underline; -} +### New files: +- None (all changes in existing files) -.app-backup-status-row { - display: flex; - align-items: center; - gap: .5rem; -} +### Modified files: -.app-backup-status { - font-size: .8rem; - font-weight: 500; - display: inline-flex; - align-items: center; - gap: .35rem; -} +| File | Changes | +|------|---------| +| `internal/backup/backup.go` | Deep copy in `GetFullStatus()`, remove cross-drive/unconfigured population from cache | +| `internal/backup/crossdrive.go` | Pre-run destination validation, notification on blocked destination | +| `internal/system/mounts_linux.go` | `CheckBackupDestination()`, `isSameBlockDevice()` | +| `internal/system/mounts_other.go` | Stubs for new functions | +| `internal/web/handlers.go` | `buildAppBackupRows()`, remove `settingsAppBackupHandler()`, updated `deployHandler()` validation | +| `internal/web/server.go` | Remove `POST /settings/app-backup` route | +| `internal/web/templates/backups.html` | Complete rewrite of app backup sections → unified expandable rows | +| `internal/web/templates/deploy.html` | Updated schedule dropdown, weekly note, tiered warnings, unconfigured warning | +| `internal/web/templates/style.css` | New styles for expandable rows, status dots, layer detail grid | +| `internal/settings/settings.go` | Deprecate `SetAppBackupBulk()`, `IsAppBackupEnabled()` | +| `cmd/controller/main.go` | Sequential chaining in scheduler, startup drive check | -.app-backup-status::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; -} +--- -.app-backup-active { - color: var(--green); -} +## Design Decisions -.app-backup-active::before { - background: var(--green); - box-shadow: 0 0 4px rgba(35, 134, 54, 0.4); -} +### Why merge the two backup sections? +Separation between "Alkalmazás adatok" and "Másolatok másik meghajtóra" forced users to mentally correlate information across two lists. A single per-app row with expandable detail gives complete backup coverage at a glance without cross-referencing. -.app-backup-inactive { - color: var(--text-muted); -} +### Why remove the BackupEnabled toggle? +It wrote to settings.json but nothing consumed the flag. No Backrest deployment exists; the nightly restic script doesn't check settings.json. Showing a toggle that doesn't affect anything is worse than showing nothing — it gives false confidence. -.app-backup-inactive::before { - border: 1px solid var(--text-muted); - background: transparent; - width: 6px; - height: 6px; -} +### Why sequential chaining instead of independent schedules? +DB ↔ file consistency. If cross-drive runs at a different time than the DB dump, a restore could produce an inconsistent state (DB references files that don't exist, or vice versa). Chaining ensures DB dump → restic → cross-drive all happen in one window, and a "restore to date X" operation can grab all three from the same night. -.app-backup-na { - color: var(--text-muted); -} +### Why allow system drive as backup target with warning? +For small data (configs, documents), having a second copy on the system SSD is better than no copy at all. For large data (Immich photos), the system drive may not have enough space and a drive failure would lose both copies. The warning lets users make an informed choice rather than blocking a useful configuration for small-data apps. -.app-backup-na::before { - content: '—'; - width: auto; - height: auto; - border-radius: 0; - background: none; - border: none; - box-shadow: none; - font-size: .8rem; -} -``` - -**Remove unused CSS classes** (no longer needed — these were for the old section): -- `.app-backup-toggle` -- `.app-backup-disabled-icon` -- `.app-backup-details` -- `.app-backup-path` -- `.app-backup-volume` -- `.app-backup-dbinfo` -- `.app-backup-actions` -- `.app-backup-notice` - -Only remove them if they're not used elsewhere. Search the templates first. - -### 3. Handlers — keep backend as-is - -The `POST /settings/app-backup` endpoint and the `DiscoverAppData()` function should remain — the per-app backup toggle still works from the app's deploy/settings page. We're only removing the UI from the centralized backup page. - -The `AppDataInfo` data is still populated and still needed — we use `.HasHDDData`, `.BackupEnabled`, `.StorageLabel`, `.HDDSizeHuman`, `.StackName`, and `.DisplayName` in the new simplified template. - -### 4. Ensure ALL deployed apps appear - -Currently, `DiscoverAppData()` iterates `provider.ListDeployedStacks()` which should include all deployed apps. Verify that apps without HDD data (like Gokapi, Mealie) also appear in the list — they should since `.NeedsHDD` can be false and they'd still be in the output with `HasHDDData: false`. - -If some apps are missing, the issue is in `ListDeployedStacks()` filtering — make sure it returns all deployed stacks, not just HDD-capable ones. - -## Testing - -1. Visit `/backups` page -2. All deployed apps should appear in the list (Immich, Paperless-ngx, Gokapi, Mealie, RoMM, etc.) -3. Apps with backup enabled show green dot + "Aktív" + size -4. Apps with HDD data but backup disabled show hollow dot + "Inaktív" -5. Apps without HDD data show dash + "N/A" -6. Clicking app name navigates to `/stacks/{name}/deploy` -7. No checkboxes, no paths, no volumes, no save button visible -8. The per-app backup toggle on the deploy page still works (toggle on backup page → save → return to backup page → status updated) \ No newline at end of file +### Why "Hetente" restores also roll back the DB? +A weekly user data snapshot from Sunday combined with a daily DB from Wednesday would create inconsistency — the DB would reference files from Mon-Wed that don't exist in Sunday's snapshot. Rolling both back to the same point (Sunday night) guarantees consistency at the cost of losing up to 7 days of DB changes. This is the correct trade-off for data integrity. \ No newline at end of file