diff --git a/TASK.md b/TASK.md index 402c6e0..8b7d347 100644 --- a/TASK.md +++ b/TASK.md @@ -1,724 +1,200 @@ -# TASK: Backup Page Overhaul — Unified App Backup Status & Bug Fixes +# 0.12.1 Bug Fixes -## Summary +## Bug 1: Version displays "vv0.12.0" -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. +**Severity:** Low (cosmetic) +**Location:** Two templates with hardcoded `v` prefix + +The sidebar and settings page templates both prepend `v` to `{{.Version}}`: +``` +v{{.Version}} ← line 17845 (layout.html) +v{{.Version}} ← line 19061 (settings.html) +``` + +`build.sh` takes VERSION as its first argument (`VERSION="${1:-dev}"`), and Viktor passes `v0.12.0` → the template renders `vv0.12.0`. + +**Fix (template side — recommended):** +Replace `v{{.Version}}` with `{{.Version}}` in both locations. This way the version displayed matches the git tag exactly. The `v` prefix comes from the tag/build arg naturally. + +**Affected files:** +- `internal/web/templates/layout.html` — sidebar footer +- `internal/web/templates/settings.html` — version info row --- -## Bug Fixes (Critical) +## Bug 2: "Külső HDD" stats show same values as SSD -### Bug 1: Duplicate unconfigured apps on every page load +**Severity:** Medium (wrong data displayed) +**Location:** Dashboard and Backup page storage bars; also metrics collector in `main.go` -**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. +**Setup on demo-felhom.eu:** +- Internal SSD (512G): system disk, mounted at `/`. `/mnt/hdd_placeholder` is a normal folder on this. +- External USB HDD (1TB): mounted at `/mnt/hdd_1` (ext4, `/host-dev/sdb1`) +- Two storage paths registered (visible in Settings > Adattárolók): + - `/mnt/hdd_placeholder` — "Rendszermeghajtón" badge, Aktív, 19.8 GB / 460.2 GB + - `/mnt/hdd_1` — "Alapértelmezett" + "Aktív" badge, 0.0 GB / 915.8 GB +- No `hdd_path` in `controller.yaml`, no `HDD_PATH` in any `app.yaml` +- The Settings page (Adattárolók section) shows **correct** values for both paths -**File:** `internal/backup/backup.go` — `GetFullStatus()` +**Current behavior:** +- Dashboard: "Külső HDD" shows 19.8 GB / 460 GB — same as SSD (wrong!) +- Backup page: Same duplicate in "Tárhely áttekintés" -**Fix:** Return a **deep copy** of the cached status. Specifically, `CrossDriveSummary`, `UnconfiguredApps`, `CrossDriveWarnings`, and `AppDataInfo` slices must not share backing arrays with the cache. +**Root cause:** +`primaryHDDPath()` (line 15164) returns `paths[0].Path` — the **first** element in the slice, which happens to be `/mnt/hdd_placeholder` (on the SSD). Then `system.GetInfo("/mnt/hdd_placeholder", ...)` calls `syscall.Statfs` on a directory that lives on the root filesystem → returns SSD stats. ```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 +// current — broken +func (s *Server) primaryHDDPath() string { + if paths := s.settings.GetStoragePaths(); len(paths) > 0 { + return paths[0].Path // ← returns first element, NOT the default! } - // ... (rest unchanged) + return s.cfg.Paths.HDDPath } ``` -**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. +But `GetDefaultStoragePath()` (line 8946) already exists and returns the path with `IsDefault: true` — which is `/mnt/hdd_1` (the actual HDD). -**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). - -### Bug 2: Misleading error message for non-mount-point destinations - -**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. - -**File:** `internal/web/handlers.go` — `deployHandler()` (line ~13818), `backupsHandler()` (line ~14036) - -**Current logic:** +**Same bug in `main.go`** (line 2403-2405) for the metrics collector: ```go -if !system.IsMountPoint(path) || !system.IsWritable(path) { - // "nem elérhető! Ellenőrizd a meghajtó csatlakozását." +metricsHDDPath := cfg.Paths.HDDPath +if paths := sett.GetStoragePaths(); len(paths) > 0 { + metricsHDDPath = paths[0].Path // ← same bug } ``` -**Fix:** Replace the single check with tiered validation: +**Fix:** +1. `primaryHDDPath()` → use `s.settings.GetDefaultStoragePath()` instead of `paths[0].Path` +2. `main.go` metrics collector → use `sett.GetDefaultStoragePath()` instead of `paths[0].Path` +3. Fallback: if no default is set, fall back to `cfg.Paths.HDDPath` (existing behavior) ```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 +// fixed +func (s *Server) primaryHDDPath() string { + if p := s.settings.GetDefaultStoragePath(); p != "" { + return p } - // 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 + return s.cfg.Paths.HDDPath } ``` -- `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) +**Affected files:** +- `internal/web/handlers.go` — `primaryHDDPath()` function +- `cmd/controller/main.go` — metrics collector HDD path selection --- -## Architecture Changes +## Bug 3: Gokapi/Mealie show gray ("auto") status — should be green -### 1. Backup Coverage Model +**Severity:** Medium (misleading status) +**Location:** `buildAppBackupRows()` in `internal/web/handlers.go` (line 14302-14367) -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: +Apps without HDD data (like Gokapi, Mealie) always get `status = "auto"` (gray dot). From the user's perspective, these apps ARE fully backed up — their Docker volumes are included in the restic snapshot, and their configs are under `/opt/docker/stacks/`. The gray dot misleadingly suggests they're not backed up at all. +**Current logic (line 14365-14368):** ```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) +} else { + // No HDD data — fully automatic + row.Status = "auto" + row.StatusText = "Automatikus mentés (nincs felhasználói adat)" } ``` -**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` +**Desired behavior:** +- Apps with no HDD data whose Docker volumes are successfully backed up → **green** dot with text "Mentés rendben" +- Apps with no HDD data where last volume backup failed → **yellow** dot +- Keep "auto" only if there's genuinely no backup info available (edge case) +**Fix approach:** +Replace the `else` block (line 14365-14368): ```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 -
-

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 { + // No HDD data — everything backed up automatically via restic + if volumeLastStatus == "ok" { + row.Status = "green" + row.StatusText = "Mentés rendben" + } else if volumeLastStatus == "error" { + row.Status = "yellow" + row.StatusText = "Kötetek mentése sikertelen" } else { - detail.style.display = 'none'; - icon.textContent = '▶'; + row.Status = "auto" + row.StatusText = "Automatikus mentés" } } ``` -**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 +Additionally, if the app has a DB and the DB backup was successful, that should also contribute to green status. The existing DB error check at line 14372 already degrades to yellow, so this is covered. -### 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}} -``` - -### Step 8: Version bump & testing - -- 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 ✓ +**Affected files:** +- `internal/web/handlers.go` — `buildAppBackupRows()` status logic --- -## Files Summary +## Bug 4: Restore section ("Visszaállítás") UX rethink -### New files: -- None (all changes in existing files) +**Severity:** Medium (UX complexity) +**Location:** Backup page restore section in `backups.html` -### Modified files: +Now that user data folders are also backed up via cross-drive (rsync), the restore section needs rethinking. Currently: +- DB backup is a separate snapshot tied to user data +- Multiple warnings accumulate, making the UI intimidating +- User must understand the difference between DB-only and full restore -| 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 | +**Decision (from Viktor):** +- If user data is backed up, the "Pillanatkép" selector should only show snapshots that include user data → restores everything together (most failsafe) +- Daily DB-only backups remain useful as intermediate recovery points, but DB-only restore is an advanced task → contact support +- This simplifies the UI: one dropdown, one action, one clear warning + +**Implementation plan:** +1. Snapshot selector: Filter to show only snapshots containing user data paths when cross-drive is configured +2. Restore action: Restores both Docker volumes/configs AND user data in one operation +3. Simplify warnings: Single clear warning about data overwrite, remove redundant ones +4. DB-only restore: Not exposed in UI — support-level operation via CLI + +**Note:** This is a design change, not a bugfix. Needs more detailed specification before implementation. Possibly v0.13.0 scope. + +**Affected files:** +- `internal/web/handlers.go` — restore handler logic +- `internal/web/templates/backups.html` — restore section template +- `internal/backup/restore.go` — restore logic (if separate) --- -## Design Decisions +## Bug 5: Restic key buttons barely readable -### 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. +**Severity:** Low (cosmetic) +**Location:** Backup page, "Titkosítási kulcs" section (line 16660-16661 in `backups.html`) -### 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. +The "Megjelenítés" and "Másolás" buttons use `class="btn btn-sm"` without a color variant. The `.btn` base class sets `color: #fff` (white text) but has **no background color**. The browser default background (light gray) makes white text nearly invisible on the dark theme. -### 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. +**Current HTML:** +```html + + +``` -### 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. +**Fix options:** +A. **Add a default background to `.btn` base class** — e.g. `background: var(--card-bg);` or `background: rgba(255,255,255,0.1);` — This fixes all unclassed buttons globally +B. **Add variant class to these specific buttons** — e.g. `class="btn btn-sm btn-secondary"` with a defined `.btn-secondary` style -### 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 +**Recommended: Option A** — add a sensible dark default to `.btn`: +```css +.btn { + /* existing properties... */ + background: rgba(255,255,255,0.1); /* subtle dark default */ +} +``` +This ensures any `btn` without a specific variant is still readable on dark backgrounds. + +**Affected files:** +- `internal/web/templates/style.css` (or wherever `.btn` is defined) — add default background + +--- + +## Priority / Implementation Order + +1. **Bug 1** (version `vv`) — trivial template fix, ~2 min +2. **Bug 5** (button contrast) — trivial CSS fix, ~2 min +3. **Bug 2** (HDD stats wrong path) — 2-line fix in `primaryHDDPath()` + `main.go`, ~5 min +4. **Bug 3** (gray → green status) — small logic change, ~10 min +5. **Bug 4** (restore rethink) — design decision needed, deferred to v0.13.0 \ No newline at end of file