## Changelog ### v0.32.7 — Fix FileBrowser config not being read on fresh deployments (2026-02-27) #### Fixed - **web/handlers.go**: `generateFileBrowserCompose()` now sets `FILEBROWSER_CONFIG=/home/filebrowser/config.yaml` environment variable — the `gtstef/filebrowser` image bakes in `FILEBROWSER_CONFIG=/home/filebrowser/data/config.yaml` which reads a stale initial config from the data volume instead of the controller-managed bind mount. This caused fresh deployments to show only a single "srv" source, ignore per-drive sidebar entries, and create the database outside the persistent volume (triggering the "new database was created" warning on every container recreation) #### Changed - **scripts/docker-setup.sh**: Initial FileBrowser compose template also includes the `FILEBROWSER_CONFIG` override for consistency ### v0.32.6 — Format empty partitions on system disk (2026-02-27) #### Added - **storage/scan.go**: New `FormatablePartition` struct and `FormatablePartitions` field on `ScanResult` — detects empty (no filesystem), unmounted, non-system partitions on system disks - **storage/scan_linux.go**: New `getSystemPartitionPaths()` resolves actual system partition device paths from fstab (more granular than `getSystemDiskNames()` which returns parent disk names). `ScanDisks()` now populates `FormatablePartitions` after enrichment - **storage/safety_linux.go**: New `IsSystemPartition()` — checks if a specific partition is a system partition (/, /boot, /boot/efi, swap) or is currently mounted; more granular than `IsSystemDisk()` which blocks the entire disk - **web/storage_handlers.go**: Scan API response now includes `formatable_partitions` array - **web/templates/storage_init.html**: Init wizard shows formatable system-disk partitions as a separate selectable section with info banner, conditional warning text, and hidden partitioning progress step #### Changed - **storage/format_linux.go**: `FormatAndMount()` now uses `IsSystemPartition()` for partition-only operations (`CreatePartition=false`) instead of `IsSystemDisk()` — allows formatting empty data partitions on the system disk while still blocking system partitions ### v0.32.5 — USB badge fix + graceful Tier2 backup on disconnected/inactive/removed destinations (2026-02-27) #### Fixed - **system/mounts_linux.go**: `IsUSBDevice()` and `diskModel()` now strip findmnt bind-mount suffix (`[/subdir]`) before parsing device path — fixes USB badge and disk model not showing for drives mounted via the attach wizard - **backup/crossdrive.go**: Disconnected source/destination drives now silently skip with WARN log instead of returning error — prevents noisy error aggregation in `RunAllScheduled()` and false "failed" counts - **web/handlers.go + backup/crossdrive.go**: Tier2 destination check now covers drives **removed** from storage (not just marked disconnected) — `IsStoragePathKnown()` detects when destination path is no longer in any registered storage, UI shows yellow "Cél meghajtó leválasztva" and scheduler skips silently - **web/handlers.go + backup/crossdrive.go**: Tier2 destination check now also covers **inactive** (Schedulable=false) drives — `IsStoragePathSchedulable()` detects when destination drive is deactivated, UI shows yellow "Cél meghajtó inaktív" and scheduler skips silently #### Added - **settings/settings.go**: New `IsStoragePathKnown(path)` method — returns whether a path belongs to any registered storage (connected, disconnected, or decommissioned); paths removed entirely return false - **settings/settings.go**: New `IsStoragePathSchedulable(path)` method — returns true only if path belongs to a registered, active (Schedulable), non-disconnected, non-decommissioned storage - **web/handlers.go**: New `Tier2DestDisconnected` and `Tier2DestInactive` fields on `AppBackupRow` — detect when Tier2 destination is disconnected/removed/inactive, sets yellow status dot instead of green/red - **web/templates/backups.html**: New template branches for disconnected ("Cél meghajtó leválasztva") and inactive ("Cél meghajtó inaktív") Tier2 destinations — grayed-out info, warning badge, no "Futtatás most" button ### v0.32.4 — Controller telemetry: include controller in hub app telemetry (2026-02-27) #### Added - **report/telemetry.go**: Include the `felhom-controller` container as a special entry in the `app_telemetry` array sent to the hub — reuses all existing hub telemetry infrastructure (memory trends, known issues, fleet aggregation) with zero hub-side changes - **report/telemetry.go**: New `buildControllerTelemetry()` function collects controller container metrics (memory, CPU) and log scan results (warnings, errors, deduplicated issues) ### v0.32.3 — Logging cleanup: consistent tags, dedup, standardized prefixes (2026-02-26) #### Fixed - **All modules**: Standardized `[LEVEL] [module]` format across every log line — added missing module tags (`[stacks]`, `[backup]`, `[cloudflare]`, `[sync]`, `[scheduler]`, `[storage]`, `[monitor]`, `[metrics]`, `[report]`, `[settings]`, `[setup]`, `[api]`, `[integrations]`, `[selfupdate]`, `[assets]`, `[web]`) - **Removed duplicate logs**: ScanStacks double completion, GetLogs INFO+DEBUG, LoadAppConfig WARN+DEBUG, copyStackDBDumps DEBUG+INFO, invalidateAllSessions INFO+DEBUG - **Standardized stale prefixes**: `[CF]`/`[CF-DEBUG]` → `[INFO/DEBUG] [cloudflare]`, `[SYNC]` → `[LEVEL] [sync]`, `[SCHED]` → `[LEVEL] [scheduler]`, `[API]` → `[LEVEL] [api]`, `[STORAGE]` → `[storage]`, `[HEALTH]` → `[monitor]`, `[ROLLBACK]`/`[ROLLBACK-ERROR]` → `[LEVEL] [storage]`, `[DEBUG-SIM]` → `(simulation)` - **Fixed wrong log levels**: Restic restore start WARN→INFO, ungated DEBUG lines in crossdrive/dbdump/onlyoffice/alerts removed or gated - **Improved vague messages**: Settings SetDisconnected/SetDecommissioned now include storage path and migration target - **Added missing logs**: `execCommand()` error, `DiscoverAppData()` completion, `BuildInfraBackup()` completion ### v0.32.2 — Comprehensive INFO/WARN/ERROR logging across all modules (2026-02-26) #### Added - **stacks/manager.go**: INFO logs for status refresh container/stack counts, log fetching, encryption migration, ScanStacks completion - **stacks/deploy.go**: INFO logs for config updates, InjectMissingFields summary; ERROR logs for SaveAppConfig failures; WARN for LoadAppConfig errors - **stacks/delete.go**: INFO log for ParseComposeHDDMounts result count - **stacks/metadata.go**: Fixed LoadMetadata error to use `log.Printf` instead of `fmt.Fprintf(os.Stderr)` - **backup/backup.go**: WARN for perDriveRepoStats failures; INFO for drive stats, aggregate stats, dump file count, snapshot history save - **backup/crossdrive.go**: INFO for cross-drive backup start/completion with success/fail counts; ERROR for rsync failures; INFO for DB dump copy counts - **backup/restic.go**: INFO for Snapshot and Check success - **backup/dbdump.go**: INFO for DiscoverDatabases count; INFO for DumpAll start/completion - **backup/restore_drives_linux.go**: INFO for fstab entry additions - **backup/local_infra.go**: INFO for backup version pruning with kept/removed counts - **cloudflare/geosync.go**: Standardized all `[GEO]` prefixed logs to `[INFO]/[WARN]/[ERROR] [cloudflare]` format - **scheduler/scheduler.go**: Standardized all `[SCHED]` prefixed logs to `[INFO]/[WARN]/[ERROR] [scheduler]` format - **sync/sync.go**: INFO for catalog sync start/completion; ERROR for git/network failures; WARN for file copy errors (replaced `[SYNC]` prefix) - **report/pusher.go**: WARN for Push and InfraBackup push failures - **report/builder.go**: INFO for BuildReport start - **monitor/healthcheck.go**: WARN for CPU/memory/disk/temperature threshold breaches; INFO for health check result status - **system/mounts_linux.go**: WARN for unsafe backup destinations and storage path probe failures - **settings/settings.go**: INFO for settings load/save, storage path add/remove, disconnect/decommission, pending events; ERROR for save failures - **storage/attach_linux.go**: INFO for disk attach start/success; ERROR for attach failures - **storage/scan_linux.go**: INFO for disk scan start/completion with count - **storage/format_linux.go**: INFO for format start/success; ERROR for format failures - **storage/migrate.go**: INFO for migration start/completion; ERROR for migration failures - **integrations/manager.go**: ERROR for integration apply failures; WARN for context build and env load failures - **integrations/lifecycle.go**: Added `[integrations]` module tag to all logs; upgraded re-apply failure from WARN to ERROR - **integrations/onlyoffice_filebrowser.go**: ERROR for all Apply/Revoke error paths - **integrations/onlyoffice_nextcloud.go**: ERROR for all Apply/Revoke error paths - **selfupdate/updater.go**: INFO for up-to-date and update-available results; INFO/ERROR for compose file updates - **selfupdate/state.go**: INFO for state cleared - **assets/syncer.go**: ERROR for manifest save failures (previously silent); changed sync failure log from WARN to ERROR - **appexport/restore.go**: INFO for import start - **web/auth.go**: INFO for logout/session invalidation/session cleanup; WARN for unauthorized API requests - **web/server.go**: WARN for 404 Not Found on unknown routes - **web/handlers.go**: INFO for default storage path and schedulable state changes - **web/handler_restore.go**: INFO for restore-all initiation - **web/handler_export.go**: ERROR for export/import start failures - **web/storage_handlers.go**: INFO for disk disconnect/reconnect/restart-apps completion - **api/router.go**: ERROR for stack action failures, backup snapshot listing failures, metrics query failures #### Changed - **stacks/healthprobe.go**: Summary log now always prints — WARN when unhealthy, INFO when all ok (was debug-only for all-ok) - **backup/restore.go**: Changed RestoreApp start log from `[WARN]` to `[INFO] [backup]` - **backup/restore_app_linux.go**: Changed restoreUserData/restoreDBDumps failure logs from `[WARN]` to `[ERROR]` where data loss could occur ### v0.32.1 — Comprehensive debug logging across all modules (2026-02-26) #### Added - **stacks/delete.go**: Debug logging for DeleteStack/RemoveStack with stack state, HDD mounts, compose output, path removal; GetStackHDDData/GetStackBackupData path scanning - **stacks/manager.go**: Debug logging for ScanStacks per-stack discovery, refreshStatusLocked container resolution, Start/Stop/Restart pre-operation state, MigrateEncryption progress, getCatalogTemplateSlugs count - **stacks/deploy.go**: Debug logging for UpdateStackConfig/UpdateOptionalConfig changed keys, InjectMissingFields per-stack checks, SaveAppConfig encryption counts, LoadAppConfig results - **stacks/healthprobe.go**: Debug logging for per-target interval calculations and target collection summary - **backup/restic.go**: `debug` field + `SetDebug()` method; debug logs for Snapshot/Prune/Check/ListSnapshots/LatestSnapshot/Stats/RestoreAppData with timing, sizes, and command details - **backup/restore_scan.go**: Debug logging for ScanDrivesForBackups drive/app scanning with per-drive availability and backup component summary - **backup/restore_app_linux.go**: Debug logging for RestoreAppFromBackup step timing, restoreUserData per-dir rsync, restoreDBDumps per-file copying - **backup/restore_drives_linux.go**: Debug logging for MountDrivesFromLayout device discovery, mount strategy selection, fstab checks - **cloudflare/geosync.go**: `debug` field + `SetDebug()` method; debug logs for Sync zone/ruleset resolution, existing/desired rule diffing, rule create/update/delete operations - **cloudflare/waf.go**: Debug logging for GetCustomRulesetID/GetRules/GetFelhomRules counts, CreateRule/UpdateRule expression snippets - **cloudflare/zone.go**: Debug logging for GetZoneID progressive domain lookup attempts - **integrations/manager.go**: `debug` field + `SetDebug()` method; debug logs for Toggle validation/timing, ListForProvider counts, buildApplyContext details, ReapplyConfigForTarget per-integration progress - **integrations/lifecycle.go**: Debug logging for OnStackStop/OnStackStart/OnStackRemove with integration counts, state checks, revoke/re-apply operations - **integrations/onlyoffice_filebrowser.go**: Debug logging for Apply/Revoke config path, JWT secret presence, office URL - **system/**: Package-level `DebugLogger` variable; debug logs for GetInfo timing/summary, readMemInfo/readDiskUsage/readLoadAvg/readTemperature raw values, CPU collector samples, GetDiskUsage/GetFSInfo/CheckBackupDestination/ProbeStoragePath/IsUSBDevice details - **monitor/pinger.go**: `debug` field + `SetDebug()` method; debug logs for Ping/Fail/Start with UUIDs, send URL/attempts/response status - **settings/settings.go**: `debug` field (json:"-") + `SetDebug()` method; debug logs for Load counts, save data size, AddStoragePath/RemoveStoragePath, SetDisconnected/SetDecommissioned, AddPendingEvent/DrainPendingEvents, SetGeoRestriction, SetIntegrationState, AutoDiscoverStoragePaths - **scheduler**: `debug` field + `SetDebug()` method; debug logs for job registration, execution timing, daily job wait calculations - **storage/**: Consistent `[DEBUG] [storage]` prefix; scan timing; drive migration debug logging - **metrics/logscanner**: Debug logging for per-container scan timing, error/warning counts - **api/router**: `debug` field + `SetDebug()` method; logs incoming API requests and handler entry points - **selfupdate**: Expanded debug coverage with `dbg()` helper for TriggerUpdate preconditions, performUpdate step transitions, docker pull timing - **assets/syncer**: Expanded debug coverage with `dbg()` helper for per-file hash comparison, download timing, manifest fetch details - **web/auth.go**: Debug logging for RequireAuth middleware decisions, login attempts (IP, success/fail), session creation/cleanup - **web/handlers.go**: Debug logging for deploy/restore/settings/storage handler entry points with key parameters - **web/handler_restore.go**: Debug logging for restore page, status polls, restore-all execution per-app timing - **web/storage_handlers.go**: Debug logging for all storage API operations (scan, init, migrate, disconnect, reconnect, attach, cleanup) - **web/server.go**: Debug logging for NewServer initialization, template loading, ServeHTTP request routing - **main.go**: Wire `SetDebug()` for settings, pinger, geoSync, integrationMgr, scheduler, apiRouter ### v0.32.0 — App export/import (.fab bundles) (2026-02-26) #### Added - **App export**: Per-app export to `.fab` bundles containing config, database dump, and all user data (HDD bind mounts or Docker named volumes) - **App import**: Restore apps from `.fab` bundles — works for both existing and new apps (standalone import page) - **Password protection**: Optional AES-256-CTR + HMAC-SHA256 encryption with scrypt key derivation for exported bundles - **Pre-export estimation**: Size estimation with free space check before starting export - **Export UI**: New export page accessible from app info header with drive picker, password field, stop-app checkbox, and real-time progress tracking - **Import UI**: Standalone import page (`/import`) scans all registered storage drives for `.fab` files, shows manifest details, and handles encrypted bundles with password prompt - **FileBrowser link**: After export, link to open the exports directory in FileBrowser - **Bundle format**: `{appname}_{timestamp}.fab` — tar.gz internally with `manifest.json`, `config/`, `database/`, `data/` directories - **New package**: `internal/appexport/` — export/import engine with provider adapter pattern (same as backup.StackDataProvider) - **API endpoints**: `/api/export/estimate`, `/api/export/start`, `/api/export/status`, `/api/export/bundles`, `/api/export/manifest`, `/api/export/import`, `/api/export/import/status` #### Changed - **backup/appdata.go**: Exported `ParseComposeNamedVolumes` (was lowercase) for reuse by appexport package ### v0.31.7 — Infra backup retention + version picker (2026-02-26) #### Changed - **docker-setup.sh hub mode**: `--hub-customer` now generates a minimal `controller.yaml` (no `customer.id`) instead of installing the full hub config — this triggers the setup wizard on first run, giving the user a choice to restore from an infra backup or start fresh - **docker-setup.sh**: Hub credentials are passed to the controller via `FELHOM_SETUP_CUSTOMER_ID` and `FELHOM_SETUP_PASSWORD` environment variables so the setup wizard auto-fills them - **Local infra backup**: `WriteLocalInfraBackup()` now rotates previous backup into `history/` subdirectory before writing new files (keeps last 5 versions per drive) - **Setup wizard scan results**: Table now shows app names/count, disk count, and "korábbi" badge for historical versions #### Added - **Setup wizard hub pre-seeding**: When deployed with `--hub-customer`, the wizard auto-detects pre-seeded credentials and auto-processes Hub API calls (no manual form entry needed) - **Hub mode welcome page**: Shows three options instead of two — "Visszaállítás a Hub-ról" (auto-connects to Hub), "Helyi mentés keresése" (local drive scan), "Friss telepítés" (fresh config download) - **Auto-process fallback**: If Hub auto-connect fails, the wizard clears the pre-seeded password and falls back to the manual form with the error displayed - **Hub backup version picker**: When multiple backup versions exist on the Hub, the setup wizard shows a version picker page (date, controller version, app names, disk count) — user selects which version to restore - **Local backup history restore**: Setup wizard can restore from historical versions found in `history/` subdirectory on local drives - **`ReadLocalInfraHistory()`**: Scans `history/` directory for all retained backup versions with rich metadata (stack names, disk count, integrity status) - **`ReadLocalInfraBackupFromHistory()`**: Reads a specific historical version by timestamp prefix - **`PullRecoveryVersion()`**: Fetches a specific backup version from the Hub recovery endpoint via `?version=ID` parameter #### Fixed - **Bind mount write**: `atomicWriteFile()` now falls back to direct write when rename fails (fixes "device or resource busy" on Docker bind-mounted `controller.yaml`) - **Drive mounting after restore**: Restore flow now calls `MountDrivesFromLayout()` to mount drives by UUID and add fstab entries — previously drives referenced in the infra backup were not mounted, causing "Adattároló nem elérhető" warnings - **Post-restore redirect**: UI now polls until the controller is actually up instead of using a fixed 5-second timeout (which was too short for container restart) - **FileBrowser DB reset scoped to restore**: `SyncFileBrowserMounts()` no longer resets the FileBrowser database volume on source changes — only the post-restore startup path (`SyncFileBrowserMountsReset`) does, preserving user accounts, permissions, and share links during normal storage operations ### v0.31.6 — UI: Brand-consistent button & card styling (2026-02-25) #### Changed - **Buttons**: Replaced traffic light colors (green/yellow/red) with brand-consistent palette — primary actions use blue gradient, secondary actions use ghost/outline, destructive actions show red tint on hover only (modal confirmations keep filled red) - **Card borders**: Running apps now show a subtle blue glow instead of green top border; all other states have neutral borders - **Status badges**: Running state badge uses brand blue instead of green - **Button alignment**: Cards use flexbox column layout with `margin-top: auto` on actions — buttons always align to the bottom regardless of card content height - **Dashboard cards**: Left border indicator changed from green to blue for running apps ### v0.31.5 — Fix Nextcloud-OnlyOffice callback URL + trusted_domains (2026-02-25) #### Fixed - **StorageUrl trailing slash**: `http://nextcloud` → `http://nextcloud/` — without trailing slash, Nextcloud's OO connector concatenates the hostname with `/apps/...` path, producing `http://nextcloudapps/...` (unresolvable hostname) - **trusted_domains**: OO Document Server callbacks arrive with `Host: nextcloud` header; added `nextcloud` to Nextcloud's trusted_domains so these internal callbacks are not rejected ### v0.31.4 — Fix FB container not restarting + OO mixed content (2026-02-25) #### Fixed - `SyncFileBrowserMounts` now uses `--force-recreate` so the container always restarts and picks up config.yaml changes (bind mounts are invisible to `docker compose up`) - OnlyOffice compose template: added Traefik `X-Forwarded-Proto=https` middleware to fix mixed content errors when OO generates `http://` URLs behind HTTPS proxy - Nextcloud integration: added `StorageUrl=http://nextcloud` for internal file download callbacks from OO Document Server ### v0.31.3 — Fix FileBrowser integration config persistence (2026-02-25) #### Fixed - FileBrowser integration config (OnlyOffice URL, JWT secret) was lost after `SyncFileBrowserMounts` regenerated `config.yaml` — the async `OnStackStart` re-apply hook failed due to timing issues - New `ReapplyConfigForTarget()` method applies integration config synchronously between config generation and container restart, ensuring it survives regen cycles ### v0.31.2 — Show FileBrowser URL on app card (2026-02-25) #### Fixed - Protected stacks (e.g. FileBrowser) now show their subdomain URL link on the app card — condition relaxed from `Deployed` to `Deployed OR Protected` ### v0.31.1 — Move integration & geo settings to deploy page (2026-02-25) #### Changed - **Integration toggles** and **geo-restriction settings** moved from app info page to deploy/settings page (user feedback: settings belong on the "Beállítások" page) - Data wiring moved from `appDetailHandler()` to `deployHandler()` in handlers.go ### v0.31.0 — App-to-App Integration Framework (2026-02-25) #### Added - **Generic integration framework** (`internal/integrations/`) — Extensible system for connecting deployed apps to each other via toggle switches on the provider's app info page - **OnlyOffice → FileBrowser integration** — Toggle enables document editing in FileBrowser by patching `config.yaml` with OnlyOffice URL and JWT secret - **OnlyOffice → Nextcloud integration** — Toggle installs and configures the OnlyOffice connector app via `occ` CLI commands - **Integration lifecycle hooks** — Integrations auto-suspend when provider or target stops, auto-re-enable when both are running again, permanently removed on app deletion - **Integration API endpoints** — `GET /api/integrations/{provider}` (list), `POST /api/integrations/{provider}/{target}` (toggle) - **Integration UI** — "Integrációk" section on app info page with toggle switches, status badges, and target availability indicators - **`IntegrationDef`** in `.felhom.yml` metadata — Apps can declare integrations with target app slug, label, and description - **`IntegrationState`** in `settings.json` — Persistent integration state with enabled/status/error tracking - **SyncFileBrowserMounts re-apply** — After config regeneration (which overwrites config.yaml), active integrations are automatically re-applied ### v0.30.7 — Monitoring: Fix Memory Legend Overflow (2026-02-25) #### Fixed - **Memory legend overflow** — Legend items in the memory distribution chart now wrap properly instead of overflowing off-screen (`flex-wrap`, `white-space: nowrap`) #### Improved - **Sort by consumption** — Memory distribution bar and legend are now sorted by memory usage (descending), largest consumers first ### v0.30.6 — Telemetry: Better Log Deduplication (2026-02-25) #### Fixed - **ANSI escape code stripping** — Log scanner now strips ANSI color codes (e.g. `\x1b[35m`) before classifying and fingerprinting lines, preventing color codes from polluting error messages and breaking deduplication - **Timezone offset in timestamps** — ISO timestamp regex now handles `+01:00`/`-0500` timezone offsets and optional trailing colons (fixes Vikunja-style log entries) - **Mid-line timestamps** — Removed `^` anchor from both ISO and syslog timestamp regexes, so timestamps embedded after log-level keywords (e.g. `ERROR 2026-02-24T21:27:05`) are now stripped correctly #### Improved - **`cleanLine()` helper** — Consolidated ANSI + timestamp stripping into a single reusable function used by both message display and fingerprint deduplication ### v0.30.5 — Health Probe: Fast Initial Checking (2026-02-25) #### Improved - **Clear stale health probes on start/restart** — `StartStack` and `RestartStack` now clear the previous `HealthProbe` result, preventing stale "unhealthy" state from being re-applied by `RefreshStatus` - **Fast 10s probing until healthy** — Stacks with no probe result (just started) or failing probes use 10-second intervals instead of waiting the full 5-minute default; reverts to normal interval once healthy - **Scheduler frequency 1m → 10s** — Health probe scheduler runs every 10 seconds (interval logic inside `RunHealthProbes` skips stacks that don't need probing, so no extra overhead for healthy stacks) ### v0.30.4 — Deep Bug Hunt II: Concurrency, Security & Optimization (2026-02-25) #### Fixed (Critical) - **Watchdog mutex panic** — Wrapped `handleDisconnect` call in anonymous func with deferred re-lock to guarantee mutex re-acquisition even on panic (C1) - **SetGeoAppOverride nil crash** — Added nil guard; passing nil override now correctly deletes the entry instead of panicking (C2) - **SSD-only app DB restore** — `restoreDBDumps` now falls back to `app.DrivePath` when `HDDPath` is empty (C3) #### Fixed (High) - **Double deploy race** — Added atomic check-and-set of `Deploying` flag with `clearDeploying()` helper on all error paths (H1) - **Delete/Remove during deploy** — Both `DeleteStack` and `RemoveStack` now reject operations while stack is deploying (H2) - **ScanStacks overwrite** — Skips updating `Deployed`/`AppConfig` for stacks with active deploy in progress (H3) - **FileBrowser mount race** — Added `fileBrowserMu` mutex to prevent concurrent `SyncFileBrowserMounts` calls (H5) - **PushEvent history gap** — Added `recordHistory` calls on both success and failure paths in PushEvent goroutine (H6) - **PushOnce silent failure** — Now returns error for non-2xx HTTP responses instead of nil (H7) - **DB dump file corruption** — Added `tmpFile.Sync()` and `tmpFile.Close()` before rename in `DumpOne` (H8) - **Restic retry timeout** — Creates fresh 30-minute context for retry after unlock instead of reusing near-expired original (H9) - **Encrypt failure silent** — Added warning log when encryption fails in `SaveAppConfig` (H10) - **Cross-backup path traversal** — Validates destination path against registered storage paths in both web and API handlers (H11) - **deepCopyStack incomplete** — Now deep-copies `Meta.OptionalConfig`, `Meta.HealthCheck`, and `DeployField.Options` (H12) #### Security - **Constant-time API key** — Replaced `==` with `subtle.ConstantTimeCompare` for API key comparison, preventing timing attacks (M1) - **Login rate limiting** — Added per-IP rate limiter (5 attempts/minute) to login handler (M8) - **Git credential masking** — Applied `maskRepoURL()` in `runGitInDir` log output to prevent credential leakage (M23) - **Path prefix traversal** — Fixed `storageAttachBrowseHandler` prefix check to require trailing `/`, preventing sibling directory matches (M24) #### Concurrency & Logic - **MigrateEncryption race** — Moved `encKey == nil` check inside the mutex lock (M5) - **SubdomainInUse I/O under lock** — Collect stack dirs under RLock, release, then perform disk I/O outside (M4) - **Scheduler late jobs** — Jobs registered after `Start()` now immediately get their goroutine launched (M10) - **SQLite WAL verification** — WAL pragma now verified via `QueryRow` + `Scan` instead of silent `Exec` (M13) - **Metrics shutdown** — `sampleContainers` now uses parent context instead of `context.Background()` for clean shutdown (M14) - **Telemetry scan logging** — Row scan errors now logged instead of silently swallowed (M15) - **Asset sync lock** — Refactored to hold mutex only for status updates, not during entire HTTP download (M22) #### Optimization - **DB dump copy** — Replaced `os.ReadFile`/`os.WriteFile` with streaming `io.Copy` via `copyFile` helper for large dumps (M16) - **Restic stats dedup** — Per-drive stats now computed once and aggregated, eliminating duplicate restic subprocess calls (M17) - **Infra config atomic** — `syncInfraConfig` controller.yaml copy now uses atomic write via `copyFile` (M20) ### v0.30.3 — Comprehensive Bug Hunt Fixes (2026-02-25) #### Fixed (Critical — P0) - **Encrypted env vars** — `UpdateStackConfig` now uses decrypted values when building compose env, preventing `ENC:...` literals in containers (C01) - **Silent decrypt failures** — `DecryptMap` now logs warnings on decrypt failure instead of silently returning empty values (C02) - **Deploy race condition** — `Deployed = false` flag now set inside the mutex lock in `runComposeDeploy` (C03) - **Shared state mutation** — `GetStack`/`GetStacks` now return deep copies preventing callers from mutating cached state (C04) - **Watchdog races** — Added per-state mutex to `pathProbeState` for thread-safe probe state access (C05) - **Metrics double-start** — `MetricsCollector.Start()` guarded with `sync.Once` (C06) - **Raw mount race** — `diskJobMu` now held across entire cleanup+mount+set operation (C07) - **Encryption key race** — Added mutex to `SetEncryptionKey` (C08) #### Fixed (High — P1) - **Restic lock detection** — `Snapshot()` now extracts stderr from `*exec.ExitError` and checks `unlockCmd.Run()` error (H01) - **Disconnected drives in backup** — `activeDrives()` now skips disconnected/decommissioned drives (H02) - **Template rendering** — Buffered via `bytes.Buffer` to prevent partial HTML on error (H07) - **Sync stop panic** — `Stop()` uses `sync.Once` for safe channel close (H08) - **Sync race** — `syncing = true` set before releasing lock in `TriggerSync` (H09) - **Cloudflare context** — Threaded `context.Context` through all Cloudflare API calls for cancellation support (H10) - **Cross-drive collision** — Replaced flawed leaf-name dedup with proper `seen` map (H15) - **CSRF bypass** — Bearer token now validated against Hub API key before skipping CSRF (H16) - **Nil pointer** — Added nil check for `crossDriveRunner` in handlers (H17) - **Selftest panic** — Replaced `out[:len(out)-1]` with `strings.TrimSpace` (H18) - **Stderr goroutine** — Added `sync.WaitGroup` in `MigrateDrive` (H19) - **UUID slice** — Guarded `uuid[:8]` with length check (H20) - **Fstab matching** — Parse fields exactly instead of loose `strings.Contains` (H21) - **Atomic save** — `SaveAppConfig` writes to `.tmp` then renames (H04) - **Deploy failure** — `SaveAppConfig` on failure now includes `encKey` (H05) - **Encryption migration** — Uses write lock instead of read lock (H03) - **Deep copy** — `GetFullStatus` deep-copies `lastDBDump`/`lastBackup` (H11) - **IPv6** — TCP health probe uses `net.JoinHostPort` for IPv6 compatibility - **Backup path validation** — `RemoveStack` validates paths under expected directory (M12) - **Updater race** — `SetBackupRunningCheck` protected by mutex (M18) #### Fixed (Medium — P2) - **Config env overrides** — `LoadFromBytes` now calls `applyEnvOverrides` (M05) - **Selfupdate state** — Compose-up failure now sets `state.Status = "failed"` (M16) - **Memory check** — `usableMB` clamped to min 0 (M22) - **Cross-backup trigger** — Removed invalid "manual" schedule from `triggerAllCrossBackups` (M23) - **mmcblk support** — Partition path and `stripPartition` now handle mmcblk devices (M21, L25) - **Scheduler** — `Start()` guarded against double-start, `Stop()` acquires mutex (M14, L24) - **Pending events** — Events restored on save failure in `DrainPendingEvents` (M03) - **Duplicate storage** — `AddStoragePath` rejects already-registered paths (M04) - **Setup scan** — `CleanupTempMounts` called after drive scan (H13) - **Setup state** — `SetStep` now logs save errors (M25) #### Fixed (Low — P3) - **UTF-8 truncation** — `TruncateStr` now operates on runes and handles negative maxLen (L05/L06) - **AllDone** — Returns false for empty restore plans (L14) - **PushOnce** — Returns actual errors instead of swallowing them (L39) - **CSRF token** — Panics on `crypto/rand.Read` failure instead of using static fallback (L40) - **Logout** — Requires POST method (L32) - **Server.Close** — Uses `sync.Once` to prevent double-close panic (L49) - **Log cap** — `lines` query parameter capped at 10000 (L31) - **Hash function** — Replaced custom `simpleHash` with `crc32.ChecksumIEEE` (L48) - **hasPrefix** — Replaced custom implementation with `strings.HasPrefix` (L13) - **DefaultEnabledEvents** — Copied in `GetNotificationPrefs` early return (L09) - **Variable shadowing** — Renamed `copy` to `cp` in `SetNotificationPrefs` (L07) #### Removed - Dead `imageName` function in selfupdate (L02) - Dead `detectHostIPViaRoute` function in setup (L03) - Custom `hasPrefix` function in restore_scan (L13) ### v0.30.2 — Report geo-restriction + logo/favicon update (2026-02-25) #### Added - **Geo-restriction in reports** (`internal/report/`) — New `GeoRestrictionReport` struct and `geo_restriction` field in the Report JSON. Hub can now display current geo-blocking status (enabled, allowed countries, per-app overrides, sync state) on customer detail pages. - **Favicon route** (`/static/favicon.svg`) — Separate favicon SVG served from synced assets or embedded fallback. Uses the cloud icon from `logo_favicon_2.svg`. - **Hub Bearer auth for geo API** — `/api/geo/` routes now accept `selfUpdateAuthMiddleware` (session auth OR Hub API key), allowing the Hub to send geo-disable commands to controllers. #### Changed - **Logo SVG updated** (`internal/web/templates.go`) — Replaced embedded logo with the latest `logo.svg` from the website (white text variant). - **Favicon link** — Layout and catch-all templates now reference `/static/favicon.svg` instead of the full logo. ### v0.30.1 — Geo-Restriction fix (2026-02-25) #### Fixed - **WAF rule creation** — Removed custom block response body from WAF rules (requires paid Cloudflare plan). Block action now uses Cloudflare's default 403 page. ### v0.30.0 — Geo-Restriction via Cloudflare WAF (2026-02-25) #### Added - **Geo-restriction feature** (`internal/cloudflare/`) — New package for managing Cloudflare WAF Custom Rules. Allows restricting access to apps by country using the `http_request_firewall_custom` phase. Rules are identified by `[felhom-geo]` description prefix — other WAF rules are untouched. - **Cloudflare API client** (`internal/cloudflare/client.go`) — HTTP client with Bearer token auth for the Cloudflare v4 API. Supports zone lookup, ruleset management, and rule CRUD operations. - **Country data** (`internal/cloudflare/countries.go`) — Embedded map of ~250 ISO 3166-1 alpha-2 country codes with Hungarian names. Includes search helpers for the UI. - **Geo sync manager** (`internal/cloudflare/geosync.go`) — Orchestrator that diffs desired vs existing Cloudflare rules and applies changes. Runs on settings change, after app deploy/remove, and every 6 hours for verification. - **Settings page UI** (`templates/settings.html`) — New "Földrajzi korlátozás" section with searchable country selector (autocomplete dropdown → tag chips), enable/disable toggle, per-app override summary, and sync status display. Hungary removal triggers a confirmation warning. - **Per-app override** (`templates/app_info.html`) — Each app's detail page now has a "Földrajzi korlátozás" section (when the feature is globally enabled) to set app-specific allowed countries. - **Geo API endpoints** (`internal/api/geo.go`) — `GET /api/geo/status`, `POST /api/geo/settings`, `POST /api/geo/sync`, `GET /api/geo/countries`, `POST/DELETE /api/stacks/{name}/geo/override`. - **Settings model** (`internal/settings/settings.go`) — New `GeoRestriction` struct with `AllowedCountries`, `AppOverrides`, and sync state (zone ID, ruleset ID, last sync). Thread-safe getter/setter methods following existing RWMutex pattern. #### Changed - **Router** (`internal/api/router.go`) — Added `OnGeoRelevantChange` callback triggered after app deploy/remove to re-sync geo rules when hostnames change. - **Main wiring** (`cmd/controller/main.go`) — Cloudflare client, geo sync manager, and scheduler job initialized when `cf_api_token` is configured. New `geoStackAdapter` provides deployed app hostnames. #### Hub Changes - **Config form** (`hub/internal/web/templates/config_form.html`) — Updated CF API token help text to indicate Zone WAF:Edit permission is needed for geo-restriction. #### Notes - The existing `cf_api_token` needs **Zone WAF:Edit** permission added (in addition to existing Zone DNS:Edit for ACME). No new token field is needed. - Local network access is inherently unaffected — local traffic bypasses Cloudflare entirely. - Cloudflare Free plan supports up to 5 custom rules, which is sufficient for a global rule + a few per-app overrides. ### v0.29.3 — Controller-side Health Probes (2026-02-25) #### Added - **HTTP/TCP health probes** (`internal/stacks/healthprobe.go`) — The controller now probes deployed apps directly over the Docker network to verify services are actually responding, not just that containers are running. Runs every minute, configurable per-app interval (default 5 min). - **Three probe types**: `http` (any response = alive), `api` (validates status code and response body), `tcp` (port reachability). Multiple checks per app supported. - **`.felhom.yml` healthcheck config** (`internal/stacks/metadata.go`) — New `healthcheck:` section with `interval`, `checks[]` (type, port, path, method, expect). Parsed from app catalog metadata. - **State override** (`internal/stacks/manager.go`) — If a running container's health probe fails, the stack state is overridden to "unhealthy". Clears automatically when probe passes again. #### Fixed - **Vikunja healthcheck** — Removed Docker-level healthcheck (distroless image has no wget/curl). Controller-side API probe to `:3456/api/v1/info` replaces it. ### v0.29.2 — Dynamic Logo & Favicon (2026-02-25) #### Changed - **Logo served from synced assets** (`internal/web/server.go`) — `serveLogoHandler` now checks the Hub-synced assets directory for `felhom-logo.svg` first, falling back to the embedded SVG constant if not found. This allows logo updates via Hub without a controller rebuild. #### Added - **SVG favicon** (`templates/layout.html`, `templates/catchall.html`) — Added `` pointing to `/static/felhom-logo.svg` so browsers display the Felhom logo as a tab icon. ### v0.29.1 — Fix Git Lock File Stale After Interrupted Sync (2026-02-24) #### Fixed - **Stale git lock file recovery** — Catalog sync now removes stale `.git/index.lock`, `.git/shallow.lock`, and `.git/HEAD.lock` files before running `git fetch`/`git reset`. Previously, if the container was killed mid-sync, the leftover lock file would block all subsequent syncs until manual intervention. ### v0.29.0 — Encrypt Sensitive Values in app.yaml (2026-02-23) #### Added - **AES-256-GCM encryption for app.yaml secrets** — Sensitive deploy field values (`type: password` and `type: secret`) are now encrypted at rest in each stack's `app.yaml` using a per-node 32-byte key. Encrypted values are stored as `ENC:base64(nonce+ciphertext)`. New `internal/crypto` package provides `Encrypt`, `Decrypt`, `LoadOrCreateKey`, `DecryptMap`, and `IsEncrypted` helpers. - **Encryption key in infra backup** — The encryption key (`encryption.key`) is included in the Hub infra backup bundle (`encryption_key_b64` field) and local drive infra backups for disaster recovery. - **Encryption key restore** — The setup wizard's infra restore flow restores `encryption.key` from the backup bundle so encrypted app.yaml values remain readable after disaster recovery. - **Startup migration** — On first start after upgrade, existing plaintext sensitive values in deployed stacks' `app.yaml` files are automatically encrypted in-place. #### Changed - **`SaveAppConfig` signature** — Now accepts `encKey []byte` and `sensitiveVars []string` parameters for encryption. All callers (deploy, update, optional config, inject missing fields, HDD path update, storage handlers) updated. - **`LoadAppConfigDecrypted`** — New helper that loads app.yaml and transparently decrypts all `ENC:` values for docker-compose env injection and web UI display. - **`SensitiveEnvVars`** — New exported helper that identifies sensitive env vars from `.felhom.yml` metadata (`type: password` or `type: secret` deploy fields). - **Manager struct** — Added `encKey` field and `SetEncryptionKey()` / `MigrateEncryption()` methods. - **Web Server struct** — Added `encKey` field and `SetEncryptionKey()` method; deploy handler decrypts values before template rendering. ### v0.28.8 — Password UX Polish (2026-02-23) #### Fixed - **Password fields empty after deployment** (`templates/deploy.html`) — Password-type deploy fields now read their stored value from `DeployedFieldValues` (app.yaml env) when viewing settings for an already-deployed app, instead of always using the field's `.Default` (which was empty). - **Post-deploy credentials masked** — Passwords on the post-deploy success card are now shown as `••••••••••••` with "Megjelenítés" (reveal) and "Másolás" (copy to clipboard) buttons, instead of displaying plaintext. #### Changed - **Settings page: initial password hint** — Deployed password fields show a note: *"Telepítéskor beállított kezdeti jelszó — ha az alkalmazásban megváltoztattad, az itt nem frissül."* Generate button is hidden for already-deployed apps. - **Post-deploy credential detection** — Added EMAIL to the username-detection heuristic (catches Kimai's `ADMIN_EMAIL`). ### v0.28.7 — Password Field UX (2026-02-23) #### Changed - **Password deploy fields: masked input with reveal & confirmation** (`templates/deploy.html`) — `type: password` fields now render as masked inputs (hidden by default) with an eye toggle button to reveal/hide. Added a "Jelszó megerősítése" confirmation field below each password input. The "Generálás" button fills both fields simultaneously. Form validation checks that both fields match before allowing deploy. Confirmation fields are only shown for new deployments. - **App catalog: admin passwords use `type: password`** (separate repo: `app-catalog-felhom.eu`) — Changed 4 apps (Nextcloud, Grafana, Kimai, Code-server) from `type: secret` to `type: password` so users can see/edit/generate admin passwords during deployment (matching the existing Paperless-ngx pattern). ### v0.28.6 — Filebrowser Link, Appdata Paths & Log Timestamps (2026-02-23) #### Added - **Post-deploy credential display** (`templates/deploy.html`) — The success page now shows actual username/password values from the deploy form instead of a generic message. Reads from deploy field metadata, filtering out internal DB passwords and secret keys. Falls back to `defaultCreds` for apps without typed deploy fields. #### Fixed - **Filebrowser "open" link on stacks page** (`web/handlers.go`) — Protected stacks like filebrowser have no `.felhom.yml` or `app.yaml`, so the subdomain lookup found nothing. Added `protectedStackSubdomains` fallback map for programmatically managed protected stacks (filebrowser → "files"). Now shows `files. ↗` link on both the stacks page and dashboard. - **App catalog: appdata volume paths** (separate repo: `app-catalog-felhom.eu`) — 4 compose templates (nextcloud, immich, paperless-ngx, romm) used `${HDD_PATH}/appdata/` instead of `${HDD_PATH}/felhom-data/appdata/` as designed in the v0.26.0+ storage structure. Fixed all templates. Existing deployments need redeployment or manual volume path update. - **Debug log viewer timestamps** (`web/logbuffer.go`, `templates/debug.html`) — Naplóviewer showed relative times like "-3586mp" (negative due to timezone bug: `time.Parse` assumed UTC but `log.LstdFlags` outputs local time). Now uses `time.ParseInLocation` with `time.Local`, and displays absolute `HH:MM:SS` timestamps. ### v0.28.5 — Post-Deploy Info Card (2026-02-23) #### Added - **Post-deploy success page** (`web/templates/deploy.html`) — After a successful deploy, instead of auto-redirecting to the apps list, shows a rich info card with: direct app link ("Alkalmazás megnyitása ↗"), first steps from catalog metadata (with DOMAIN placeholders replaced), default credentials info, documentation link, and a link to the settings page where passwords can be revealed. Also shown for unhealthy/timeout states since apps may still be usable during initialization. ### v0.28.4 — Telemetry: Skip Stopped Apps (2026-02-23) #### Fixed - **Stopped apps no longer send zero-value telemetry to hub** (`report/telemetry.go`) — Previously, deployed-but-stopped apps were included in the telemetry report with all-zero memory/CPU values, which dragged down hub-side averages. Now `buildAppTelemetry` checks `isStackRunning()` and only includes apps in running, starting, unhealthy, or restarting states. ### v0.28.3 — Catch-All Page, Deploy Controls, Dashboard Open (2026-02-23) #### Added - **Catch-all page for stopped/undeployed apps** — When a user visits a stopped app's subdomain (e.g., `travel.demo-felhom.eu`), they now see a branded felhom page with the app name and status ("Az alkalmazás jelenleg le van állítva") instead of Traefik's raw 404. Implemented via a low-priority (1) Traefik catch-all router on the controller container + `CatchAllMiddleware` in `server.go` that intercepts non-controller hosts and renders standalone `catchall.html` without auth. - **Start/Stop/Restart buttons on deploy settings page** — Deployed apps now show Indítás/Leállítás/Újraindítás buttons in the page header, plus a "Megnyitás ↗" link to the app's subdomain (visible when running). Previously the deploy page had no state controls. - **"Megnyitás ↗" button on Vezérlőpult** — Running apps on the dashboard now show an open button that launches the app in a new tab. Uses the `Subdomains` map built from `app.yaml` SUBDOMAIN env with metadata fallback. - **`findStackBySubdomain()`** helper in `server.go` — looks up stacks by subdomain, checking deployed `app.yaml` env first, then `.felhom.yml` metadata. #### Changed - **Subdomain links on Alkalmazások page** — Links now only shown for deployed apps (previously shown for all apps including non-deployed ones where the subdomain isn't final yet). - **`docker-compose.yml`** — Added 6 catch-all Traefik router labels (`traefik.http.routers.catchall.*`) with `priority=1` and `certresolver=letsencrypt`. ### v0.28.2 — Async Deploy & AdventureLog Fix (2026-02-23) #### Changed - **Async deploy** — `DeployStack()` now runs `docker compose up -d` in a background goroutine instead of blocking the HTTP response. The deploy API returns immediately after validation + config save, so the UI switches to the progress panel instantly (previously waited 30-60s for image pulls). New `StateDeploying` container state shown while compose-up is in progress. On failure, the goroutine reverts both disk and in-memory state and stores the error in `DeployError` for the polling UI to display. - **Deploy progress UI** — Polling now handles the `deploying` state ("Képek letöltése, konténerek indítása...") and `deploy_error` (shows error message with links to logs). Previous behavior only showed progress after compose-up completed. #### Fixed - **RestartStack uses `up -d` with env vars** — `RestartStack()` previously used bare `docker compose restart` which only sends SIGTERM+start without re-reading the compose file or injecting env vars from `app.yaml`. Now uses `docker compose up -d` with full env, matching `StartStack()` behavior. This ensures template changes (images, healthchecks) and env var updates are picked up on restart. - **AdventureLog backend healthcheck** — Replaced `wget` (not available in v0.11.0 image) with `python urllib.request`. Also uses `127.0.0.1` instead of `localhost` to avoid IPv6 resolution issues. - **AdventureLog frontend healthcheck** — Changed `localhost` → `127.0.0.1` to fix IPv6 resolution causing connection refused (Node.js only listens on IPv4). - **AdventureLog SECRET_KEY** — Added `SECRET_KEY=${SECRET_KEY}` env var alongside `DJANGO_SECRET_KEY` for v0.11.0 compatibility (Django settings now reads `SECRET_KEY` directly). ### v0.28.1 — Telemetry Debug Section (2026-02-23) #### Added - **Telemetria teszt section on Debug page** — New collapsible section between "Hub & Kapcsolatok" and "Önfrissítés teszt". Click "Telemetria futtatása" to run the full telemetry collection pipeline on-demand without waiting for the 15-minute report cycle. - **`GET /api/debug/telemetry`** — New debug endpoint in `handler_debug.go`. Invokes `GetTelemetryPreview` callback, returns per-app data: container list, memory (current/avg/peak), CPU avg, catalog limit, log error/warning counts, top issues, and overall latency. Response: `{latency_ms, app_count, total_errors, total_warnings, app_telemetry[]}`. - **`GetTelemetryPreview` callback** added to `DebugCallbacks` struct. Wired in `main.go` debug-mode block: calls `report.BuildAppTelemetryForDebug(stackMgr, metricsStore, logger)`. Available regardless of hub configuration. - **`report.BuildAppTelemetryForDebug()`** — Exported wrapper in `internal/report/telemetry.go` around the private `buildAppTelemetrySection()`. Allows debug endpoint access without exposing internal package details. - **JS rendering** — `runTelemetryTest()` fetches the endpoint and shows a summary message. `renderTelemetryDetail()` builds a table with per-app rows (color-coded errors in red, warnings in yellow) and sub-rows for top issues. Includes a collapsible "Nyers JSON" section showing the exact payload that would go to the hub. ### v0.28.0 — App Telemetry & Analytics (2026-02-23) #### Added - **App telemetry in Hub reports** — `Report.AppTelemetry` (new field in `report/types.go`) carries per-stack memory/CPU metrics and log scan results to the Hub on every report push. Backward-compatible: old Hub versions silently ignore the new field. - **`internal/metrics/telemetry.go`** — New `MetricsStore.GetContainerTelemetry(since)` method aggregates container memory (current/avg/peak) and CPU averages from the existing `container_metrics` SQLite table over the last 15 minutes. - **`internal/metrics/logscanner.go`** — New `ScanContainerLogs(containerNames, since, logger)` function runs `docker logs --since=15m --tail=1000` on each non-protected deployed container. Detects errors/warnings by keyword matching, deduplicates via fingerprinting (strips timestamps, replaces 6+ digit numbers with ``, hex with ``, UUIDs with ``). Returns `[]ContainerLogSummary` with counts and `RecentIssues` (top 10 per container). - **`internal/report/telemetry.go`** — New `buildAppTelemetrySection()` and `buildAppTelemetry()` functions assemble per-stack `AppTelemetry` records by aggregating container-level metrics and log summaries. Only non-protected, deployed stacks are included. #### Changed - **`internal/report/builder.go`** — `BuildReport()` now calls `buildAppTelemetrySection()` after the stacks section, populating `r.AppTelemetry`. - **`internal/report/types.go`** — Added `AppTelemetry []AppTelemetry` field to `Report` struct. Added new `AppTelemetry` type with fields: app_name, display_name, containers, memory metrics, catalog estimate/limit, log error/warning counts, and top issues. ### v0.27.3 — Real System Memory Everywhere (2026-02-23) #### Changed - **Deploy page uses real system memory** — Memory bar now shows actual `/proc/meminfo` usage instead of declared `mem_request` sums. Labels changed from "Jelenlegi foglalás" to "Jelenlegi használat". `system.GetMemoryMB()` provides real-time total and used memory. - **Pre-start memory check uses real memory** — `actionStack("start")` in `router.go` and `DeployStack()` in `deploy.go` now check real used memory (`usedMB + newReqMB > usableMB`) instead of declared committed sums. `CommittedMemory()` kept only for soft overcommit warnings. #### Added - **`system.GetMemoryMB()` helper** — Lightweight function in `internal/system/info_linux.go` that returns real total and used memory from `/proc/meminfo` without the overhead of full `GetInfo()` (no disk/CPU/temp). Stub in `info_other.go` for non-Linux. - **Monitoring page memory distribution bar** — New stacked bar on `/monitoring` showing per-container memory usage (colored segments), OS/system overhead (gray), and free memory. Built dynamically from container summary data + real-time `/api/system/info`. Color-coded legend with per-app labels. ### v0.27.2 — Comprehensive Fixes and New Labels (2026-02-23) #### Fixed - **Deploy error popups now copyable** — Replaced all native `alert()` calls with a custom modal (`showAlert()` in layout.html) using a `
` block with `user-select:text`. Error messages can now be selected and copied. Applied across deploy.html and layout.html.
- **Manual Tier2 backup now reports to Hub** — Added `OnCrossDriveComplete` callback to `Router` (`internal/api/router.go`). Both `triggerCrossBackup` (single-app) and `triggerAllCrossBackups` (run-all) now call `pushInfraBackup()` + `writeLocalInfraBackup()` after completion, matching the automatic scheduled path.
- **Memory bar excludes stopped apps** — `CommittedMemory()` in `internal/stacks/manager.go` now skips apps with `StateStopped` or `StateExited`. Only running/starting/unhealthy apps count toward committed memory.
- **Pre-start memory check** — `actionStack("start")` in `internal/api/router.go` now validates available memory before starting a stopped app. Returns 409 Conflict with a descriptive Hungarian error if insufficient.

#### Added
- **`hungarian_ui` metadata field** — New `HungarianUI bool` field in `ResourceHints` (`internal/stacks/metadata.go`). Shows "Magyar felület" green badge on deploy, stacks, and app info pages when `hungarian_ui: true` in `.felhom.yml`.
- **USB badge on storage cards** — Settings page storage cards now show an orange "USB" badge next to Aktív/Alapértelmezett when the drive is USB-attached (using existing `IsUSB` sysfs detection).
- **`StackMemoryMB()` helper** — New method on `Manager` to get a specific stack's memory request.

#### App Catalog (app-catalog-felhom.eu)
- **AdventureLog** — Fixed image tags from `v0.12.0` (non-existent) to `v0.11.0` for both backend and frontend.

### v0.27.1 — Fix FileBrowser Mount Sync (2026-02-22)

#### Fixed
- **`internal/web/handlers.go`** — `SyncFileBrowserMounts()` was reading the domain from a `.env` file that doesn't exist in the filebrowser stack directory (domain is baked into the compose labels by `docker-setup.sh`). It always logged `[WARN] Cannot read DOMAIN from FileBrowser .env — skipping mount sync` and returned early, so storage paths were never synced to FileBrowser's config.yaml or docker-compose.yml. Fixed by using `s.cfg.Customer.Domain` directly from the controller config.

### v0.27.0 — User-Configurable App Subdomains (2026-02-22)

#### Added
- **User-configurable subdomains**: Users can now customize the subdomain (e.g., `wiki`, `cloud`, `my-notes`) for each app during deployment, instead of using a fixed value. The deploy page shows an editable text input with the default subdomain pre-filled and the base domain as a suffix (e.g., `[wiki] .demo-felhom.eu`).
- **New deploy field type `"subdomain"`** — `internal/stacks/metadata.go`, `deploy.go`: A new field type that is user-editable with a default value, validated, and locked after deployment. Changing the subdomain requires removing the app (clean install) and redeploying.
- **Subdomain validation** — `internal/stacks/deploy.go`: Three-layer validation: DNS-safe format (lowercase alphanumeric + hyphens, max 63 chars), reserved name blocklist (`felhom`, `files`, `traefik`, `api`, `www`, `mail`, `admin`, etc.), and uniqueness check across all deployed stacks.
- **Backward compatibility** — `internal/stacks/deploy.go`: `InjectMissingFields()` auto-fills `SUBDOMAIN` from the `.felhom.yml` default for existing deployed apps when templates are synced, so no manual intervention is needed.
- **`internal/web/handlers.go`** — `stacksHandler()` builds an effective subdomain lookup map (stored env → metadata fallback). `appDetailHandler()` passes `EffectiveSubdomain` to templates.
- **`internal/web/templates/deploy.html`** — New `.subdomain-input-group` widget with inline `.domain` suffix. Client-side validation enforces DNS-safe format with real-time lowercasing.
- **`internal/web/templates/stacks.html`**, **`app_info.html`** — Subdomain links now read from stored `app.yaml` env (via lookup map) instead of hardcoded metadata, showing the user's actual chosen subdomain.

#### Changed
- **`internal/stacks/deploy.go`** — `PreviewDeployValues()` domain case simplified: shows just the base domain now (subdomain is a separate field).
- **`internal/web/handlers.go`** — Deploy page domain auto-field no longer prepends `meta.Subdomain + "."`. Passes `DeployedFieldValues` for rendering stored subdomain on settings page.

#### App Catalog (app-catalog-felhom.eu)
- All 51 template `docker-compose.yml` files updated: hardcoded `{subdomain}.${DOMAIN}` replaced with `${SUBDOMAIN}.${DOMAIN}` in Traefik labels, app env vars (APP_URL, trusted domains, webhook URLs, etc.), and comments.
- All 51 `.felhom.yml` files updated: added `SUBDOMAIN` deploy field with `type: subdomain` and `default:` matching the existing `subdomain:` metadata value.

### v0.26.2 — Show Full App URL on Deploy Page (2026-02-22)

#### Fixed
- **`internal/stacks/deploy.go`** — `PreviewDeployValues()` now shows the full reachable URL (`subdomain.base_domain`) for domain-type fields instead of just the base domain. Informational only — stored env var remains the base domain.
- **`internal/web/handlers.go`** — Same fix applied to the already-deployed settings page: domain field displays `subdomain.base_domain` matching what the app card shows.

### v0.26.1 — Show Auto-Generated Values on Deploy Page (2026-02-22)

#### Changed
- **`internal/stacks/deploy.go`** — Added `PreviewDeployValues()` method: pre-generates domain and secret field values when the deploy page is loaded, so the user can see (and note down) exact values before deploying. Updated `DeployStack()` to accept pre-generated secret values from the form instead of always regenerating.
- **`internal/web/handlers.go`** — `deployHandler` now calls `PreviewDeployValues()` for non-deployed apps and populates `AutoFieldValues` (previously empty for pre-deploy).
- **`internal/web/templates/deploy.html`** — "Automatikusan generált értékek" section now shows actual values on the pre-deploy page too: domain as a readonly text input, secrets as readonly password inputs with a "Megjelenítés" reveal button. Updated section description to inform the user to note down passwords. Pre-generated secret values are submitted as hidden inputs so the same values shown to the user are saved to `app.yaml`.

### scripts — Hub Mode + FileBrowser Controller-Managed Volumes (2026-02-22)

#### `scripts/docker-setup.sh` — v6.0.0
- **Hub mode** (`--hub-customer` / `--hub-password`): downloads `controller.yaml` from Hub API early in setup, extracts `domain`, `email`, `cf_api_token`, `cf_tunnel_token` and auto-populates all infrastructure settings. Single one-liner deploys fully configured Traefik + TLS + Cloudflare Tunnel with no additional flags needed. CLI flags always override hub values.
- **`yaml_get()` helper**: strips leading whitespace before key comparison — required because Go's `yaml.v3` uses 4-space indentation.
- **`apply_hub_config()`**: called before `print_banner` in `main()` so hub-sourced values are reflected in the plan display.
- **FileBrowser initial install**: removed drive auto-discovery from `install_filebrowser()`. FileBrowser is now installed with no drive volumes and a minimal `config.yaml` with `/srv` fallback. Drive volumes are managed entirely by the controller (`SyncFileBrowserMounts()`) after storage is registered via the dashboard.
- **Bug fix**: `((found_mounts++))` → `found_mounts=$(( found_mounts + 1 ))` — `set -euo pipefail` traps post-increment when var=0 (exit code 1). Same fix applied to `step_num` in `install_filebrowser()`.

#### `scripts/felhom-wipe.sh`
- **`cleanup_scan_dir()`**: removes `/mnt/.felhom-scan/` (ephemeral DR scan directory) — called from `full` level onwards.
- **`cleanup_raw_mounts()`**: removes raw helper mount infrastructure (`/mnt/.felhom-raw/`) at `nuclear` level: unmounts bind mounts first, then raw mounts, strips fstab entries, removes empty directories. Physical drive data untouched.
- **Bug fix**: `do_soft_wipe()` used `[ -f "$f" ] && rm -f "$f" && info "..."` — with `set -euo pipefail`, when a state file doesn't exist `[ -f ]` returns 1, the whole `&&` chain returns 1, and `set -e` exits the script. Nuclear wipe was silently stopping after removing only the first two state files that existed. Fixed with `if [ -f "$f" ]; then ...; fi`.

#### `scripts/README.md`
- Hub mode quick start simplified to one-liner
- Updated installation steps table: step 7 reflects controller-managed FileBrowser volumes
- Added "Raw helper mounts" section explaining two-level mount architecture
- Updated wipe levels table for `full` (scan dir) and `nuclear` (raw mounts + scan dir)

### v0.26.0 — Storage Namespace `felhom-data/` + Test Node Wipe Script (2026-02-22)

All felhom-managed data on external drives now lives under a `felhom-data/` subdirectory, cleanly separating controller-managed data from user files. Plus a multi-level wipe script for repeatable test node cleanup.

**Key design principle:** `HDD_PATH` env var stays as the mount point (e.g., `/mnt/hdd_1`). The `felhom-data` segment is embedded in path helpers and compose templates — not in `HDD_PATH`.

#### Changed
- **`internal/backup/paths.go`** — Added `FelhomDataDir = "felhom-data"` constant. Updated 8 path functions to insert `felhom-data` between the drive root and data subdirectory:
  - `PrimaryBackupPath` → `/felhom-data/backups/primary`
  - `PrimaryResticRepoPath` → `/felhom-data/backups/primary/restic`
  - `AppDBDumpPath` → `/felhom-data/backups/primary//db-dumps`
  - `SecondaryBackupPath` → `/felhom-data/backups/secondary`
  - `AppSecondaryRsyncPath` → `/felhom-data/backups/secondary//rsync`
  - `SecondaryResticRepoPath` → `/felhom-data/backups/secondary/restic`
  - `SecondaryInfraPath` → `/felhom-data/backups/secondary/_infra`
  - `AppDataDir` → `/felhom-data/appdata/`
  - `InfraBackupDir` **unchanged** — stays at drive root for DR scanner
- **`internal/stacks/delete.go`** — Added local `felhomDataDir = "felhom-data"` constant (cannot import `backup` due to architectural boundary). Updated `ProtectedHDDPaths()` to protect `/felhom-data`, `/felhom-data/appdata`, `/felhom-data/backups`. Fixed hardcoded paths in `GetStackBackupData()`.
- **`internal/storage/migrate_drive.go`** — Added `backup` package import. Fixed 4 issues:
  - Conflict check: uses `backup.AppDataDir()` instead of hardcoded `appdata/`
  - Verify step: uses `backup.AppDataDir()` instead of hardcoded `appdata/`
  - rsync excludes: updated from `backups/primary/restic/` to `felhom-data/backups/primary/restic/`
  - Size estimation: now scans inside `felhom-data/` namespace, skipping restic repos correctly
- **`internal/storage/migrate.go`** — Added `backup` package import. Post-migration DB dump copy now uses `backup.AppDBDumpPath()` instead of hardcoded paths.
- **`internal/web/handlers.go`** — Fixed legacy `"storage"` path in storage app detail size calculation (was dead code — path never existed); now uses `backup.AppDataDir()`.
- **`internal/storage/format_linux.go`** — Format wizard creates `felhom-data/` subdirectory instead of legacy `storage/`.
- **`internal/storage/attach_linux.go`** — Attach wizard creates `felhom-data/` subdirectory instead of legacy `storage/`.

#### Added
- **`scripts/felhom-wipe.sh`** — Test node cleanup script with 4 wipe levels:
  - `soft` — Removes controller state files (settings.json, metrics.db, session/setup/update/snapshot state)
  - `controller` — Soft + removes all app containers, volumes, and stack directories (skips protected stacks by default)
  - `full` — `controller`-level cleanup + removes `felhom-data/` on all storage drives (also removes old-style `appdata/` and `backups/` for migration compatibility); infra containers preserved, controller restarted after cleanup
  - `nuclear` — Full + removes controller.yaml, all infrastructure containers (controller, traefik, cloudflared, portainer), DR markers, and runs `docker system prune -af --volumes`
  - Auto-detects paths from `controller.yaml` and `settings.json`
  - Dry-run by default; requires `--yes` to execute
  - Interactive confirmation prompt with `--yes` execution

#### Notes
- **Migration**: Pre-v0.26.0 restic snapshots reference old paths (without `felhom-data/`). Existing installations need data migration before upgrading.
- **App catalog**: Compose templates need separate update: `${HDD_PATH}/appdata/` → `${HDD_PATH}/felhom-data/appdata/` (tracked as separate task).
- All backup, crossdrive, and restore logic automatically picks up new paths via `paths.go` helpers — no changes needed in `backup.go`, `crossdrive.go`, or `restore.go`.

---

### v0.25.0 — Debug Page: Operator Testing & Diagnostics Dashboard (2026-02-21)

**Full debug dashboard with 8 sections for testing all controller subsystems in debug mode.**

Only available when `logging.level: "debug"` — sidebar link, page, and all `/api/debug/*` endpoints return 404 otherwise.

#### New files
- `internal/web/logbuffer.go` — Ring buffer (1000 entries) implementing `io.Writer` for capturing log output. Parses Go standard log format (with/without `Lshortfile`), extracts level/source/timestamp. Supports filtered retrieval by level and timestamp.
- `internal/web/handler_debug.go` — Debug page handler + 20 API endpoint handlers organized in 8 sections. `DebugCallbacks` struct (6 fields) for wiring main.go closures.
- `internal/web/templates/debug.html` — Full debug dashboard template with 8 collapsible sections, complete JS framework (lazy-load, polling, action buttons, log viewer with filter/auto-refresh).

#### Debug page sections
1. **Rendszer diagnosztika** — Diagnostic dump (migrated from `api/router.go`) with structured UI rendering: controller info, storage paths, deployed stacks, scheduler jobs, alerts. JSON download button.
2. **Értesítés teszt** — Send test events with configurable type/severity, view event history ring buffer (last 50 events, newest first).
3. **Mentés teszt** — Trigger individual backup phases: full backup, DB dump only, cross-drive only, restic integrity check, infrastructure backup.
4. **Tárhely teszt** — Storage watchdog status table with per-path probe state. Simulate disconnect (stops apps, marks disconnected, skips unmount) and reconnect (cleans locks, clears state). 5s auto-refresh.
5. **Hub & Kapcsolatok** — Hub report push, infra backup push, Hub/Gitea connectivity tests with latency, preference sync.
6. **Önfrissítés teszt** — Version check + dry-run (shows current/new image lines, compose writability, backup status).
7. **DR / Telepítő varázsló** — Infra backup status per drive (files, timestamps). "RESET" confirmation + infra backup pre-check before triggering setup mode via marker file.
8. **Naplóviewer** — In-memory log viewer with level filter (DEBUG/INFO/WARN/ERROR), 2s auto-refresh, color-coded entries, clear display.

#### Module additions
- `notify/notifier.go`: `PushTestEventSync()` (synchronous, returns Hub status), `GetEventHistory()` (ring buffer), `recordHistory()` for debug page.
- `backup/crossdrive.go`: `RunAllConfigured()` — runs all enabled apps ignoring schedule filter.
- `selfupdate/updater.go`: `DryRun()` — checks update availability, compose writability, backup status without performing changes.
- `monitor/watchdog.go`: `SimulateDisconnect()` / `SimulateReconnect()` with `simulatedPaths` map, `GetDebugStatus()` for per-path probe state. Watchdog `Check()` skips simulated paths.
- `setup/setup.go`: `NeedsSetup()` now checks `.needs-setup` marker file. `ClearSetupMarker()` for cleanup.

#### Routing changes
- **Mux carve-out**: `/api/debug/` routes to web server (same pattern as `/api/storage/`), with auth + CSRF.
- **Removed** `SetDebugDumpDeps()` from `api/router.go` and the `/api/debug/dump` route — dump handler migrated to `handler_debug.go` using Server's existing fields.

#### Infrastructure
- `setupLogger()` now returns `(*log.Logger, *web.LogBuffer)`. In debug mode, creates `io.MultiWriter(os.Stdout, logBuffer)` so all log output is captured from the start.
- Debug CSS: ~170 lines of styles for sections, result badges, log viewer, confirm input, danger button, spinner.

### v0.24.0 — Pre-Testing Observability (2026-02-21)

**Three features for pre-testing diagnostics: verbose debug logging, diagnostic dump endpoint, and startup self-test.**

#### Feature 1: Debug logging across all modules

All `[DEBUG]` log lines are gated behind `logging.level: "debug"` — zero overhead at `info` level.

- **New** `internal/util/strings.go`: shared `TruncateStr()` for safely truncating command output in logs.
- **Backup** (`backup.go`, `dbdump.go`, `crossdrive.go`, `restore.go`, `local_infra.go`): added `isDebug()` method and per-operation debug logging. DB dump logs container discovery, per-dump command details (passwords masked as `***`), validation results. Cross-drive logs source/dest paths, rsync results, auto-enable decisions. Restore logs step-by-step progress.
- **Storage** (`scan_linux.go`, `format_linux.go`, `attach_linux.go`, `migrate.go`): added `Logger`/`Debug` fields to request structs. Logs raw lsblk output (truncated), per-disk classification, pipeline steps for format/attach, rsync progress for migrate. Updated `*_other.go` stubs.
- **Sync** (`sync.go`): logs masked clone URLs, per-file hash comparison, post-sync hook triggers.
- **Self-update** (`updater.go`): logs registry API calls, tag parsing, version comparison, compose file edits.
- **Monitor** (`watchdog.go`): smart logging — periodic 60-probe summaries (~5 min), immediate log on unexpected failures, reconnect attempt details. (`healthcheck.go`): logs raw check values and per-check results.
- **Notify** (`notifier.go`): logs event push URL/type/response, preference sync details.
- **Report** (`pusher.go`, `builder.go`): logs payload sizes, section summaries, push responses.
- **Assets** (`syncer.go`): logs manifest fetch, per-file hash comparison, download/removal actions.
- **Setup** (`scanner.go`, `handlers.go`): logs drive scan details, hub recovery/config write operations.

#### Feature 2: Diagnostic dump endpoint (`GET /api/debug/dump`)

Returns a comprehensive JSON snapshot of all controller state. Only available when `logging.level: "debug"` — returns 404 otherwise.

- Sections: `controller` (version, uptime, config hash, PID), `storage` (per-path usage), `stacks` (deployed/running/stopped counts + list), `backup` (status, repo stats), `hub` (push status, consecutive failures), `scheduler` (all jobs with last_run/running/errors), `health` (fresh check), `notifications`, `self_update`, `alerts`.
- API router expanded with `SetDebugDumpDeps()` setter for scheduler, hub pusher, alert manager, version, and start time.

#### Feature 3: Startup self-test

- **New** `internal/selftest/selftest.go`: runs 9 diagnostic checks on boot with 5s timeout each.
- Checks: Docker socket, stacks directory, data directory (write test), system data path (mount point), storage paths (connected vs disconnected), git catalog (.felhom.yml files), Hub connectivity (/healthz), restic repos, metrics DB.
- Results logged in a clear block: `[PASS]`/`[WARN]`/`[FAIL]` per check, summary at end.
- Self-test summary (pass/warn/fail counts) sent to Hub via `NotifyControllerStarted` details map.
- Never blocks startup — purely diagnostic.

#### Constructor/signature changes

- `notify.New()`: added `debug bool` param. `NotifyControllerStarted()`: added `details map[string]interface{}` param.
- `report.NewPusher()`: added `debug bool` param. `BuildReport()`: added `logger *log.Logger` param.
- `monitor.RunHealthCheck()`: added `logger *log.Logger` param (5 call sites in main.go).
- `selfupdate.NewUpdater()`: added `debug bool` param.
- `assets.New()`: added `debug bool` param.
- `backup.NewCrossDriveRunner()`: added `debug bool` param. `WriteLocalInfraBackup()`: added `debug bool` param.
- `backup.DiscoverDatabases()`, `DumpOne()`: added `debug bool` param.
- `storage.ScanDisks()`: added `logger, debug` params. `FormatRequest`, `AttachRequest`, `MigrateRequest`: added `Logger`/`Debug` fields.
- `setup.ScanDrivesForInfraBackups()`: added `debug bool` param.

### v0.23.0 — CSRF Protection (2026-02-21)

**CSRF (Cross-Site Request Forgery) protection on all browser-facing POST endpoints — controller and hub.**

**Controller changes:**

- New `internal/web/csrf.go`: `CsrfProtect` HTTP middleware validates CSRF tokens on all state-mutating requests (POST/DELETE/PATCH).
  - Reads token from `_csrf` form field or `X-CSRF-Token` request header.
  - Exempt paths: `Authorization: Bearer` requests (selfupdate, config/apply hub→controller calls) — browsers cannot auto-send Bearer headers, so no CSRF risk.
  - Auth-disabled mode (no password set): CSRF check is skipped entirely.
  - On rejection: JSON error for `/api/` paths, HTTP 403 text for page routes.
- `internal/web/auth.go`: `session` struct gains a `csrfToken string` field. `createSession()` generates a second 32-byte random CSRF token alongside the session token. New `csrfTokenForSession(sessionToken)` method returns the CSRF token for a given session.
- `internal/web/server.go`: New `executeTemplate(w, r, name, data)` wrapper auto-injects `CSRFField` (`template.HTML` hidden input) and `CSRFToken` (raw string) into every page render data map.
- `cmd/controller/main.go`: All route registrations wrapped with `webServer.CsrfProtect(...)` middleware. Version bumped to `v0.23.0`.
- All handlers (`handlers.go`, `storage_handlers.go`, `handler_restore.go`): Switched from `s.render(w, ...)` to `s.executeTemplate(w, r, ...)`.
- All templates updated:
  - `layout.html`: Added `` and inline `csrfHeaders()` JS helper (returns `{'X-CSRF-Token': ...}`) in `` (before page-specific scripts). Updated 4 fetch POST/DELETE calls.
  - `settings.html`: Added `{{$.CSRFField}}` to 5 forms inside `{{range .StoragePaths}}` (must use `$` for outer scope inside range). Added `{{.CSRFField}}` to 3 page-level forms. Inline-label form uses `document.querySelector('meta[name="csrf-token"]').content`. Updated 5 fetch calls.
  - `deploy.html`: Added `{{.CSRFField}}` to cross-backup form. Updated 3 fetch calls.
  - `backups.html`: Updated 3 fetch calls. Dynamically-created restore form injects `_csrf` from meta tag.
  - `storage_init.html`, `storage_attach.html`, `migrate.html`, `migrate_drive.html`, `app_info.html`, `restore.html`: All fetch calls updated.
  - `storage_attach.html`: Replaced `navigator.sendBeacon()` with `fetch(..., {keepalive: true})` — `sendBeacon` cannot send custom headers, making CSRF impossible.

**Hub changes (v0.3.8):**

- `internal/web/server.go`: Replaced insecure literal `hub_session=authenticated` cookie with proper server-side session map.
  - New `hubSession` struct with `csrfToken string` and `expiresAt time.Time`.
  - `sessions map[string]*hubSession` + `sessionsMu sync.RWMutex` on `Server` struct.
  - `handleLogin`: Generates cryptographically random 64-char hex session token + 64-char hex CSRF token. Cookie gains `SameSite=Lax` and `Secure` (when TLS) attributes. Session expires after 7 days.
  - `RequireAuth`: Validates session token against map (constant-time compare), redirects to `/login` on failure.
  - `CleanupSessions(ctx)`: Goroutine that purges expired sessions every hour.
  - CSRF validation block at top of `ServeHTTP`: checks `X-CSRF-Token` header or `_csrf` form field on POST/DELETE/PATCH. Skips when no session cookie (Basic Auth / API path).
  - `csrfToken(r)`, `csrfField(r)` helpers for template data injection.
- `internal/web/configs.go`: Added `html/template` import. All template render calls pass `CSRFField template.HTML` and/or `CSRFToken string`. `renderConfigForm` gains `r *http.Request` parameter.
- Templates updated:
  - `config_form.html`: Added `{{.CSRFField}}` inside the `
`. - `customer_unified.html`: Added `` + inline `csrfHeaders()` in ``. Added `{{.CSRFField}}` to all 5 POST forms (unblock, block, delete config, create-config, regen-password). Updated 3 JS fetch POST calls (trigger-update, push-config, pull-config). - `cmd/hub/main.go`: Started `go webServer.CleanupSessions(ctx)` goroutine. ### v0.22.3 — Hub Asset Sync (2026-02-21) **Hub-managed asset downloads** - New `internal/assets` package: downloads and caches app assets (logos, screenshots) from the Hub API with SHA-256 change detection. - Asset syncer resolves files from downloaded cache first, falls back to baked-in `/usr/share/felhom/assets/` directory. - Config: `assets.sync_enabled: true` + `assets.sync_schedule: "05:00"` to enable daily sync. - API: `POST /api/assets/sync` triggers on-demand sync, `GET /api/assets/status` returns sync status. - Web server's `serveAsset()` now routes through syncer's `Resolve()` when available. ### v0.22.2 — Setup Logo Fix (2026-02-21) - **Fix setup wizard logo**: Logo failed to load because `handleLogo()` tried to read it as a file from the filesystem, but it only exists as an embedded string constant. Now imports and serves `web.FelhomLogoSVG` directly. ### v0.22.1 — Setup Wizard Bugfixes (2026-02-21) - **Fix setup mode detection**: Remove `demo-felhom` from `NeedsSetup()` check — only empty `customer.id` triggers setup mode. Previously the demo customer was stuck in setup mode. - **Fix CSRF nil pointer panic**: `renderError()` was passing `nil` instead of `*http.Request` to `ensureCSRFToken()`, causing panic when rendering error pages. - **Fix double-v version display**: Welcome page showed "vv0.22.0" — removed redundant `v` prefix from template. - **Fix IP detection in Docker**: Setup wizard showed container bridge IP (172.x) instead of host LAN IP. Now reads `HOST_IP` env var (set by docker-setup.sh). - **Add Hub download logging**: Log Hub config download attempts and errors for easier debugging. - **docker-setup.sh**: Inject `HOST_IP` env var into generated docker-compose.yml. ### v0.22.0 — First-Run Setup Wizard & Local Infra Backup (2026-02-21) Major feature release: moves ALL initial configuration and disaster recovery setup from `docker-setup.sh` into the controller itself as a web-based wizard. **Setup Wizard (`internal/setup/`):** - New web-based setup wizard replaces interactive CLI wizard from `docker-setup.sh` - Dual listener: `:8080` (behind Traefik) + `:8081` (direct HTTP for LAN access before DNS is configured) - Setup mode detection: controller enters wizard when `customer.id` is empty - Two paths: "Restore from backup" (local drive scan + Hub recovery) and "Fresh install" (Hub download or manual config) - Drive scanner: detects `.felhom-infra-backup/` on all connected drives, validates checksums - Hub recovery: `GET /api/v1/recovery/{id}` with retrieval password auth — returns combined config + infra backup - CSRF protection (cookie + hidden field) for all wizard POST endpoints - State persistence (`setup-state.json`) survives browser crashes - All UI text in Hungarian, uses existing dark theme CSS - After setup: writes `controller.yaml`, creates `settings.json`, `os.Exit(0)` → Docker restart into normal mode **Local Infra Backup (`internal/backup/local_infra.go`):** - Writes infrastructure backup to all connected drives as `.felhom-infra-backup/backup.json` + `metadata.json` - Schema-versioned with SHA256 checksum validation - Runs on startup and after each nightly backup cycle - Enables disaster recovery without Hub connectivity — any drive can bootstrap a new controller **Hub Verification:** - Pusher parses Hub report response for `customer_blocked` field - Updates `hub_verified` / `hub_verified_at` in settings on each successful push - `IsLimitedMode()` checks verification state + 7-day grace period **Recovery Info:** - New `internal/recovery/` package generates `recovery-info.txt` in data directory - Settings page shows recovery info section (customer ID, Hub URL, masked retrieval password) - Recovery file auto-regenerated on each startup when retrieval password is set **Pending Events:** - New `PendingEvent` type in settings with `AddPendingEvent()` / `DrainPendingEvents()` - Events queued during setup (e.g., DR completed) are drained and pushed to Hub on first successful report push **Config & Settings Schema:** - `config.go`: Added `SetupListen` field (default `:8081`), `LoadPermissive()`, `Default()` - `settings.go`: Added `hub_verified`, `hub_verified_at`, `retrieval_password`, `pending_events` fields with RWMutex accessors **Infrastructure:** - `docker-compose.yml`: Added port `8081:8081` mapping for setup wizard - Removed old fresh-deployment auto-restore code from `main.go` (lines 70-141) - Removed `restoreSettingsFromHub()` and `restorePasswordsFromHub()` helpers ### v0.21.3 — Config Apply Infra Push + Fixes (2026-02-20) - **Push infra backup after config apply**: After a successful `POST /api/config/apply`, the controller immediately pushes an infra backup to the Hub so the config sync status updates right away. - **Fix double "v" prefix in startup event**: "Controller elindult (vv0.21.2)" → "Controller elindult (v0.21.3)". ### v0.21.2 — Config Apply Bind Mount Fix (2026-02-20) - **Fix config apply on Docker bind mounts**: `POST /api/config/apply` failed with "device or resource busy" because `os.Rename()` doesn't work on bind-mounted files. Now falls back to direct write when rename fails. ### v0.21.1 — Config Content Endpoint (2026-02-20) - **`GET /api/config`**: New endpoint returning raw controller.yaml content (text/yaml). Used by Hub for live config diff and pull operations. Same auth as other config endpoints (Bearer token or session cookie). ### 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/