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}}
Az alkalmazások felhasználói adatainak mentési állapota. Beállítás az alkalmazás oldalán.
-