## Changelog ### What was just completed (2026-02-20 session 64) - **v0.21.0 — Hub Monitoring Takeover (Controller-side, Phases 5+6):** Replaces external Healthchecks.io dependency with Hub-native event system. The controller now pushes structured events directly to the Hub's `/api/v1/event` endpoint, and the Hub handles dead man's switch detection, notification dispatch, and cooldown management. **Phase 5 — Event Push System (`internal/notify/notifier.go`):** - New core method `PushEvent(eventType, severity, message, details)` — non-blocking goroutine, 2 retries with 3s backoff, POSTs to Hub `/api/v1/event` - 8 typed detail structs: `BackupDetails`, `DBDumpDetails`, `DiskDetails`, `HealthDetails`, `StorageDetails`, `UpdateDetails`, `AppDetails`, `CrossDriveDetails` - Replaced all old `Notify*` methods with event-based equivalents: - `NotifyBackupCompleted/Failed` → `backup_completed`/`backup_failed` events - `NotifyDBDumpCompleted/Failed` → `db_dump_completed`/`db_dump_failed` events - `NotifyIntegrityOK/Failed` → `backup_integrity_ok`/`backup_integrity_failed` events - `NotifyHealthChange` → detects transitions, pushes `health_degraded`/`health_critical`/`health_recovered` - `NotifyStorageDisconnected/Reconnected` → `storage_disconnected`/`storage_reconnected` events - `NotifyControllerStarted` → `controller_started` event on startup - `NotifyControllerUpdated` → `controller_updated` event (replaces `NotifyUpdateSuccess/Failed`) - `NotifyAppDeployed/Removed` → `app_deployed`/`app_removed` events - `NotifyCrossDriveCompleted/Failed` → `crossdrive_completed`/`crossdrive_failed` events - `NotifyDRStarted/Completed` → `disaster_recovery_started`/`disaster_recovery_completed` events - Removed old `/api/v1/notify` relay, `classifyWarning()`, and client-side cooldown logic (Hub handles cooldowns now) - `SendTest()` now pushes `test` event type via `PushEvent` - `SyncPreferences` updated to include `cooldownHours` parameter **Phase 5 — Event Wiring:** - `main.go`: Wired success events for backup, db-dump, integrity check; startup event with 5s delay; update event after `VerifyStartup()` - `router.go`: Added `NotifyAppDeployed`/`NotifyAppRemoved` after successful deploy/remove via API - `handler_restore.go`: Added `NotifyDRStarted`/`NotifyDRCompleted` in DR restore flow - `server.go`: New `HubPushStatusData` struct and `SetHubPushStatus` callback for monitoring page **Phase 5 — Hub Connection Monitoring:** - `pusher.go`: Added `PushStatus` tracking (LastAttempt, LastSuccess, LastError, Consecutive failures) to report Pusher - `handlers.go`: Monitoring page now shows Hub connection status (connected/unreachable, URL, customer ID, last success, last error) instead of Healthchecks ping UUIDs - `monitoring.html`: Replaced "Távoli monitoring" section with "Hub kapcsolat" section - `alerts.go`: Replaced "Missing ping UUIDs" alert with Hub connection alerts (`hub-disabled` warning, `hub-unreachable` error) **Phase 5 — Expanded Notification Settings:** - `settings.html`: Expanded from 4 checkboxes to 11 grouped toggles in two categories: - "Hibák és figyelmeztetések": backup_failed, db_dump_failed, backup_integrity_failed, crossdrive_failed, disk alerts, storage_disconnected, node_down, health_critical, expected missed - "Tájékoztató": storage_reconnected, health_recovered - Compound toggles: "Lemez figyelmeztetés" maps to `disk_warning` + `disk_critical`; "Elvárt mentés elmaradt" maps to `expected_backup_missed` + `expected_dbdump_missed` - `settings.go`: Updated `DefaultEnabledEvents` to new Hub event types - `handlers.go`: Updated settings POST handler for expanded event names and compound toggles **Phase 6 — Config Cleanup:** - `main.go`: Deprecation log on startup when ping UUIDs are configured: `[INFO] Healthchecks ping UUIDs configured but no longer used — monitoring is now handled by the Hub` - Pinger still runs for transitional backward compatibility ### What was just completed (2026-02-20 session 63) - **v0.20.0 — Hub Config Management (Phase B):** Two new features enabling the Hub to manage and compare controller configuration remotely. **Feature A — Config Apply Endpoint:** - `router.go`: Added `POST /api/config/apply` — accepts YAML body from Hub, validates it's parseable via `config.LoadFromBytes()`, writes atomically to controller.yaml (`.tmp` + `os.Rename`), returns success JSON. Restart required to apply. - `router.go`: Added `GET /api/config/hash` — returns SHA256 hex digest of current controller.yaml - `router.go`: Router struct gained `configPath string` field; `NewRouter()` signature updated - `config.go`: Added `LoadFromBytes([]byte)` — parses YAML without file I/O (for validation) - `config.go`: Added `FileHash(path)` — SHA256 hex digest helper - `main.go`: Config endpoints use same dual auth middleware as self-update (session OR Hub API key Bearer token) - `main.go`: Added `/api/config/` mux entry with `selfUpdateAuthMiddleware` **Feature B — Config Hash in Reports:** - `types.go`: Added `ConfigHash string` field to `Report` struct (JSON: `config_hash`) - `builder.go`: `BuildReport()` now accepts `configPath string` parameter, computes SHA256 of controller.yaml and includes it in every report - `main.go`: All 4 `BuildReport()` call sites updated to pass `*configPath` - Hub uses this hash to compare against its generated YAML — shows "In sync" / "Config mismatch" / "Unknown" on the unified customer detail page ### What was just completed (2026-02-20 session 62) - **docker-setup.sh — Hub Config Download:** - Added `--hub-customer` and `--hub-password` CLI flags for downloading pre-configured controller.yaml from Felhom Hub - Added `HUB_URL` global variable (default: `https://hub.felhom.eu`) - Hub download logic at start of `run_config_wizard()`: downloads YAML via `curl` with `X-Retrieval-Password` header, validates response, extracts key variables (domain, CF tokens, email), sets global variables for subsequent setup steps - Falls back to interactive wizard if download fails or credentials not provided ### What was just completed (2026-02-20 session 61) - **v0.19.0 — Deployed App Removal + Missing Field Injection:** Two new features: "Eltávolítás" (Remove) action for deployed stacks and automatic missing deploy field injection on template updates. **Feature A — Deployed App Removal ("Eltávolítás"):** - `delete.go`: Added `RemoveStack()` — removes deployed (non-orphaned) stack: `docker compose down --volumes`, optional HDD data cleanup, optional backup data cleanup (DB dumps + cross-drive rsync), removes `app.yaml` only (template files preserved for redeploy); stack reverts to "Nincs telepítve" state - `delete.go`: Added `GetStackBackupData()` — returns backup path info (DB dump dir + cross-drive rsync dir) with sizes and existence status - `delete.go`: Added `RemoveResponse`, `BackupDataResponse` structs, `buildPathInfo()` helper - `router.go`: Added `POST /api/stacks/{name}/remove` endpoint — accepts `{remove_hdd_data, remove_backups}`, computes backup paths via `AppDBDumpPath()`/`AppSecondaryRsyncPath()`, cleans cross-drive config on success - `router.go`: Added `GET /api/stacks/{name}/backup-data` endpoint — returns backup data paths with sizes - `crossdrive.go`: Made `getAppDrivePath` → `GetAppDrivePath` (public) for use by router - `stacks.html`: Added "Eltávolítás" button for stopped, deployed, non-orphaned, non-protected stacks - `dashboard.html`: Same button in compact card layout - `layout.html`: Added `removeStack()` modal — fetches HDD + backup data in parallel, 3-section layout (always removed / HDD data with checkbox / backup data with checkbox), reimport warning for preserved HDD data, restic retention note - `layout.html`: Added `confirmRemoveStack()` — POST to `/remove`, shows result summary with removed/preserved paths **Feature B — Missing Deploy Field Injection:** - `deploy.go`: Added `InjectMissingFields(stackNames)` — iterates deployed stacks, compares `.felhom.yml` deploy_fields against `app.yaml` env vars, auto-generates values for missing `secret` (using generator spec) and `domain` fields, saves updated `app.yaml` - `deploy.go`: Added `base64key` generator type — produces `base64:` (for Laravel APP_KEY and similar) - `deploy.go`: Added `containsStr()` helper - `manager.go`: Added `DeployedStackNames()` — returns names of all deployed stacks - `sync.go`: Added `postSyncHook func(updated []string)` field to `Syncer`; `New()` accepts optional hook; hook called in `doSync()` after rescan with names of updated stacks - `main.go`: Wired injection on startup (all deployed stacks) and after sync (updated stacks only) ### v0.18.0 (2026-02-19 session 60) - **v0.18.0 — Drive Migration & Tier 2 Restic Deprecation:** Full drive replacement workflow with decommissioned state, enhanced per-app migration with backup awareness, and deprecation of restic as a Tier 2 cross-drive backup method (rsync only). **Phase 1 — Restic Tier 2 Deprecation:** - `settings.go`: Auto-migrate restic→rsync on startup via `migrateResticToRsync()` in `Load()` - `crossdrive.go`: Removed `runResticBackup()`, `pruneResticRepo()`, `ensureResticRepo()`; `RunAppBackup()` calls rsync directly - `backup.go`: Removed Tier 2 secondary restic scanning from `ListAllSnapshots()` - `settings.go`: Removed cross-drive restic password methods (`GetOrCreateCrossDrivePassword`, etc.) - `deploy.html`: Removed method dropdown (rsync/restic selector) - `handlers.go`: Simplified `Tier2DriveGroup` (flat `Items` list), removed method handling from `settingsCrossBackupHandler()` - `backups.html`: Removed method split in Tier 2 details section - `router.go`: Always set method to "rsync" in cross-backup API - `infra_backup.go`: Removed cross-drive password block from `CollectInfraBackup()` - `main.go`: Removed `SetCrossDriveResticPassword` restore block **Phase 2 — Enhanced Per-App Migration:** - `backup.go`: Extracted `backupDrive()` from `runBackupInternal()` loop; added `TryRunDriveBackup()` with non-blocking lock - `crossdrive.go`: Added `AnyRunning()` method - `migrate.go`: Added `BackupTrigger` interface, `MigrateOrchestrator`, `RunEnhancedMigration()` with post-migration steps (DB dump copy, Tier 2 conflict clearing, auto-delete stale data, immediate Tier 1 backup) - `storage_handlers.go`: Wired orchestrator into migration handler with `auto_delete_stale` support - `migrate.html`: Added auto-delete checkbox, "cleaning" + "backing_up" progress steps **Phase 3 — Full Drive Migration:** - `settings.go`: Added `Decommissioned`/`DecommissionedAt`/`MigratedTo` fields to `StoragePath`; added `SetDecommissioned()`, `ClearDecommissioned()`, `IsDecommissioned()`, `GetDecommissionedPaths()`, `GetStorageLabel()`; `GetConnectedPaths()`/`GetSchedulableStoragePaths()` exclude decommissioned - `migrate_drive.go` (NEW): `DriveMigrator` with `MigrateDrive()` 10-step flow (validate→stop→rsync→verify→configure→decommission→Tier2→start→backup→notify), `migrationTx` rollback pattern, excludes restic repos from rsync - `settings.html`: Decommissioned card variant with "Kiváltva" badge, "Összes adat átköltöztetése" button on connected cards - `migrate_drive.html` (NEW): Drive migration wizard (form + progress + done cards) - `storage_handlers.go`: Added `/api/storage/migrate-drive`, `/api/storage/migrate-drive/status`, `/api/storage/decommission/remove` endpoints - `server.go`: Added `/settings/storage/migrate-drive` route, `SetDriveMigrator()` setter - `watchdog.go`: Skip decommissioned drives in `Check()`; block `SafeDisconnect()` for decommissioned - `healthcheck.go`: Skip decommissioned paths in `checkStoragePaths()` - `backup.go`: Skip decommissioned drives in `backupDrive()`/`runDBDumpsInternal()`; added `MigrationActiveCheck` callback to skip nightly backup during migration - `crossdrive.go`: Reject decommissioned destinations in `ValidateDestination()`; skip decommissioned paths in `AutoEnableSmallApps()` - `handlers.go`: Skip decommissioned drives in `buildStorageBars()`; made `SyncFileBrowserMounts()` public - `main.go`: Added `driveMigrateStackAdapter`, wired `DriveMigrator` with all dependencies **Phase 4 — Hub Changes:** - `report/types.go`: Added `Decommissioned`/`MigratedTo` fields to `StorageReport` - `report/builder.go`: Include decommissioned drives in report with flag **Files modified:** 21 files modified + 2 new files (`migrate_drive.go`, `migrate_drive.html`). ### What was just completed (2026-02-19 session 59) - **v0.16.1 + hub v0.1.8 — Hub Update Trigger + Controller URL Reporting:** Controller now includes its external URL (`controller_url`) in periodic hub reports so the hub can trigger self-updates remotely. Hub tracks the URL in a new `controller_url` DB column, checks the Gitea registry for the latest controller image version (VersionChecker goroutine, `web/version.go`), and shows a "Controller Update" card on the customer detail page. **Controller (v0.16.1):** - `internal/report/types.go`: Added `ControllerURL string` field to Report struct. - `internal/report/builder.go`: Sets `ControllerURL` from `cfg.Customer.Domain` → `https://felhom.`. - `internal/api/router.go`: **Bug fix** — moved selfupdate routes to before `hasSuffix(path, "/update")` stack case (which was catching `/selfupdate/update` first). **Hub (v0.1.8):** - `cmd/hub/main.go`: Added `Registry` config section + defaults; creates `VersionChecker` goroutine if credentials configured; passes `apiKey` to `web.New()`. - `internal/store/store.go`: Added `ControllerURL` to `CustomerSummary`; idempotent `ALTER TABLE reports ADD COLUMN controller_url TEXT` migration; updated `SaveReport`, `GetCustomers`, `GetCustomer`, `GetCustomerHistory` queries. - `internal/web/version.go` (NEW): `VersionChecker` type — polls Gitea Docker Registry V2 API (`/v2///tags/list`) every 6h; parses semver tags; stores latest version thread-safely. - `internal/web/server.go`: Added `apiKey`, `versionChecker` fields; updated `New()` signature; added `SetVersionChecker()`; added `handleTriggerUpdate` handler that proxies POST to controller's `/api/selfupdate/update`; added trigger-update route (before `/customers/` catch-all); updated `handleCustomerDetail` with `ControllerURL`, `LatestVersion`, `UpdateAvailable` template data; added `compareVersions` helper. - `internal/web/templates/customer.html`: New "Controller Update" section between Health and Notifications — shows current/latest version with update indicator, controller URL link, and conditional "Trigger Update" button with JS. - `internal/api/handler.go`: Added `ControllerURL` to `/api/v1/customers` JSON response. - Hub config (`hub.yaml`): Added `registry:` section with Gitea admin credentials. **Files modified/created:** controller: 3 files; hub: 5 modified + 1 created (version.go). ### What was just completed (2026-02-19 session 58) - **v0.16.0 — Controller Self-Update:** Watchtower-style self-update mechanism. New package `internal/selfupdate/` with 3 files: `version.go` (semver parsing/comparison), `state.go` (audit log state file I/O), `updater.go` (registry check via Gitea V2 API, update trigger, startup verification). **Flow:** Gitea registry tag list → `docker pull` → atomic compose file rewrite → `docker compose up -d` → process replaced. State file (`update-state.json`) persists across restart as audit log; verified on next startup to detect success/failure. **Config:** `SelfUpdateConfig` extended with `AutoUpdateTime` field + defaults for `Image` and `AutoUpdateTime`. Scheduler jobs: periodic check every `check_interval` (default 6h); optional daily auto-update at `auto_update_time` (default 04:30). **API:** 3 new endpoints under `/api/selfupdate/` (`status`, `check`, `update`). Auth via session cookie OR `Authorization: Bearer ` header (for external triggering from build scripts). **UI:** Settings page "Verzió és frissítés" card shows current/latest version, check time, auto-update status, last update result. "Frissítés keresése" button queries registry; "Frissítés telepítése" button appears when update is available. `pollUntilBack()` JS polls `/api/health` after triggering update and reloads when container is back up. **Notifications:** `NotifyUpdateSuccess()` and `NotifyUpdateFailed()` added to notifier for post-update startup verification results. **Alert:** Dashboard shows "Új controller verzió elérhető" info alert when update is available. **docker-compose.yml:** Added `/opt/docker/felhom-controller:/opt/docker/felhom-controller` directory bind mount (required for compose file access during self-update); named volume and read-only config override on top. **Files modified/created (12):** `internal/selfupdate/version.go` (NEW), `internal/selfupdate/state.go` (NEW), `internal/selfupdate/updater.go` (NEW), `internal/config/config.go`, `internal/notify/notifier.go`, `internal/api/router.go`, `internal/web/server.go`, `internal/web/handlers.go`, `internal/web/alerts.go`, `internal/web/templates/settings.html`, `cmd/controller/main.go`, `docker-compose.yml` ### What was just completed (2026-02-19 session 57) - **v0.15.7 — Fix backup page storage display & rename system drive label:** Backup page ("Biztonsági mentés") now shows all registered storage paths instead of only a single "Külső HDD". Added `data["StorageBars"] = s.buildStorageBars()` to `backupsHandler` (was missing unlike dashboard/monitoring handlers). Updated `backups.html` storage bars section to use `StorageBars` loop (same pattern as monitoring page), replacing the old `{{if .HDDConfigured}}` single-HDD block. Renamed system root partition label from "SSD (/)" to "Rendszer (/)" on all three pages (backup, monitoring, dashboard), as the root filesystem is not necessarily on an SSD. **Files modified (4):** `internal/web/handlers.go`, `internal/web/templates/backups.html`, `internal/web/templates/monitoring.html`, `internal/web/templates/dashboard.html` ### What was just completed (2026-02-19 session 56) - **v0.15.6 (controller) + hub v0.1.7 — Bug hunt fixes (BUGHUNT.md):** **Controller — Restore race conditions (P0-P1):** All 4 restore handlers (`restorePageHandler`, `apiRestoreStatus`, `apiRestoreAll`, `apiRestoreSkip`) now hold `restoreMu.RLock()` across nil-check and field reads. `apiRestoreAll` uses new `TryStartRestore()` method for atomic check-and-set (eliminates double-restore race). `executeAllRestores()` snapshots plan under lock, uses `SetStatus("done")` instead of direct write. Removed dead no-op goroutine. **Controller — restore_scan.go:** `dirIsEmpty()` now returns `false` on read errors (was silently treating unreadable dirs as empty, losing backup data). `Snapshot()` deep-copies Apps and Drives slices. Added `TryStartRestore()`, `SetStatus()`, `GetStatus()` helper methods. **Controller — infra_backup.go (P0):** `controller.yaml` read failure now returns a real error (was silently creating empty backup). `settings.json` and restic password read failures now logged. Added `logger *log.Logger` parameter to `BuildInfraBackup`. **Controller — main.go DR wiring:** Fixed ordering — `restoreSettingsFromHub` + settings reload now happens before `restorePasswordsFromHub` (prevents cross-drive password loss). Nil check after `ScanDrivesForBackups`. `os.MkdirAll` error now logged. `os.MkdirAll` added to `restoreSettingsFromHub` before write. **Hub — store.go (P2):** 5 `json.Unmarshal` calls now log `[WARN]` on failure. `GetInfraBackupMeta` logs unmarshal error instead of silently returning wrong counts. **docker-setup.sh (P0-P2):** DRY_RUN check moved to top of `run_config_wizard()` with dummy values (was prompting interactively even in dry-run). CF tunnel token quoted in docker-compose env. `htpasswd` uses `cut -d: -f2` + bcrypt format validation. `grep -qF` for literal path matching. Volume paths quoted in YAML output. Post-wizard validation rejects default `demo-felhom`/`homeserver.local` values. **restore.html (P2-P3):** Error text uses `textContent` instead of `innerHTML`. Poll errors counted; after 10 failures shows "Kapcsolat megszakadt" message instead of polling silently forever. **Files modified (controller, 6):** `internal/backup/restore_scan.go`, `internal/web/handler_restore.go`, `internal/report/infra_backup.go`, `cmd/controller/main.go`, `internal/web/templates/restore.html`, `scripts/docker-setup.sh` **Files modified (hub, 1):** `hub/internal/store/store.go` ### What was just completed (2026-02-19 session 55) - **v0.15.5 — Fix startup hub report silently failing:** `Push()` now returns actual errors instead of always `nil`. Previously, push failures were logged internally but the caller could never detect them, leading to a misleading `[INFO] Startup hub report sent` log even when the push actually failed (e.g., hub returning HTTP 503 during simultaneous deployment). Removed the "Never returns error to caller" behavior: marshal error returns a wrapped error, and after 3 failed retries the error is returned to the caller (the internal `[WARN]` log before `return nil` is gone). Startup hub push now retries 3 times with 15-second delays between outer attempts, giving the hub time to come up when both are deployed together. Each outer attempt uses `Push()`'s own internal 3-retry logic (5s backoff), so the hub gets up to ~40s total to become ready. If all 3 outer attempts fail, logs a clear warning with the next scheduled push interval. **Files modified (2):** `internal/report/pusher.go`, `cmd/controller/main.go` ### What was just completed (2026-02-19 session 54) - **v0.15.4 (controller) + hub v0.1.6 — Hub reporting improvements:** **Controller:** When `hub.enabled: false` but URL+API key are configured, the controller now creates the `Pusher` and sends a one-time "disabled" notification on startup (`health.status = "disabled"`, `reporting_disabled: true`). This replaces the old behavior where a disabled controller was indistinguishable from a crashed node. Added `PushOnce()` method to `Pusher` (bypasses the `enabled` flag). Added `ReportingDisabled` field to the `Report` struct. **Hub:** Added "disabled" status handling — when the latest report has `health_status = "disabled"`, the overall status is "disabled" (checked BEFORE the stale-time logic, so it stays "PAUSED" even after 30min+). Dashboard shows gray "PAUSED" badge. Customer detail shows "Reporting has been disabled on this node" with a hint to re-enable. Storage labels now shown (`label` field with fallback to `mount`). Report history timestamps now show date + time ("Feb 19 09:46" instead of "09:46:54"). New `.status-badge-disabled` CSS (neutral gray `#475569`). **Files modified (controller):** `internal/report/types.go`, `internal/report/pusher.go`, `cmd/controller/main.go` **Files modified (hub):** `hub/internal/web/server.go`, `hub/internal/web/templates/dashboard.html`, `hub/internal/web/templates/customer.html`, `hub/internal/web/templates/style.css` ### What was just completed (2026-02-19 session 53) - **v0.15.3 — Show all storage paths on dashboard + fix hub report:** Dashboard ("Vezérlőpult") and monitoring ("Rendszermonitor") pages now show usage bars for ALL registered storage paths instead of just one hardcoded "Külső HDD" bar. New `StorageBarInfo` type and `buildStorageBars()` helper build bars from `settings.GetStoragePaths()`. Each bar shows the storage label and live disk usage. Hub storage report now correctly includes all registered storage paths with proper mount paths and labels. Previously it sent only root `/` plus one HDD entry using the deprecated (empty) `cfg.Paths.HDDPath`. Now uses `system.GetDiskUsage()` per storage path, same as the dashboard bars. Added `Label` field to `StorageReport` in `types.go`. **Files modified (5):** `internal/web/handlers.go`, `internal/web/templates/dashboard.html`, `internal/web/templates/monitoring.html`, `internal/report/builder.go`, `internal/report/types.go` ### What was just completed (2026-02-19 session 52) - **v0.15.2 — Fix data loss on container restart (2 bugs):** **Bug 1:** Snapshot history delta stats (HOZZÁADOTT, ÚJ FÁJL, VÁLTOZOTT) showed 0 after container restart because restic doesn't store these stats — they were only in memory. Fixed by persisting the snapshot history ring buffer to `data/snapshot-history.json`. On startup, persisted stats are merged with restic repo snapshots. Added `saveSnapshotHistory()` (atomic write via tmp+rename), `loadSnapshotHistoryFromFile()`, updated `appendSnapshotRecord()` to save after each backup, and updated `LoadSnapshotHistory()` to merge persisted + restic data. **Bug 2:** DB validation (ÉRVÉNYESÍTÉS column) showed "–" after restart because the synthesized `LastDBDump.Results` didn't copy `Validation` from `DumpFileInfo`. One-line fix: added `Validation: f.Validation` to the synthesized `DumpResult` in `GetFullStatus()`. **Files modified:** `internal/backup/backup.go` ### What was just completed (2026-02-19 session 51) - **v0.15.1 — Backup Page "Részletek" Overhaul:** Replaced the "Tároló" section on the backup page with a new "Részletek" section containing 3 collapsible tier sections with per-drive breakdowns. **Tier 1 (Helyi mentés):** Shows per-drive restic repo stats (size, snapshot count) with storage labels. Includes aggregated totals when multiple drives exist, plus DB dump summary, integrity check, and encryption key (all carried over). **Tier 2 (Másodlagos másolat):** Groups cross-drive backup items by destination drive, separated into restic and rsync method sections with per-app sizes. **Tier 3 (Távoli mentés):** Placeholder for future B2/S3/SFTP remote backup. **Restore UI improvements:** Snapshot dropdown now groups by tier (optgroup), shows tier label + drive name per snapshot (e.g., "1. szint, hdd_1"), and marks Tier 1 as recommended. Also lists Tier 2 (secondary restic) snapshots for visibility. **Backend:** New `DriveRepoInfo` struct, `perDriveRepoStats()` method, `ListAllSnapshots()` that includes secondary restic repos, and `Tier2DriveGroup` handler struct. `SnapshotInfo` now carries `Tier` and `DriveLabel` fields. **Files modified (5):** `internal/backup/backup.go`, `internal/backup/restic.go`, `internal/web/handlers.go`, `internal/api/router.go`, `internal/web/templates/backups.html`, `internal/web/templates/style.css` ### What was just completed (2026-02-18 session 50) - **v0.15.0 — Attach Existing Drive (bind mount wizard):** New feature: Settings → "Meglévő meghajtó csatolása" wizard. Allows attaching a drive that already has a filesystem (ext4, etc.) without formatting. Solves the real-world scenario where a customer's drive contains existing data that must be preserved. **How it works:** The partition is mounted read-only at a hidden staging path (`/mnt/.felhom-raw/