## Changelog
### What was just completed (2026-02-18 session 48)
- **v0.13.1 — UI Polish Fixes Round 2 (4 fixes):**
**Fix 1:** Deploy page "Biztonsági mentés" section now has proper card border. Root cause: `.deploy-cross-drive` used undefined CSS variables `--card-bg` and `--border` (only `--bg-secondary` and `--border-color` exist). Fixed by using correct vars (`style.css`).
**Fix 2:** Auto-generated env values section cleaned up (`deploy.html`, `style.css`). Badge moved inline with label. "Másolás" buttons removed (native select+copy sufficient). Secret fields keep show/hide toggle. Non-secret fields now plain readonly input without button wrapper. Removed `copyAutoField()` JS. CSS updated: `.form-group-auto` now block layout (was flex row), label uses `display: flex; gap: .5rem`, badge downsized to `0.75rem / normal weight`, readonly inputs get muted background.
**Fix 3:** Snapshot table n/a → 0 (`backups.html`). Replaced `n/a ` with plain `0` in all three stats columns. Removed `.col-na` CSS class (no longer used).
**Fix 4:** Disk warnings moved from top banner to inline under storage bars (`alerts.go`, `layout.html`, `handlers.go`, `dashboard.html`, `monitoring.html`, `style.css`). Added `Inline bool` field to `Alert` struct. Disk-related warnings set `Inline: true`. Layout banner skips inline alerts. New `GetInlineAlerts(page)` method on `AlertManager`. Dashboard and monitoring handlers pass `DiskWarnings`. Inline warning block rendered below storage bars. New `.inline-warning*` CSS classes (compact, subtle, colored).
**Files modified (8):** `alerts.go`, `handlers.go`, `templates/style.css`, `templates/dashboard.html`, `templates/backups.html`, `templates/deploy.html`, `templates/monitoring.html`, `templates/layout.html`
### What was just completed (2026-02-18 session 47)
- **v0.13.0 — UI Polish Fixes (8 independent fixes):**
**Fix 1:** backup-status-card border already correct (verified same styling as system-info-card).
**Fix 2:** Deploy page auto-generated fields now show actual values for deployed apps (`deploy.html`, `handlers.go`). Secrets show as password fields with show/hide toggle; domain/plain values show as readonly text with copy button. JS helpers `toggleAutoField()` / `copyAutoField()` added.
**Fix 3:** Temperature display made more prominent (`dashboard.html`, `style.css`). Dot enlarged to 11px; value wrapped in colored pill badge (`.temp-value-pill` / `.temp-pill-{green|yellow|red}`).
**Fix 4:** Dashboard backup card reworked (`dashboard.html`, `handlers.go`). Removed "Mentés most" button and `triggerBackup()` JS. Removed "Tároló méret" line. Added Tier 2 status line (configured/total apps) + warning row for failed cross-drive backups. Handler now computes `CrossDriveTotal`, `CrossDriveConfigured`, `CrossDriveFailed`.
**Fix 5:** HDD warning banner scoped to dashboard + monitoring pages only (`alerts.go`, `layout.html`, `funcmap.go`). Added `PageOnly []string` field to `Alert` struct. Disk-related warnings (keywords "meghajtón", "adattároló") get stable ID `"disk-not-separate"` + `PageOnly: ["dashboard", "monitoring"]`. `pageMatch()` template function added. Layout renders alerts conditionally.
**Fix 6:** Tárhely section moved up in Rendszermonitor — now appears right after "Rendszer áttekintés", before "Távoli monitoring" (`monitoring.html`).
**Fix 7:** Snapshot table improvements (`backups.html`, `style.css`). "MÉRET" renamed to "HOZZÁADOTT (új adat)". `–` for unavailable data replaced with `n/a` (with tooltip explaining restic limitations). New `.col-subtitle` and `.col-na` CSS classes.
**Fix 8:** Tároló section restructured into tiers (`backups.html`, `handlers.go`, `style.css`). Tier 1 (restic local), Tier 2 (cross-drive, only shown if configured), DB dump directory + total size. Removed "Távoli másolat: Nincs beállítva" placeholder. Handler passes `DBDumpDir`, `DBDumpTotalBytes`, `Tier2Dests` (deduplicated). New `.repo-tier` / `.repo-tier-title` CSS.
**Files modified (9):** `alerts.go`, `funcmap.go`, `handlers.go`, `templates/style.css`, `templates/dashboard.html`, `templates/backups.html`, `templates/deploy.html`, `templates/monitoring.html`, `templates/layout.html`
### What was just completed (2026-02-18 session 46)
- **v0.12.9 — Tier 2 for All Apps + Status Dot Update:**
**Fix 1: Tier 2 now configurable for ALL apps — not just HDD apps (`crossdrive.go`)**
- Removed `len(mounts) == 0` error gate from `RunAppBackup()` — empty mounts = config-only backup
- rsync: DB dump copy (`_db/`) + config rsync (`_config/`) still runs even with zero HDD mounts
- restic: config dir + DB dump dir still appended even without mount paths
- Non-HDD apps (Mealie, Gokapi, etc.) can now be protected against drive failure via Tier 2
**Fix 2: Status dot logic updated, HasHDDData gate removed (`handlers.go`)**
- `buildAppBackupRows()`: "auto" (gray) status removed — all apps start yellow ("Csak helyi mentés")
- Green requires Tier 2 configured + last status "ok" (not just "configured but never run")
- Tier2 section is now unconditional — no `if app.HasHDDData` gate
- Cross-drive summary loop: removed `if !app.HasHDDData { continue }` — all apps in summary
**Fix 3: Backup page template updates (`backups.html`)**
- Tier 2 row shown for all apps (removed `{{if .HasHDDData}}` gate)
- Meta badge: non-HDD apps show "Konfig" or "Konfig + DB" instead of "Auto"
- Tier 3 placeholder row added (grayed out "Hamarosan / távoli offsite")
- Button text: "Összes HDD mentés" → "Összes 2. mentés futtatása most"
**Fix 4: Deploy page cross-drive section visible for all deployed apps (`deploy.html`)**
- Removed `{{if .StorageInfo}}` double-gate — section now shows for all deployed apps
- Updated heading: "Másolat másik meghajtóra (felhasználói adatok)" → "2. mentés — másolat másik meghajtóra"
- Updated hint: "mint az alkalmazás adattárolója" → "a meghibásodás elleni védelem érdekében"
**Files modified (4):** `internal/backup/crossdrive.go`, `internal/web/handlers.go`, `internal/web/templates/backups.html`, `internal/web/templates/deploy.html`
### What was just completed (2026-02-18 session 45)
- **v0.12.8 — Complete Cross-Drive Backup + Per-Tier UI:**
**Fix 1: Cross-drive backup now includes DB dumps + app config (`crossdrive.go`, `main.go`)**
- `CrossDriveRunner` gets `dbDumpDir` field + `SetDBDumpDir(dir string)` setter
- `copyStackDBDumps()` helper copies `_*.sql` files to `_db/` subfolder in rsync dest
- `runRsyncBackup()`: after HDD mount rsync loop, copies DB dumps to `_db/` and rsyncs config dir to `_config/` — both non-fatal on error
- `runResticBackup()`: appends config dir and full DB dump dir to restic paths (restic deduplicates)
- rsync destination layout: `backups/rsync//_db/` (dumps) + `_config/` (compose+yaml) + user data
- `main.go`: `crossDriveRunner.SetDBDumpDir(cfg.Paths.DBDumpDir)` wired after runner init
**Fix 2: UI restructured from per-layer to per-tier (`handlers.go`, `backups.html`, `style.css`)**
- `AppBackupRow` struct rebuilt: dropped old `DBLastRun/Status`, `VolumeLastRun/Status`, `HasUserData`, `UserDataConfigured/Method/Dest/Schedule/LastRun/LastStatus/LastError/StatusBadge` fields
- New fields: `BackupContents` (e.g., "DB + Konfig + Adatok"), `Tier1LastRun/LastStatus/DBStatus`, `Tier2Configured/Method/MethodLabel/Dest/Schedule/LastRun/LastStatus/LastError/StatusBadge/SizeHuman/Browsable`
- `buildAppBackupRows()` rewritten: destination health now via `s.crossDriveRunner.ValidateDestination()` instead of `system.CheckBackupDestination()`
- `backups.html`: two tier rows (1. mentés / 2. mentés) replace the old three layer rows (DB / Konfig / Userdata)
- `style.css`: added `.tier-label`, `.tier-location`, `.tier-contents`, `.tier-size`, `.tier-browsable` classes
**Fix 3: Cleanup (`router.go`)**
- `filterSnapshotsByPaths()` and `pathCovers()` deleted (were unused since v0.12.7a)
**Files modified (6):** `internal/backup/crossdrive.go`, `cmd/controller/main.go`, `internal/web/handlers.go`, `internal/web/templates/backups.html`, `internal/web/templates/style.css`, `internal/api/router.go`
### What was just completed (2026-02-18 session 44)
- **v0.12.7a — Post-deploy fixes:**
**Fix A: Restore now shows snapshots for all apps (`internal/api/router.go`)**
- Root cause: `filterSnapshotsByPaths` filtered older snapshots (pre-v0.12.7) by HDD paths. Older snapshots don't contain HDD paths (backup wasn't mandatory yet), so Immich got zero snapshots.
- Fix: removed HDD path filtering entirely from `backupSnapshots`. All snapshots contain config + DB dumps and are useful for any app. `RestoreApp` extracts whatever paths are available from the chosen snapshot.
- `filterSnapshotsByPaths` and `pathCovers` functions kept (unused, no compile error).
**Fix B: Clarified "no cross-drive" warning (`internal/web/handlers.go`, `backups.html`, `style.css`)**
- Root cause: "Nincs beállítva" / red dot implied no backup at all — misleading since nightly restic now always covers HDD data.
- `handlers.go`: status `"red"` → `"yellow"`, StatusText → `"Nincs második másolat (csak helyi mentés)"`
- `backups.html`: added `✓ Helyi mentés auto` badge before the `⚠ Nincs 2. másolat` warning
- `style.css`: `.layer-auto-ok` class added (green text for the auto badge)
**Files modified (3):** `internal/api/router.go`, `internal/web/handlers.go`, `internal/web/templates/backups.html`, `internal/web/templates/style.css`
### What was just completed (2026-02-18 session 43)
- **v0.12.7 — Backup Architecture Overhaul (mandatory HDD backup, pre-dump, restore for all apps):**
**Fix 1: HDD data backup now mandatory (`backup.go`, `appdata.go`, `settings.go`)**
- `resolveAppBackupPaths()` rewrote to iterate ALL deployed stacks via `ListDeployedStacks()` — no longer reads `GetAppBackupMap()` or checks `Enabled` flag
- `DiscoverAppData()` signature simplified: dropped `backupPrefs map[string]bool` parameter; `BackupEnabled` is now derived from `HasHDDData` (if app has HDD data, it's always backed up)
- `RefreshCache()` updated to call new `DiscoverAppData(m.stackProvider, status.DiscoveredDBs)` signature
- 5 dead settings methods deleted: `IsAppBackupEnabled`, `SetAppBackup`, `GetAppBackupMap`, `SetAppBackupBulk`, `GetAppBackupPrefs` — `AppBackupPrefs.Enabled` field kept in struct for backward-compat JSON loading
**Fix 2: Cross-drive backup triggers fresh DB dump first (`crossdrive.go`, `backup.go`, `main.go`)**
- New `DBDumper` interface with `DumpStackDB(ctx, stackName)` in `crossdrive.go`
- `CrossDriveRunner` gets `dbDumper` field + `SetDBDumper(d DBDumper)` setter
- `Manager.DumpStackDB()` discovers containers for that stack via `DiscoverDatabases()`, runs `DumpAll()`, persists validation cache — same logic as nightly dump but scoped to one stack
- `RunAppBackup()` calls `DumpStackDB()` before `ValidateDestination()` — non-fatal on failure (logs warn, proceeds with user data)
- `main.go` wires `crossDriveRunner.SetDBDumper(backupMgr)` after both are initialized
**Fix 3: Restore dropdown shows ALL deployed apps (`backups.html`, `restore.go`, `router.go`)**
- `restore.go` rewritten: no `IsAppBackupEnabled()` check; resolves `GetStackComposePath` + `DBDumpDir` + HDD mounts; always restores config+DB, adds user data if `hasHDD`; logs restore type (`config+DB` vs `full (config+DB+userdata)`)
- Restore dropdown template: removed `{{if and .HasHDDData .BackupEnabled}}` filter; every app gets an `` with `data-has-hdd` and `data-has-db` attributes
- New `#restore-type-info` div added between snapshot selector and warnings
- `onRestoreAppChange()` JS updated: reads `data-has-hdd`/`data-has-db` from selected option, shows Hungarian restore type banner (full / config+DB / config only) with color-coded styling
- `router.go` `backupSnapshots`: added clarifying comment for non-HDD apps (no filter = all snapshots returned)
**Fix 4: Honest UI label (`backups.html`)**
- "Docker kötetek" renamed to "Konfiguráció" — Docker named volumes at `/var/lib/docker/volumes/` are NOT in the restic backup paths; what's actually backed up is compose files + app.yaml + .felhom.yml
**CSS: `.restore-info` and `.restore-info-partial` classes added to `style.css`**
**Files modified (9):** `internal/backup/backup.go`, `internal/backup/appdata.go`, `internal/settings/settings.go`, `internal/backup/crossdrive.go`, `internal/backup/restore.go`, `cmd/controller/main.go`, `internal/web/templates/backups.html`, `internal/web/templates/style.css`, `internal/api/router.go`
### What was just completed (2026-02-18 session 42)
- **v0.12.6 — Cross-Drive Backup Rsync Fixes:**
**Context:** After fixing mount-point validation and system-drive thresholds (v0.12.5), testing revealed two more rsync issues for Immich.
**Fix 3: Simplified rsync destination path structure (`internal/backup/crossdrive.go` `runRsyncBackup`)**
- Old logic stripped only the first 2 path segments and kept the rest as a subpath, producing redundant nesting: `backups/rsync/immich/storage/immich/` instead of `backups/rsync/immich/`
- New logic: if app has a single mount, rsync directly into the stack folder (`backups/rsync/immich/`); if multiple mounts, use each mount's leaf directory name as subfolder
- Duplicate leaf names disambiguated by appending `_N` index suffix
- Loop variable changed from `_, srcMount` to `i, srcMount` to support the index-based disambiguation
- Old nested `storage/immich/` folder will remain orphaned after first run (no data loss; `--delete` only affects the target subtree)
**Fix 4: Exclude app-internal DB dump files from rsync (`internal/backup/crossdrive.go` `runRsyncBackup`)**
- Apps like Immich store their own periodic DB dumps in `/backups/*.sql.gz` (~16 MB/day)
- The controller already handles DB backups via `pg_dump` separately — copying these again via rsync is redundant and wastes space
- Added `--exclude backups/*.sql.gz`, `--exclude backups/*.sql`, `--exclude backups/*.dump` to rsync command
- The `backups/` directory itself and non-dump files within it are preserved
**Files modified (1):** `internal/backup/crossdrive.go`
### What was just completed (2026-02-18 session 41)
- **v0.12.5 — Cross-Drive Backup Validation Fix:**
**Root cause:** Immich cross-drive backup failed with `destination /mnt/hdd_placeholder is not a mount point` because `ValidateDestination()` hard-blocked non-mount-point destinations. The `/mnt/hdd_placeholder` folder is on the internal SSD (not a separate mount), so the device-ID check returned false.
**Fix 1: Drive-type-aware space checks in `ValidateDestination` (`internal/backup/crossdrive.go`)**
- `onSystemDrive` flag replaces the previous boolean-only mount-point check
- System-drive destinations: require **≥10 GB free** and **<90% usage** to protect OS stability
- External-drive destinations: require **≥100 MB free** (original threshold)
- Updated function comment to reflect the new tiered logic
**Fix 2: Aligned `CheckBackupDestination` UI thresholds for system drives (`internal/system/mounts_linux.go`)**
- Tier 4 disk checks now branch on `h.SystemDrive` flag (set in Tier 3)
- System drive: block at <10 GB free OR ≥90% used (matches runner enforcement); Hungarian warning messages
- External drive: warn at ≥90% used, block at ≥95% used (unchanged)
- Removed the `&& h.Severity == "ok"` guard that prevented system-drive warnings from being overridden properly
**Files modified (2):** `internal/backup/crossdrive.go`, `internal/system/mounts_linux.go`
### What was just completed (2026-02-18 session 40)
- **v0.12.4 — Correctness & Robustness Bug Fixes (TASK.md — 15 bugs fixed):**
**CRITICAL fixes (data loss, panics):**
- **C1: `SetAppBackupBulk` data loss + nil map panic** — Fixed: now updates map IN PLACE instead of replacing it, so stacks absent from the input are preserved. Added nil guard for `s.AppBackup`. (`internal/settings/settings.go`)
- **C2: `UpdateStackConfig` nil Env map panic** — Added nil check `if appCfg.Env == nil { appCfg.Env = make(...) }` before the field assignment loop. (`internal/stacks/deploy.go`)
- **C3: `ValidateDump` missing scanner.Err() check** — Added `if err := scanner.Err()` check after the scan loop so I/O errors don't silently mark a partial dump as valid. (`internal/backup/dbdump.go`)
**HIGH fixes (logic errors, resource leaks):**
- **H1: `nextDailyRun` DST bug** — Replaced `next.Add(24 * time.Hour)` with `time.Date(day+1, ...)` for correct scheduling across Europe/Budapest DST transitions. (`internal/scheduler/scheduler.go`)
- **H2: `nextDailyRun` repeated `LoadLocation`** — Cached timezone in package-level `sync.Once` variable; `getBudapestLocation()` now loaded only once. (`internal/scheduler/scheduler.go`)
- **H3: `settings.save()` .tmp file leak** — Added `os.Remove(tmpPath)` cleanup on `WriteFile` failure path. (`internal/settings/settings.go`)
- **H4: `SetNotificationPrefs` nil pointer panic** — Added nil guard at start of function, returns error instead of panicking. (`internal/settings/settings.go`)
- **H5: `appDirSize` ignores `Sscanf` return value** — Now checks `n != 1` and returns `(0, "?")` on parse failure. Same fix applied to `getDirSizeBytes` in `stacks/delete.go`. (`internal/backup/appdata.go`, `internal/stacks/delete.go`)
- **H6: `getDirSizeBytes` no timeout** — Added `exec.CommandContext` with 30s timeout. Added `"context"` import. (`internal/stacks/delete.go`)
- **H7: `dbdump.go` tmpFile not using `defer Close`** — Replaced explicit `tmpFile.Close()` call with `defer tmpFile.Close()` so the file handle is released even on panic. (`internal/backup/dbdump.go`)
- **H8: `UpdateCrossDriveStatus` misleading comment** — Updated comment to accurately describe the "does nothing if nil" behavior instead of claiming it "creates one if nil". (`internal/settings/settings.go`)
**MEDIUM fixes (code quality, edge cases):**
- **M1: Custom `contains`/`containsBytes` replaced** — Removed bespoke `containsBytes` and simplified `contains` to delegate to `strings.Contains`. Added `"strings"` import. (`internal/notify/notifier.go`)
- **M2: `scheduler.Every()` doesn't validate interval** — Added early return with error log if `interval <= 0` to prevent panic in `time.NewTicker`. (`internal/scheduler/scheduler.go`)
- **M3: `executeJob` panic recovery missing `LastRun`** — Panic recovery defer now also sets `job.LastRun = time.Now()` so the job status shows a timestamp after a panic. (`internal/scheduler/scheduler.go`)
- **M4: `logPostStartStatus` goroutine captures env by reference** — Copies the env slice before launching the goroutine (`envCopy`). (`internal/stacks/manager.go`)
- **M5: Multiple `time.LoadLocation` calls in web package** — Added package-level `getTimezone()` with `sync.Once` in `funcmap.go`. Replaced all `time.LoadLocation("Europe/Budapest")` calls in the web package with `getTimezone()`. (`internal/web/funcmap.go`, `internal/web/handlers.go`)
**Files modified (8):** `internal/settings/settings.go`, `internal/stacks/deploy.go`, `internal/backup/dbdump.go`, `internal/scheduler/scheduler.go`, `internal/backup/appdata.go`, `internal/stacks/delete.go`, `internal/stacks/manager.go`, `internal/notify/notifier.go`, `internal/web/funcmap.go`, `internal/web/handlers.go`
### What was just completed (2026-02-17 session 39)
- **v0.12.3 — Security & Correctness Bug Fixes (TASK.md — 33 bugs fixed):**
**CRITICAL fixes (data races, security vulnerabilities):**
- **C1: Data race in RefreshCache** — Moved `m.lastDBDump.Results` mutation inside `m.mu.Lock()`. Was previously mutating shared state without the lock, causing potential torn writes visible to `GetFullStatus()` goroutines. (`internal/backup/backup.go`)
- **C2: SnapshotHistory reversed after unlock** — Moved snapshot reversal loop before `m.cachedStatus = status` (inside the lock). Previously reversed after `Unlock()`, so `m.cachedStatus.SnapshotHistory` was reversed without protection. (`internal/backup/backup.go`)
- **C3: SetStackProvider write without lock** — `m.stackProvider = provider` now wrapped in `m.mu.Lock()`. Read by `resolveAppBackupPaths()` concurrently. (`internal/backup/backup.go`)
- **C4: GetFullStatus shallow-copies mutable pointers** — `LastDBDump` and `LastBackup` are now deep-copied (struct + Results slice) so callers cannot mutate shared manager state. (`internal/backup/backup.go`)
- **C5: IsSystemDisk 8-bit major mask** — Replaced `>> 8 & 0xff` with `unix.Major()`/`unix.Minor()` (12-bit extraction). Also compares disk-portion of minor (groups of 16) to correctly distinguish physical disks of the same type. Adds `golang.org/x/sys/unix` import. (`internal/storage/safety_linux.go`)
- **C6: No /dev/ prefix validation on DevicePath** — `FormatAndMount` now validates `DevicePath` starts with `/dev/` and does not contain `..` before any disk operations. (`internal/storage/format_linux.go`)
- **C7: Path traversal in extractName** — `extractName()` now rejects empty string, `.`, `..`, and names containing `/` or `\`. (`internal/api/router.go`)
- **C8: Path traversal in TargetPath** — Migration API validates `TargetPath` against registered storage paths from settings before starting migration job. (`internal/web/storage_handlers.go`)
- **C9: Path traversal in DestinationPath** — Cross-drive backup config API validates `DestinationPath` against registered storage paths when `enabled=true`. (`internal/api/router.go`)
- **C10: Path traversal in ParseComposeHDDMounts** — `filepath.Clean()` applied before prefix check; uses separator-aware check `cleanHDD + string(filepath.Separator)` to prevent `${HDD_PATH}/../../etc/passwd` escaping. (`internal/stacks/delete.go`)
**HIGH fixes (logic errors, resource leaks):**
- **H1: ValidateDump reads entire file into memory** — Replaced `os.ReadFile` with `bufio.Scanner` reading line-by-line. 256KB per-line buffer prevents OOM on large (500MB+) SQL dumps during 5-min cache refresh. (`internal/backup/dbdump.go`)
- **H2/H3: Double du invocation per mount + no timeout** — Replaced `appDirSizeHuman()`+`appDirSizeBytes()` with single `appDirSize()` function using `exec.CommandContext` with 30s timeout. Halves subprocess calls per mount point. (`internal/backup/appdata.go`)
- **H4: Snapshot validation only checks first 100** — Replaced `ListSnapshots(100)` existence check with regex validation (`^[0-9a-f]{8,64}$`). Allows restoring any snapshot; `restic restore` returns a clear error for non-existent IDs. (`internal/backup/restore.go`)
- **H5: No pruning for cross-drive restic repos** — Added `pruneResticRepo()` called after each successful cross-drive restic backup (`forget --keep-daily 7 --keep-weekly 4 --prune`). Non-fatal — logs warning on failure. (`internal/backup/crossdrive.go`)
- **H6: Temp password file management** — Reorganized temp file lifecycle: close before deferred remove, remove-on-write-error cleanup. (`internal/backup/crossdrive.go`)
- **H7: dirSizeBytes swallows walk errors** — `filepath.Walk` callback now returns errors instead of `nil`, propagating permission/IO issues. (`internal/backup/crossdrive.go`)
- **H8: Non-atomic fstab write** — `AppendFstabEntry` now reads existing fstab, writes to `.tmp`, then atomically renames. Crash-safe. (`internal/storage/safety_linux.go`)
- **H9: IsDeviceMounted naive prefix matching** — After prefix check, next character must be digit (`0-9`) or `p` (partition marker). Prevents `/dev/sdb` matching `/dev/sdba`. (`internal/storage/safety_linux.go`)
- **H10: eMMC device mapping bug** — `partitionToParentDisk` now handles `mmcblk0p1 → mmcblk0` and `nvme0n1p1 → nvme0n1` patterns. Uses `LastIndex("p")` with digit-suffix check before falling back to `TrimRight("0-9")`. (`internal/storage/scan_linux.go`)
- **H11: Data race on bytesCopied in rsync error path** — Error return path in `runRsync` now reads `bytesCopied` under mutex lock. (`internal/storage/migrate.go`)
- **H13: Path prefix match without separator** — Migration source path check now uses `srcPath == req.CurrentHDDPath || strings.HasPrefix(srcPath, req.CurrentHDDPath+"/")`. Prevents `/mnt/hdd` matching `/mnt/hdd_backup/data`. (`internal/storage/migrate.go`)
- **H14: DeleteStack continues after failed compose down** — `docker compose down` failure now returns an error immediately, preventing deletion of files while containers are still running. (`internal/stacks/delete.go`)
- **H16: exec.Command("docker") without timeout** — `syncFileBrowserMounts()` now uses `exec.CommandContext` with 60s timeout. (`internal/web/handlers.go`)
- **H17: SetNotificationPrefs stores caller's pointer** — Deep-copies `NotificationPrefs` struct and `EnabledEvents` slice before storing. (`internal/settings/settings.go`)
- **H18: wipefs error silently discarded** — wipefs failure logged as warning via progress channel; continues (wipefs may not be installed). (`internal/storage/format_linux.go`)
- **H19: Orphaned fstab entry on mount failure** — New `RemoveFstabEntry()` function atomically removes UUID entry. Called as rollback on `mount` failure and `findmnt` verify failure. (`internal/storage/safety_linux.go`, `format_linux.go`)
**MEDIUM fixes (edge cases, code quality):**
- **M1: formatBytes duplicate in dbdump.go** — Removed `formatBytes()` from `dbdump.go`; all callers (backup.go, restic.go, dbdump.go) now use `humanizeBytes()` from appdata.go. (`internal/backup/dbdump.go`, `backup.go`, `restic.go`)
- **M2: Dead code .tmp suffix check** — Reordered filter in `ListDumpFiles`: `.tmp` check now comes before `.sql` check to correctly skip `.sql.tmp` temp files (was unreachable before). (`internal/backup/dbdump.go`)
- **M3: sizeBytes() returns 0 for string types** — Added `case string:` to `sizeBytes()` using `strconv.ParseUint`. (`internal/storage/scan_linux.go`)
- **M6: Dead elapsed variable** — Removed `_ = elapsed`; elapsed time now shown inline in the "done" progress message. (`internal/storage/migrate.go`)
- **M7: time.LoadLocation error silently discarded** — Two locations in handlers.go now handle `LoadLocation` error, falling back to `time.UTC`. (`internal/web/handlers.go`)
- **M10: filterSnapshotsByPaths imprecise prefix** — Added `pathCovers()` helper using separator-aware prefix check. Prevents `/mnt/hdd_1` matching `/mnt/hdd_10/data`. (`internal/api/router.go`)
- **M11: XSS in editStorageLabel innerHTML** — `cancelEditLabel()` in settings.html now uses DOM manipulation (`document.createElement`, `.textContent`) instead of `innerHTML` for the label text. (`internal/web/templates/settings.html`)
**Files modified (15):** `internal/backup/backup.go`, `internal/backup/appdata.go`, `internal/backup/dbdump.go`, `internal/backup/restore.go`, `internal/backup/crossdrive.go`, `internal/backup/restic.go`, `internal/storage/safety_linux.go`, `internal/storage/format_linux.go`, `internal/storage/scan_linux.go`, `internal/storage/migrate.go`, `internal/stacks/delete.go`, `internal/api/router.go`, `internal/web/handlers.go`, `internal/web/storage_handlers.go`, `internal/settings/settings.go`, `internal/web/templates/settings.html`
### What was just completed (2026-02-17 session 38)
- **v0.12.2 — Restore Section Simplification (Bug 4 from v0.12.1 TASK.md):**
- **Feature: Snapshot filtering by app** — `GET /api/backup/snapshots?stack={name}` now filters snapshots to those whose `Paths` overlap with the app's HDD mount paths. Uses prefix matching (snapshot path is prefix of required, or vice versa). New `filterSnapshotsByPaths()` helper in `internal/api/router.go`. Manager gains `GetStackHDDMounts()` method to expose stackProvider's mount resolution.
- **Feature: Auto-stop/restart on restore** — `RestoreApp()` now stops the app's containers before running `restic restore` and restarts them after (even on failure). Avoids data corruption from live writes during restore. Eliminates the "Javasoljuk az alkalmazás leállítását" advisory from the UI.
- **Interface extension: StackDataProvider** — Added `StopStack(name string) error` and `StartStack(name string) error` to the `backup.StackDataProvider` interface in `internal/backup/appdata.go`. `stackAdapter` in `cmd/controller/main.go` wires these through to `stacks.Manager`.
- **UI simplification: Restore section** — Removed confusing "Visszaállítandó útvonalak" path list (technical detail not needed by customer). Snapshot dropdown now populated per-app (filtered) with human-friendly format: `2026-02-17 hétfő 03:00 (a3f2b1)`. Single calm warning replacing the triple-exclamation block. Empty filtered result shows inline message instead of empty dropdown. `data-paths` attribute removed from app dropdown options.
- **Files modified (6):** `internal/backup/appdata.go`, `internal/backup/backup.go`, `internal/backup/restore.go`, `internal/api/router.go`, `internal/web/templates/backups.html`, `cmd/controller/main.go`
### What was just completed (2026-02-17 session 37)
- **v0.12.0 — Backup Page Overhaul — Unified App Backup Status & Bug Fixes:**
- **Bug Fix 1: Duplicate unconfigured apps** — `GetFullStatus()` now returns a deep copy of the cached status. `CrossDriveSummary`, `UnconfiguredApps`, and `CrossDriveWarnings` slices are always nil in the returned copy so the handler builds them fresh on every page load. Previously the handler appended to the cached slices, causing 3× duplication on 3 page loads.
- **Bug Fix 2: Misleading "drive disconnected" error** — Replaced the binary `IsMountPoint || !IsWritable` check with tiered `CheckBackupDestination()` validation (new in `internal/system/mounts_linux.go` and stub in `mounts_other.go`). Tiers: path doesn't exist (critical/blocked), not writable (critical/blocked), same block device as `/` (warning/allowed with note about system drive), disk >95% full (critical/blocked), disk >90% (warning/allowed). `isSameBlockDevice()` replaces `IsMountPoint()` for source/dest same-device detection. Used in both `deployHandler()` and `backupsHandler()` for display, and in `crossdrive.go` logic via `CheckBackupDestination()`.
- **Bug Fix 3: Dead BackupEnabled toggle** — Removed `settingsAppBackupHandler()` from handlers.go and its `POST /settings/app-backup` route from server.go. The toggle wrote to settings.json but nothing read it to skip apps. UI nightly backup section in deploy.html now shows an informational note instead of the toggle.
- **Architecture: Unified per-app backup rows** — New `AppBackupRow` struct and `buildAppBackupRows()` in handlers.go. Replaces old "Alkalmazás adatok" + "Másolatok másik meghajtóra" sections with a single expandable row per app showing all 3 backup layers (DB, Docker volumes, user data). Status dot: green=fully covered, yellow=warning (failed run, system drive, disk full), red=HDD data without cross-drive configured, auto=no user data. Expandable JS toggle with ▶/▼ icon.
- **Architecture: Sequential backup chaining** — Removed independent `cross-drive-daily` (03:30) and `cross-drive-weekly` (04:30) scheduler jobs. Cross-drive backups now run immediately after the restic backup completes (daily jobs every night; weekly jobs on Sunday). This ensures DB dump → restic → cross-drive happen in the same window for file/DB consistency on restore.
- **Architecture: Deploy page schedule dropdown** — Removed "Csak kézi indítás" option (schedule="manual"). Two options remain: "Naponta (az éjszakai mentés után)" and "Hetente, vasárnap (az éjszakai mentés után)". Weekly option shows informational note about DB consistency implications. Existing "manual" configs treated as "weekly" in the dropdown.
- **CSS added:** `.app-backup-row`, `.app-backup-row-header`, `.app-backup-row-name`, `.app-backup-row-meta`, `.app-backup-row-detail`, `.status-dot` (green/yellow/red/auto), `.backup-layers`, `.backup-layer-row`, `.layer-label`, `.layer-badge`, `.layer-na`, `.layer-method`, `.layer-dest`, `.layer-schedule`, `.layer-last`, `.layer-unconfigured`, `.layer-actions`, `.layer-warnings`, `.backup-layer-warning`, `.btn-xs`, `.text-ok`, `.text-error`.
- **Files modified (9):** `internal/backup/backup.go`, `internal/system/mounts_linux.go`, `internal/system/mounts_other.go`, `internal/web/handlers.go`, `internal/web/server.go`, `internal/web/templates/backups.html`, `internal/web/templates/deploy.html`, `internal/web/templates/style.css`, `cmd/controller/main.go`
### What was just completed (2026-02-17 session 36)
- **v0.11.9 — UI Polish Fixes for deploy/settings backup section:**
- **Fix 1: Spacing** — `.deploy-cross-drive` `margin-bottom` increased from `1rem` to `1.5rem` for consistent spacing before deploy form.
- **Fix 2: Tooltip on "Módszer"** — Renamed "Verziózott mentés (restic)" to "Titkosított mentés (restic)". Added info `(i)` tooltip explaining rsync vs restic tradeoffs.
- **Fix 3: Nightly backup indicator** — Replaced disabled checkbox (with confusing pointer cursor) with a non-interactive green/gray dot indicator.
- **Fix 4: Progressive disclosure** — Dest/method/schedule selects are disabled until "Engedélyezve" is checked. JS `toggleCrossDriveFields()` enables/disables them. Backend handler updated to preserve existing config when disabling (disabled fields not submitted).
- **Fix 5: Emoji cleanup** — Removed all emoji from `deploy.html` backup section (h4, warning, status, hint, stale data) and `backups.html` cross-drive summary (status badges, schedule badge, unconfigured warning). JS callbacks also cleaned up.
- **CSS added:** `.info-tooltip`, `.info-icon`, `.info-tooltip-text`, `.cross-drive-nightly-status`, `.nightly-status-indicator`, `.nightly-enabled`, `.nightly-disabled`, `.meta-badge-fail`.
- **Files modified (4):** `web/templates/deploy.html`, `web/templates/backups.html`, `web/templates/style.css`, `web/handlers.go`
### What was just completed (2026-02-17 session 35)
- **v0.11.8 — Per-App Cross-Drive Backup (3-2-1 rule, second copy on different media):**
- **Feature: CrossDriveBackup data model** — `AppBackupPrefs` extended with `CrossDrive *CrossDriveBackup` field in `settings.go`. New methods: `GetCrossDriveConfig`, `SetCrossDriveConfig`, `UpdateCrossDriveStatus`, `GetAllCrossDriveConfigs`, `GetOrCreateCrossDrivePassword`. Existing `SetAppBackup`/`SetAppBackupBulk` now preserve cross-drive config. Auto-generated restic password stored in `settings.json`.
- **Feature: CrossDriveRunner** — New `internal/backup/crossdrive.go`. Supports rsync (simple mirror with `--delete`) and restic (versioned, deduplicated, shared repo). Safety guards: destination ≠ source, mount point check, writable check, per-app concurrency lock. `RunAllScheduled(ctx, schedule)` iterates all apps matching the given schedule. Status (last_run, last_status, last_error, last_duration, last_size_human) persisted to settings.json after each run.
- **Feature: Scheduler jobs** — Two new daily jobs: `cross-drive-daily` at 03:30 (for apps with `schedule: daily`), `cross-drive-weekly` at 04:30 Sundays only (for `schedule: weekly`).
- **Feature: API endpoints** — 4 new routes: `POST /api/stacks/{name}/cross-backup`, `POST /api/stacks/{name}/cross-backup/run`, `GET /api/stacks/{name}/cross-backup/status`, `POST /api/backup/cross-drive/run-all`.
- **Feature: Deploy/Settings page UI** — New "Biztonsági mentés" card on the deploy page for apps with HDD data. Shows nightly backup toggle (read-only link), cross-drive dropdowns (destination, method, schedule), last run status, manual trigger button. States: no other storage (info message), configured, destination unreachable (warning). Flash messages on save redirect.
- **Feature: Backup page summary** — New "Másolatok másik meghajtóra" section showing all configured apps with method, destination, last status, size. Warns about unconfigured apps with HDD data. Destination health warnings. "Összes futtatása most" button.
- **CSS:** `margin-bottom: 1.5rem` added to `.deploy-stale-data`. New styles: `.deploy-cross-drive`, `.cross-drive-list`, `.cross-drive-item`, `.cross-drive-header`, `.cross-drive-meta`, `.cross-drive-actions`.
- **Files modified (10):** `settings/settings.go`, `backup/crossdrive.go` (new), `backup/backup.go`, `api/router.go`, `web/handlers.go`, `web/server.go`, `web/templates/deploy.html`, `web/templates/backups.html`, `web/templates/style.css`, `cmd/controller/main.go`
### What was just completed (2026-02-17 session 34)
- **v0.11.7 — Stale Data Cleanup + FileBrowser Sync + UI Title Fix:**
- **Feature: Stale data cleanup** — After app data migration, the deploy/settings page now shows leftover data on previous storage paths with size info and a delete button. Two-step confirmation required before deletion. Protected paths (storage root, media, Dokumentumok, appdata) cannot be deleted. Also available immediately after migration on the migration-done page.
- **Fix: FileBrowser sync after migration** — `syncFileBrowserMounts()` now called after successful data migration, ensuring FileBrowser mounts reflect the current storage layout.
- **Fix: Deploy page title** — Already-deployed apps now show "Beállítások" (Settings) instead of "Telepítés" (Deploy) in both the browser page title and the `` heading.
- **Internal: Exported `ProtectedHDDPaths()`** from stacks package for reuse in web handlers.
- **Files modified (7):** `internal/stacks/delete.go`, `internal/web/handlers.go`, `internal/web/storage_handlers.go`, `internal/web/templates/deploy.html`, `internal/web/templates/migrate.html`, `internal/web/templates/style.css`
### What was just completed (2026-02-17 session 33)
- **v0.11.6 — FileBrowser Auto-Mount Sync + UI Polish (3 fixes):**
- **Feature: FileBrowser auto-mount sync** — Added `syncFileBrowserMounts()` and `generateFileBrowserCompose()` to `handlers.go`. After a storage path is added (via storage init wizard) or removed, the controller regenerates `/opt/docker/stacks/filebrowser/docker-compose.yml` with volume mounts for all registered paths (`/mnt/hdd_1:/srv/hdd_1` etc.), then recreates the FileBrowser container. Domain is read from FileBrowser's `.env`. If FileBrowser isn't deployed, the function silently returns. The generated compose is self-contained (no env vars).
- **UI Fix 1: Badge color fix** — `settings.html`: changed "Nincs csatolva!" (red `state-red`) badge to "Rendszermeghajtón" (yellow `badge-warn`). The path is on the system SSD, which isn't an error — just informational. Added `.badge-warn { background: rgba(250, 204, 21, 0.15); color: #facc15; }` to `style.css`.
- **UI Fix 2: Progress bar fix** — `storage_init.html`: replaced the disk-usage gradient progress bar (green→yellow→red zones, alarming at 30%) with a clean single-color `progress-bar-task` bar. Added `.progress-bar-task` and `.progress-bar-task .progress-fill` CSS classes to `style.css`.
- **UI Fix 3: Button text fix** — `settings.html`: "Alapértelmezett" button (reads as status, confusing) → "Legyen alapértelmezett" (clear action verb).
- **Files modified (5):** `web/handlers.go`, `web/storage_handlers.go`, `web/templates/settings.html`, `web/templates/storage_init.html`, `web/templates/style.css`
### What was just completed (2026-02-17 session 32)
- **v0.11.4 — Bugfix: Storage Initialization (FormatAndMount) — 3 bugs + 4 safety improvements:**
- **Bug 1 (sfdisk):** Added `wipefs -a` before sfdisk; changed sfdisk input from `,,,L` (unsupported GPT type shorthand) to `,,` (default Linux GUID); added `--force --wipe always` flags. Previous table confusing sfdisk and `L` type not accepted for GPT.
- **Bug 2 (mount):** Replaced `mount mountPath` (fstab lookup — uses container's /etc/fstab, not host's) with explicit `mount -t ext4 -o defaults,noatime /host-dev/sdb1 /mnt/hdd_1`. fstab entry still written to `/host-fstab` for host reboot persistence.
- **Bug 3 (mount propagation):** Changed `/mnt` volume in compose to long-form bind with `propagation: rshared`. Also ran `mount --bind /mnt /mnt && mount --make-rshared /mnt` on demo host. Confirmed `Propagation=rshared` in `docker inspect`. Mounts created inside container now propagate to host.
- **Safety 1 (post-mount verification):** Added `findmnt` check after mount — fails with clear error if mount isn't actually visible.
- **Safety 2 (ASCII label):** Use `req.MountName` (always ASCII) for ext4 `-L` label (16-byte limit). Display label (`req.Label`, may contain UTF-8 Hungarian chars) stays only in settings.json.
- **Safety 3 (smart partition):** In `storageInitAPIHandler`, if disk has exactly 1 empty partition (no filesystem), skip wipefs+sfdisk entirely and format existing partition directly. Handles demo sdb case (sdb1 exists, no FS).
- **Safety 4 (progress messages):** Updated `send()` calls to include command details (device paths, flags) for remote debugging via UI progress panel.
- **Files modified (3):** `storage/format_linux.go`, `docker-compose.yml`, `web/storage_handlers.go`
### What was just completed (2026-02-17 session 31)
- **v0.11.3 — Bugfix: Missing sfdisk in container (fdisk package):**
- `sfdisk` is in the `fdisk` package on Debian bookworm, not `util-linux`. Dockerfile had `util-linux` but not `fdisk`, so `sfdisk` was missing and partitioning failed.
- Added `fdisk` to Dockerfile's `apt-get install` list. Updated comment to clarify which package provides what.
- Verified: all six disk tools now present in container (`sfdisk`, `mkfs.ext4`, `blkid`, `mount`, `lsblk`, `partprobe`).
- **Files modified (1):** `Dockerfile`
### What was just completed (2026-02-17 session 30)
- **v0.11.2 — Bugfix: /dev/sdb not accessible inside container:**
- **Root cause:** Docker always creates a fresh tmpfs at `/dev` inside containers. Even with `privileged: true`, the bind mount `- /dev:/dev` is silently dropped. Block device nodes like `/dev/sdb` don't exist inside the container.
- **Fix:** Mount host `/dev` at `/host-dev` instead. With `privileged: true`, the kernel allows I/O to the device nodes regardless of path inside the container.
- **docker-compose.yml:** Changed `- /dev:/dev` → `- /dev:/host-dev:rw`. Also applied missing `privileged: true`, `/etc/fstab:/host-fstab`, and `/run/udev:/run/udev:ro` to demo node's live compose (never applied after v0.11.0).
- **safety.go:** Added `HostDevPath = "/host-dev"` constant and `HostDevicePath(devPath) string` helper (`/dev/sdb` → `/host-dev/sdb`).
- **format_linux.go:** All device operations (os.Stat, sfdisk, partprobe, mkfs.ext4, blkid UUID) use `HostDevicePath()`.
- **safety_linux.go:** `IsSystemDisk()` stats device via `HostDevicePath()`.
- **scan_linux.go:** `enrichWithBlkid()` probes each partition individually (`blkid -o value -s TYPE/UUID/LABEL /host-dev/sdXN`) instead of batch `blkid -o export` (which fails when `/dev` is Docker's minimal tmpfs).
- **Verified:** `/host-dev/sda`, `/host-dev/sdb`, partitions visible; `blkid /host-dev/sdb1` returns correct UUID/fstype/label.
- **Files modified (5):** `storage/safety.go`, `storage/safety_linux.go`, `storage/format_linux.go`, `storage/scan_linux.go`, `docker-compose.yml`
### What was just completed (2026-02-17 session 29)
- **v0.11.1 — Bugfix: Storage Scan — System Disk Detection & FSType in Container:**
- **Bug 1 fix: System disk detection** — Replaced mount-point string comparison (`== "/"`, `"/boot"`, `"/boot/efi"`) with host fstab parsing. Inside the container, `lsblk` reports container mount points (e.g. `/opt/docker/felhom-controller/data`), not host mount points. New `getSystemDiskNames()` reads `/host-fstab` (fallback: `/etc/fstab`), finds system entries (`/`, `/boot`, `/boot/efi`, `swap`), resolves `UUID=` entries to device paths via `blkid -U`, and marks parent disks as system. `partitionToParentDisk()` handles both standard (`sda2→sda`) and NVMe (`nvme0n1p2→nvme0n1`) naming.
- **Bug 2 fix: FSType enrichment** — `lsblk` returns null fstype in containers (udev/blkid cache incomplete). New `enrichWithBlkid()` runs `blkid -o export` after lsblk scan and fills in missing `FSType`, `UUID`, `Label` per partition from direct device probing. Runs on both `AvailableDisks` and `SystemDisks`.
- **Result:** sda (system SSD) now correctly appears in SystemDisks; sdb (USB HDD) appears in AvailableDisks; partition fstypes (vfat/ext4/swap) correctly shown; sdb1 genuinely shows "(nincs fájlrendszer)".
- **Files modified (1):** `storage/scan_linux.go`
### What was just completed (2026-02-17 session 28)
- **v0.11.0 — Phase C: Storage Init, Data Migration & Startup Fixes:**
- **Step 0: Startup ping + hub report** — Controller now fires heartbeat ping, system_health ping, and hub report immediately on startup (5s delay) instead of waiting for first scheduler tick (5-15 min). `hubPusher` instance created once and reused for both startup and periodic reports. Prevents Healthchecks showing stale "Last Ping: X ago" after restarts.
- **Step 1-3: Storage initialization wizard** — New `internal/storage/` package (`scan.go`, `format.go`, `safety.go`, `format_linux.go`, `safety_linux.go`, `scan_linux.go` + non-linux stubs). `ScanDisks()` via `lsblk -J`. `FormatAndMount()` with progress channel (partition via sfdisk → mkfs.ext4 → blkid UUID → fstab backup + UUID-based entry → mount → chown + subdirs). Safety guards: system disk detection via major device numbers, mount path conflict, confirmation "FORMÁZÁS" required. New wizard page at `/settings/storage/init`. JSON API endpoints at `/api/storage/scan`, `/api/storage/init`, `/api/storage/init/status`. Auto-registers storage path in settings.json after success.
- **Step 4-5: Data migration** — New `MigrateAppData()` in `internal/storage/migrate.go`. Per-app "Mozgatás" button on deploy page (for deployed apps with HDD data) and settings page storage app list. Migration flow: stop app → rsync with `--info=progress2` progress parsing → update `app.yaml` HDD_PATH → start app. Rollback on failure (revert config + restart with original path). Old data preserved. New migration page at `/stacks/{name}/migrate`. JSON API at `/api/storage/migrate`, `/api/storage/migrate/status`.
- **Step 6: Per-app storage display** — Deploy page (read-only mode) now shows "Adattárolás" section for deployed apps: current path + label, data size, free space. "Mozgatás" link shown when other storage paths exist.
- **Step 7: Container setup** — Added `privileged: true` to `docker-compose.yml`. New volume mounts: `/dev:/dev`, `/etc/fstab:/host-fstab`, `/run/udev:/run/udev:ro`. Docker socket changed from `:ro` to writable. `Dockerfile` adds: `util-linux`, `e2fsprogs`, `rsync`, `parted`.
- **Storage API routing** — New `/api/storage/` prefix registered in `main.go` before `/api/` catch-all (longer prefix takes priority in Go ServeMux). `ServeStorageAPI` method on web.Server handles all storage JSON endpoints.
- **CSS additions** — `.disk-step`, `.disk-step-active`, `.disk-step-done`, `.disk-progress-steps`, `.disk-progress-bar-wrap`, `.deploy-storage-info` styles.
- **Files created (13):** `storage/scan.go`, `storage/scan_linux.go`, `storage/scan_other.go`, `storage/safety.go`, `storage/safety_linux.go`, `storage/safety_other.go`, `storage/format.go`, `storage/format_linux.go`, `storage/format_other.go`, `storage/migrate.go`, `web/storage_handlers.go`, `templates/storage_init.html`, `templates/migrate.html`
- **Files modified (8):** `main.go`, `web/server.go`, `web/handlers.go`, `templates/settings.html`, `templates/deploy.html`, `templates/style.css`, `docker-compose.yml`, `Dockerfile`
### What was just completed (2026-02-17 session 27)
- **v0.10.0 — Phase B: Storage Management UI Polish & Health Severity Fix:**
- **Step 0: Health severity fix** — `checkStoragePaths()` mount-point check reclassified from **issue** (FAIL) to **warning** (WARN). All storage health messages translated to Hungarian. Added `.monitoring-banner-warn` CSS class for yellow warning banners. Prevents false FAIL status on demo/test environments where storage is intentionally on SSD.
- **Step 1: Success flash messages** — All 4 storage handlers (add/remove/set-default/toggle-schedulable) now redirect with `?storage_msg=success&storage_detail=...` query params. Settings page displays green "alert-info" flash on success. Consistent with backup page flash pattern.
- **Step 2: Edit storage path labels** — New `SetStorageLabel()` method in `settings.go`. New `POST /settings/storage/label` route + handler. Inline edit UI with ✏️ button, text input, OK/Cancel. Added `.btn-ghost` CSS class.
- **Step 3: App details per storage path** — Settings page now shows expandable `` list per storage path with app names, sizes, and links to deploy page. New `StorageAppDetail` struct + `appDetailsForPath()` helper. Added CSS for `.storage-app-details`, `.storage-app-list`, `.storage-app-row`.
- **Step 4: Storage badge on stacks page** — Deployed app cards show "💾 Label" badge indicating which registered storage path the app uses. `StorageLabels` map built from deployed apps' HDD_PATH → registered storage path label lookup. Added `.meta-badge-storage` CSS.
- **Step 5: Deploy dropdown enhancements** — Storage path dropdown now shows free space ("234 GB szabad"). `DeployStoragePath` struct wraps `StoragePath` with `FreeHuman`/`FreePercent` from `GetDiskUsage()`. JS `checkStorageSpace()` shows yellow warning when selected storage has <20% free.
- **Step 6: Filesystem & disk info** — New `FSInfo` struct + `GetFSInfo()` in `mounts_linux.go` using `findmnt` command + `/sys/block/` sysfs reads for disk model. Settings page shows "ext4 · /dev/sdb1 · WD Elements" below disk usage bar. Non-Linux stub returns nil.
- **Step 7: Backup page storage context** — Added `StorageLabel` field to `AppBackupInfo`. Backup page shows storage label badge per app by matching HDD path prefixes against registered storage paths. Uses existing `.meta-badge-storage` CSS.
- **Files modified (12):** `healthcheck.go`, `settings.go`, `mounts_linux.go`, `mounts_other.go`, `appdata.go`, `handlers.go`, `server.go`, `settings.html`, `stacks.html`, `deploy.html`, `backups.html`, `style.css`
### What was previously completed (2026-02-17 session 26)
- **v0.9.0 — Phase A: Storage Paths Foundation & Backup Toggle Fix:**
- **Root cause:** Per-app backup toggles (v0.8.0) didn't appear because `controller.yaml` had no `paths.hdd_path` set → `ParseComposeHDDMounts` returned nil. Even with global hdd_path, apps with different HDD_PATH values wouldn't match.
- **Core fix: Per-app HDD_PATH resolution** — `stackAdapter.GetStackHDDMounts()` now reads each app's own `HDD_PATH` from its `app.yaml` env section (Priority 1), falling back to all registered storage paths (Priority 2). Removed dependency on global `cfg.Paths.HDDPath`.
- **Storage paths registry** (`settings.json`) — new `StoragePath` struct with Path, Label, IsDefault, Schedulable, AddedAt. Thread-safe CRUD methods in `settings.go` (Get/Add/Remove/SetDefault/SetSchedulable). Multiple external storage paths supported.
- **Auto-discovery** — On startup, `discoverHDDPaths()` scans deployed apps' `app.yaml` for `HDD_PATH` values. `AutoDiscoverStoragePaths()` registers discovered paths with inferred labels. Legacy `cfg.Paths.HDDPath` used as fallback.
- **Mount-point validation** — New `mounts_linux.go` (build-tagged): `IsMountPoint()` via `syscall.Stat_t.Dev` comparison, `IsWritable()`, `PathsOverlap()`, `GetDiskUsage()` via `syscall.Statfs`. Non-Linux stubs in `mounts_other.go`.
- **Settings page "Adattárolók" section** — Lists registered paths with label, path, disk usage bar, app count, badges (default/active/unmounted). Actions: set default, toggle schedulable, remove (with guards). Expandable "Új adattároló hozzáadása" form with 5-step validation (exists, mount point, writable, no overlap, no duplicate).
- **Deploy page storage dropdown** — `path` field type renders as `` dropdown of schedulable storage paths. Falls back to text input with warning if no paths registered.
- **Health check storage monitoring** — `RunHealthCheck()` now accepts `storagePaths` parameter. Checks: path accessible (warning), not a mount point (issue — data writes to SSD!), disk usage ≥95% (issue) / ≥90% (warning).
- **Controller docker-compose.yml** — Changed HDD mount from `${HDD_PATH:-/mnt/hdd_placeholder}:...:ro` to `/mnt:/mnt:rw` for multi-storage support + restore capability.
- **Removed unused `hddPath` param** from `DiscoverAppData()` signature in backup/appdata.go.
- **Files created (2):** `system/mounts_linux.go`, `system/mounts_other.go`
- **Files modified (11):** `settings.go`, `main.go`, `appdata.go`, `backup.go`, `handlers.go`, `server.go`, `settings.html`, `deploy.html`, `style.css`, `healthcheck.go`, `docker-compose.yml`, `report/builder.go`
### What was previously completed (2026-02-16 session 25)
- **v0.8.0 — Phase 7: Storage Overview, Per-App Backup Toggles & Limited Restore:**
- **Storage overview on backup page** — new "Tárhely áttekintés" section as first section on backup page showing SSD/HDD progress bars + backup repo stats (repo size, dump file count, snapshot count). Reuses existing `system.GetInfo()` and `RepoStats`.
- **Restic password visibility** — new "Titkosítási kulcs" section inside the repository card. Masked password field with show/copy buttons (JS toggle). Password synced to hub via periodic report for disaster recovery (`ResticPassword` field added to `BackupReport`).
- **App data discovery** — new `internal/backup/appdata.go`:
- `StackDataProvider` interface to avoid circular imports between backup and stacks packages
- `AppBackupInfo`, `AppDataPath`, `AppDockerVolume` structs
- `DiscoverAppData()` iterates deployed stacks, discovers HDD bind mounts (via adapter calling `ParseComposeHDDMounts`), Docker named volumes (via `parseComposeNamedVolumes` using YAML parser), and DB dump status
- Stack adapter in `main.go` implements `StackDataProvider` using `stacks.Manager`
- **Per-app backup toggles** — new "Alkalmazás adatok" section on backup page:
- Toggle checkbox per app (only for apps with HDD data)
- Shows HDD paths with sizes, Docker volume info, DB dump notes
- `POST /settings/app-backup` handler saves preferences to `settings.json`
- `AppBackupPrefs` struct + bulk getter/setter in `settings.go`
- `RefreshCache()` populates `AppDataInfo` via `DiscoverAppData()`
- **Dynamic backup paths** — `RunBackup()` now includes enabled app HDD data paths:
- `resolveAppBackupPaths()` reads enabled apps from settings, resolves HDD paths via provider
- Paths logged at INFO level, included in restic snapshot
- `BackupPaths` display on backup page includes app data paths
- **Limited app restore** — new restore section on backup page:
- `RestoreApp()` in `restore.go`: validates enabled, resolves HDD paths, validates snapshot exists, uses running mutex
- `RestoreAppData()` on `ResticManager`: runs `restic restore` with `--include` flags for specific paths
- `POST /backup/restore` web handler with confirmation flow
- `GET /api/backup/snapshots` JSON endpoint for restore dropdown
- UI: app/snapshot dropdowns, warning box, confirmation checkbox, JS-driven form submission
- **Exported `ParseComposeHDDMounts`** from stacks package (was unexported `parseComposeHDDMounts`)
- **Flash messages** on backup page via query params (success/error redirects from handlers)
- **CSS**: New styles for storage overview grid, app backup toggles, encryption key field, restore section, flash messages
- **Files created**: `appdata.go`, `restore.go`
- **Files modified**: `backup.go`, `restic.go`, `handlers.go`, `server.go`, `backups.html`, `style.css`, `settings.go`, `delete.go`, `router.go`, `types.go`, `builder.go`, `main.go`
### What was previously completed (2026-02-16 session 24)
- **v0.7.2 — Fix Notification Preferences Sync (Controller → Hub):**
- **Two repos changed** (deploy-felhom-compose + felhom.eu):
- **Hub: `POST /api/v1/preferences` endpoint** (`hub/internal/api/handler.go`):
- New route in API handler: same Bearer token auth as /report and /notify
- Accepts JSON payload: `{customer_id, email, enabled_events}`
- Calls existing `store.SaveNotificationPrefs()` — no store changes needed
- Logs preference updates at INFO level
- **Hub: Notification section on customer detail page** (`hub/internal/web/`, `hub/internal/store/store.go`):
- New `GetRecentNotifications()` store method returns last N notification_log entries
- `handleCustomerDetail()` loads NotifPrefs + RecentNotifications
- `joinStrings` template function added for event list display
- `customer.html` template: new "Notifications" section showing email, events, and last 10 notification log entries (time, event, status, message)
- **Controller: `SyncPreferences` method** (`internal/notify/notifier.go`):
- New `preferencesRequest` struct for JSON payload
- `SyncPreferences(email, enabledEvents)` — synchronous POST to hub `/api/v1/preferences`
- `IsEnabled()` getter for checking hub connectivity
- Hungarian error messages for user-facing feedback
- **Controller: Sync on settings save** (`internal/web/handlers.go`):
- `settingsNotificationsHandler` now calls `SyncPreferences` after saving to `settings.json`
- Three flash message variants: success (synced), warning (local save OK, sync failed), error (save failed)
- Local save always succeeds even if hub sync fails
- **Controller: Sync on startup** (`cmd/controller/main.go`):
- Non-blocking goroutine syncs preferences to hub when controller starts
- Only runs if hub is enabled and email is configured
- Handles hub DB rebuild recovery (re-populates preferences after hub redeployment)
- **Files changed**: hub (3 files: handler.go, store.go, server.go, customer.html), controller (3 files: notifier.go, handlers.go, main.go)
- **Documentation**: README.md updated (version, notify module, phase checklist), CONTEXT.md updated
### What was previously completed (2026-02-16 session 23)
- **v0.7.1 — Phase 2: Monitoring Warnings, Dashboard Alerts & Notification System:**
- **Three workstreams across two repos** (deploy-felhom-compose + felhom.eu):
- **Monitoring page "Távoli monitoring" section** (`monitoring.html`, `handlers.go`):
- New section between System Overview and System Metrics showing healthcheck ping UUID status
- 5 rows: Heartbeat, System Health, DB Dump, Backup, Backup Integrity — each shows ✅ configured or ⚠️ missing
- Banner: green (all configured), yellow (some missing), red (monitoring disabled)
- `isPingConfigured()` helper checks non-empty AND not "CHANGEME" prefix
- **Dashboard alert banners** (new `alerts.go`, `layout.html`):
- `AlertManager` struct with `Refresh()` + `GetAlerts()` — generates alerts from health report, missing pings, backup disabled
- Alert types: `Alert{ID, Level, Message, Link, LinkText}` — levels: error/warning/info
- Renders colored banners (red/yellow/blue) after `` on all pages
- Caps at 5 alerts with "+N more" overflow; monitoring page excludes "pings-missing" (shown in table instead)
- Refreshed every 5 min via system-health scheduler task + once at startup
- **Hub notification relay** (felhom.eu repo — `hub/internal/api/handler.go`, `hub/internal/store/store.go`):
- `POST /api/v1/notify` endpoint: Bearer auth, JSON payload (customer_id, event_type, severity, message, details)
- New `customer_notifications` table (email, enabled_events JSON) + `notification_log` audit table
- Resend email integration: direct HTTP POST to `https://api.resend.com/emails`
- Hungarian email template with event details, timestamp, severity
- `hub.yaml.example` updated with notifications config section
- **Controller-side notifier** (new `internal/notify/notifier.go`):
- `Notifier` struct: fires HTTP POST to hub `/api/v1/notify`, non-blocking (goroutine)
- Cooldown tracking per event type (default 6h, configurable via UI)
- Checks notification preferences (email configured + event enabled) before sending
- `NotifyHealthChange()`: only notifies on status degradation (ok→warn, ok→fail, warn→fail)
- `NotifyBackupFailed/NotifyDBDumpFailed/NotifyIntegrityFailed` convenience methods
- `SendTest()` for test email flow
- Wired into scheduler: system-health task calls `NotifyHealthChange()`, backup tasks call failure notifiers
- **Notification preferences UI** (`settings.html`, `handlers.go`):
- New "Értesítések" Section C on Settings page (only shown when hub enabled)
- Email input, 4 event checkboxes (disk_warning, backup_failed, update_available, security_update)
- Cooldown hours input (default 6)
- "Mentés" + "Teszt email küldése" buttons
- Saved to `settings.json` via `NotificationPrefs` struct (Email, EnabledEvents, CooldownHours)
- **Settings persistence expanded** (`settings.go`):
- `NotificationPrefs` struct with Email, EnabledEvents, CooldownHours
- `DefaultEnabledEvents`: disk_warning, backup_failed, update_available
- `GetNotificationPrefs()` returns defaults if nil, `SetNotificationPrefs()` saves atomically
- **Files changed**: 3 new (alerts.go, notifier.go, notify package), ~12 modified across both repos
- **Deployed:** Controller v0.7.1 to demo-felhom.eu, verified healthy (0 alerts on clean system)
### What was previously completed (2026-02-16 session 22)
- **v0.7.0 — Phase 1: Authentication, Persistence & Settings Page:**
- **New `internal/settings/settings.go`:** Shared persistence layer via `settings.json` in the data directory. Atomic writes (tmp + rename), thread-safe with `sync.RWMutex`. Stores password hash overrides and DB validation cache. Graceful handling if file doesn't exist.
- **Auth improvements:**
- Password resolution priority: `settings.json` → `controller.yaml` → none (open dashboard)
- Startup logs which source is active: `Auth: using password from settings.json/controller.yaml/no password configured`
- Session duration extended to 7 days (was 24h)
- `?next=` redirect after session expiry — returns user to the page they were on
- Flash messages on login page (green info box, used after password change)
- Conditional logout link — hidden when auth is disabled (no password configured)
- `invalidateAllSessions()` method for password change flow
- **New Settings page (`/settings`):**
- "Rendszer konfiguráció" section: read-only display of controller.yaml values (customer ID/name/domain, git repo/sync interval, backup enabled/schedule, monitoring, healthchecks URL, hub status, controller version)
- "Jelszó módosítás" section: form with current password, new password, confirm — validates min 8 chars, match check, bcrypt comparison
- Password saved to `settings.json`, all sessions invalidated, redirect to login with flash message
- Only shown if auth is enabled; otherwise shows info message to contact operator
- **Sidebar update:**
- "Beállítások" menu item with ⚙ icon pinned to bottom (above version/logout)
- Version and logout link separated from nav links
- Logout link conditionally shown only when auth is enabled
- **DB validation persistence:**
- After each successful dump, validation results saved to `settings.json` (`db_validations` map keyed by filename)
- Cached data survives container restarts
- `DBValidationCache` struct with `validated_at`, `table_count`, `has_header`, `error`
- **10 files changed** (3 new: settings.go, settings.html; 7 modified: main.go, backup.go, auth.go, handlers.go, server.go, layout.html, login.html, style.css)
- **Deployed:** Controller v0.7.0 to demo-felhom.eu, verified healthy
### What was previously completed (2026-02-16 session 21)
- **v0.6.3 — Bug fixes from v0.6.2 code scan (4 minor fixes):**
- **Bug 1:** `--hdd-path` in `docker-setup.sh` now uses `require_arg` validation like all other flags. Previously, `--hdd-path` as the last argument without a value would crash with a cryptic bash error under `set -u` instead of a friendly message.
- **Bug 2:** `stackAction()` in `layout.html` now receives `event` as an explicit parameter instead of relying on the deprecated implicit `window.event`. All 10 onclick call sites in `dashboard.html` and `stacks.html` updated to pass `event` as first argument.
- **Bug 3:** Page `` now has an em dash separator: `"Vezérlőpult — Felhom.eu"` instead of `"VezérlőpultFelhom.eu"`.
- **Bug 4:** `nextPruneLabel()` in `funcmap.go` now returns `"ma"` (Hungarian for "today") on Sunday before 4am, consistent with the `nextRunLabel` function. Previously returned the date in `"2006-01-02"` format.
- **Deployed:** Controller v0.6.3 to demo-felhom.eu, verified healthy
### What was previously completed (2026-02-16 session 20)
- **Hub Dashboard Bugs + Backup Validation Fix (3 bugs):**
- **Bug 1&2 (Hub repo, felhom-hub v0.1.2):** Hub timestamp parsing failure — `time.Parse` with single hardcoded format silently failed for formats returned by `modernc.org/sqlite`. Added `parseSQLiteTime()` that tries 6 common formats. Fixed: hub main page showing DOWN despite OK status, and report history timestamps showing 00:00:00.
- **Bug 3 (Controller repo, v0.6.2):** Backup page showing "Hiba" for all DB validations — zero-value `DumpValidation{}` (never assigned) hit the `{{else}}` branch in template. Three fixes:
- Template: 4-branch guard (Valid → OK / Error → Hiba / zero-value → "–" with tooltip)
- Debug logging: Added `[DEBUG]` and `[WARN]` log lines to all `ValidateDump()` code paths
- Re-validation: `RefreshCache()` now cross-checks `lastDBDump` results against fresh `ListDumpFiles()` validation, healing stale in-memory state
- **Deployed:** Hub v0.1.2 to k3s, Controller v0.6.2 to demo-felhom
- **Verified:** Controller logs show `ValidateDump OK` for all 3 databases (immich: 60 tables, paperless: 67 tables, romm: 14 tables)
### What was previously completed (2026-02-16 session 19)
- **v0.6.1 — Code Review Bugfixes (7 fixes):**
- **Fix 1:** `http.NotFound(w, nil)` → pass actual `*http.Request` in `deployHandler` and `appDetailHandler`
- **Fix 2:** Dashboard running/stopped counts now computed from the filtered `deployedStacks` set (was counting ALL stacks including non-deployed)
- **Fix 3:** Session cookie `Secure` flag now dynamic based on `r.TLS != nil || X-Forwarded-Proto == "https"`. `SameSite` changed from `Strict` to `Lax` (Strict breaks Cloudflare Tunnel redirects)
- **Fix 4:** Removed misleading `subtle.ConstantTimeCompare` from `isValidSession()` (map lookup already leaks timing; comparing token to itself is meaningless). Removed unused `token` field from `session` struct. Removed `crypto/subtle` import.
- **Fix 5:** Replaced `time.Tick()` (goroutine leak) with proper `time.NewTicker` + `done` channel in `cleanupSessions()`. Added `Close()` method to Server. Added `done chan struct{}` to Server struct.
- **Fix 6:** Added `http.MaxBytesReader(w, req.Body, 1<<20)` (1MB limit) to `deployStack`, `updateOptionalConfig`, `deleteStack` API handlers via `limitBody()` helper.
- **Fix 7:** Cached `time.LoadLocation("Europe/Budapest")` once at top of `templateFuncMap()`, removed 5 per-function `LoadLocation` calls (timeAgo, fmtTime, fmtTimeShort, nextRunLabel, nextPruneLabel).
- **Post-fix verification:** All 4 grep checks pass (0 results for NotFound(w,nil), ConstantTimeCompare, time.Tick(, Secure:.*true). `go vet ./...` clean.
- **Controller version:** v0.6.1 — deployed and verified on demo-felhom.eu
### What was previously completed (2026-02-16 session 18)
- **v0.6.0 — Healthcheck Implementation + Central Push + Hub Dashboard:**
- **Part 1 — Healthcheck enhancements (controller-side):**
- Added `heartbeat` ping — lightweight "I'm alive" signal every 5 min (no logic, just ping)
- Added `backup_integrity` ping — weekly `restic check` on Sunday 04:00, pings healthchecks with result
- Added `Heartbeat` and `BackupIntegrity` fields to `PingUUIDsConfig`
- Added `RunIntegrityCheck()` to backup Manager (calls restic Check(), updates lastCheckTime/lastCheckOK, pings)
- Updated `controller.yaml.example` with new monitoring ping_uuids
- Created `monitoring/DEPRECATED.md` for legacy bash monitoring scripts
- **Part 2 — Central hub reporting (controller-side):**
- New `internal/report/` package: types.go (Report struct), builder.go (BuildReport), pusher.go (HTTP push)
- Report builder gathers data from all subsystems: system info (via metrics.GetStaticInfo + system.GetInfo), container stats (via metricsStore.QueryContainerSummary), backup status (via backupMgr.GetFullStatus), health (via monitor.RunHealthCheck), stacks (via stackMgr.GetStacks)
- Report pusher: POST JSON to hub with Bearer token auth, 3 retries with 5s backoff, never fails caller
- Added `HubConfig` to config.go (enabled, url, api_key, push_interval)
- Wired hub reporting into scheduler (configurable interval, default 15m)
- Hub reporting disabled by default (hub.enabled: false)
- **Part 3 — Hub service (felhom.eu repo, new `hub/` subfolder):**
- Full Go service: `cmd/hub/main.go`, `internal/api/handler.go`, `internal/store/store.go`, `internal/web/server.go`
- SQLite store with WAL mode, auto-migration, denormalized fields for fast queries
- REST API: POST /api/v1/report (Bearer token auth), GET /api/v1/customers, GET /api/v1/customers/{id}, GET /api/v1/customers/{id}/history
- Dark theme dashboard (English): multi-customer overview table with status indicators, customer detail page with system/storage/containers/backup/health sections
- Color coding: green (OK, <30min), yellow (warn or 30-60min), red (fail or >60min)
- K8s manifest: Deployment + Service + Ingress for hub.felhom.eu in felhom-system namespace
- Dockerfile, Makefile, hub.yaml.example config
- 90-day report retention with daily auto-prune
- **Controller version:** v0.6.0 — deployed and verified on demo-felhom.eu (9 scheduler jobs, all new jobs registered)
- **Manual steps remaining for Viktor (Part 4 of TASK.md):**
- Create 5 healthcheck checks on status.felhom.eu (heartbeat, system-health, db-dump, backup, backup-integrity)
- Update controller.yaml on demo-felhom with real UUIDs
- Build and deploy felhom-hub to k3s cluster
- Configure hub.felhom.eu DNS in Cloudflare
- Enable hub reporting on demo-felhom controller.yaml
### What was previously completed (2026-02-16 session 17)
- **v0.5.4 — Monitoring Page Frontend Fixes (4 bugs, frontend-only):**
- **Bug 1: Tooltip "Invalid Date"** — `items[0].parsed.x` unreliable across Chart.js versions. Fixed tooltip callback to use `items[0].raw.x` (direct {x,y} data access) with `parsed.x` as fallback.
- **Bug 2: Charts fill full width regardless of data density** — `setChartXBounds()` setting `min/max` at runtime was ignored because the scale was created without them. Fixed by including `min: now - defaultRangeMs, max: now` in the initial `chartOpts()` options. Now "7 nap" shows full 7-day x-axis with data clustered on the right.
- **Bug 3: Sysinfo values not consistently right-aligned** — `.sysinfo-grid` used `auto-fill` creating variable-width cells. Fixed to `1fr 1fr` (fixed 2-column). Added `align-items: baseline`, `gap: 1rem`, `white-space: nowrap` on labels, `font-weight: 600` + `word-break: break-word` on values. Removed redundant `