# felhom-controller **Central management container for Felhom home servers.** A single, lightweight Go container that replaces Portainer + scattered systemd scripts with a unified, Hungarian-language web dashboard for managing Docker Compose stacks, backups, storage, monitoring, and notifications on customer hardware. **Current version: v0.28.0** --- ## Table of Contents - [Architecture](#architecture) - [Features](#features) - [App Management](#1-app-management) - [Backup System](#2-backup-system) - [Storage Management](#3-storage-management) - [Monitoring & Health](#4-monitoring--health) - [Notifications](#5-notifications) - [Update Management](#6-update-management) - [Authentication & Settings](#7-authentication--settings) - [Central Hub](#8-central-hub-reporting) - [Setup Wizard](#9-first-run-setup-wizard) - [Disaster Recovery](#10-disaster-recovery) - [Asset Sync](#11-asset-sync) - [Debug Mode](#12-debug-mode) - [Repository Layout](#repository-layout) - [Configuration](#configuration) - [REST API](#rest-api) - [Build & Deploy](#build--deploy) - [Roadmap](#roadmap) --- ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Customer Hardware (N100 mini PC / Raspberry Pi) │ │ │ │ ┌──────────┐ ┌────────────────────────────────────────────┐ │ │ │ Traefik │ │ felhom-controller (privileged container) │ │ │ │ (reverse │──▶│ │ │ │ │ proxy) │ │ ┌──────────┐ ┌─────────────────────────┐│ │ │ └──────────┘ │ │ Web UI │ │ Stack Manager ││ │ │ │ │ (HU dash │ │ (compose ops, git sync, ││ │ │ ┌──────────┐ │ │ board) │ │ deploy, delete, update) ││ │ │ │cloudflared│ │ └──────────┘ └─────────────────────────┘│ │ │ │ (tunnel) │ │ ┌──────────┐ ┌─────────────────────────┐│ │ │ └──────────┘ │ │ Backup │ │ Storage Manager ││ │ │ │ │ (3-layer │ │ (disk scan, format, ││ │ │ ┌──────────┐ │ │ restic) │ │ mount, migrate) ││ │ │ │ App │ │ └──────────┘ └─────────────────────────┘│ │ │ │ stacks │ │ ┌──────────┐ ┌─────────────────────────┐│ │ │ │ (docker │ │ │Scheduler │ │ Monitor & Metrics ││ │ │ │ compose) │ │ │(cron-like│ │ (health, SQLite ││ │ │ └──────────┘ │ │ jobs) │ │ time-series, Chart.js) ││ │ │ │ └──────────┘ └─────────────────────────┘│ │ │ │ ┌──────────┐ ┌─────────────────────────┐│ │ │ │ │ Notify │ │ REST API + Hub Reporter ││ │ │ │ │ (events) │ │ (JSON push + events) ││ │ │ │ └──────────┘ └─────────────────────────┘│ │ │ │ ┌──────────┐ │ │ │ │ │ Assets │ │ │ │ │ │ (Hub │ │ │ │ │ │ sync) │ │ │ │ │ └──────────┘ │ │ │ └────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ events + reports │ git pull │ asset sync ▼ ▼ ▼ hub.felhom.eu gitea.dooplex.hu hub.felhom.eu (central dashboard) (stack definitions) (logos, screenshots) ``` ### Key Architecture Decisions - **Pure Go, no frameworks** — stdlib `net/http` + `html/template`. Only external deps: `bcrypt`, `yaml.v3`, `modernc.org/sqlite` (pure Go, no CGO). - **Privileged container** — Required for disk operations (format, mount, fstab), `/dev` access, and Docker socket control. - **`/host-dev` indirection** — Docker overrides `/dev` with a tmpfs. The host's `/dev` is mounted at `/host-dev` to access block devices. - **`StackDataProvider` interface** — Breaks circular import between backup and stacks packages. Implemented by `stackAdapter` in `main.go`. Provides `GetStackHDDPath()` for per-drive backup routing. - **Atomic file writes** — All persistent state (`settings.json`, `app.yaml`) written to `.tmp` then `os.Rename` for crash safety. - **`go:embed` templates** — All HTML/CSS/JS compiled into the binary. No runtime file dependencies. - **Europe/Budapest timezone** — All scheduled jobs, timestamps, and UI labels use Hungarian timezone. ### Module Map | Module | Path | Responsibility | |--------|------|----------------| | **Config** | `internal/config/` | YAML loader, validation, `FELHOM_*` env overrides | | **Settings** | `internal/settings/` | Runtime-mutable `settings.json` (passwords, backup prefs, storage paths, notifications) | | **Stacks** | `internal/stacks/` | Compose operations, scanning, `.felhom.yml` metadata, deploy/delete flow | | **Sync** | `internal/sync/` | Git-based app catalog sync (clone/pull, content-hash copy) | | **Backup** | `internal/backup/` | Per-drive 3-layer backup: DB dumps → restic snapshots → cross-drive copies, restore | | **Storage** | `internal/storage/` | Disk scanning (`lsblk`), partitioning (`sfdisk`), formatting (`mkfs.ext4`), mounting, data migration (`rsync`) | | **System** | `internal/system/` | System info (`/proc`), CPU collector, mount points, disk usage, FS info | | **Monitor** | `internal/monitor/` | System health checks, storage watchdog, legacy Healthchecks pinger (deprecated) | | **Metrics** | `internal/metrics/` | SQLite time-series store, system + container metric collection | | **Scheduler** | `internal/scheduler/` | Central job scheduler (periodic + daily, skip-if-running, panic recovery) | | **SelfUpdate** | `internal/selfupdate/` | Version checking (registry), update trigger, state persistence, startup verification | | **Notify** | `internal/notify/` | Email notifications via hub relay, preference sync, per-event cooldowns | | **Report** | `internal/report/` | Hub report builder + HTTP pusher (system, stacks, backup, health) | | **Assets** | `internal/assets/` | Hub-managed asset syncer: downloads logos/screenshots with SHA-256 change detection | | **SelfTest** | `internal/selftest/` | Startup self-test: 9 diagnostic checks (Docker, dirs, storage, hub, restic, metrics) | | **Util** | `internal/util/` | Shared utilities: `TruncateStr` for debug log output truncation | | **API** | `internal/api/` | REST JSON endpoints, diagnostic dump (`/api/debug/dump`) | | **Web** | `internal/web/` | Hungarian dashboard, auth, page handlers, template functions, alerts | --- ## Features ### 1. App Management The controller manages Docker Compose stacks through a complete lifecycle: catalog sync, first-time deployment, runtime operations, and deletion. #### Git Sync (`internal/sync/`) The app catalog lives in a separate Git repository. The controller: - Shallow-clones the catalog on startup - Periodically fetches updates (configurable, default 15 min) - Copies only `docker-compose.yml` and `.felhom.yml` to the stacks directory - **Never overwrites** `app.yaml` or `.env` (user secrets are safe) - Uses SHA-256 content hashing — only writes files that actually changed - Triggers stack rescan after sync so the dashboard updates immediately - **Post-sync hook**: auto-injects missing deploy fields (new secrets, domains) into existing `app.yaml` for stacks whose templates were updated (see Missing Field Injection below) - Manual sync via "Sablonok frissitese" button or `POST /api/sync` #### First-Time Deploy Flow 1. Customer sees app card with "Telepites" button 2. Deploy page pre-generates and **displays** all auto-values before the user clicks deploy: - `domain` fields: shown as readonly text input with the customer's configured base domain - `subdomain` fields: editable text input pre-filled with the default from `.felhom.yml`, shown with `.base-domain` suffix. Validated for DNS-safe format, reserved names, and uniqueness across deployed stacks. Locked after deploy — changing requires Remove + Redeploy - `secret` fields: pre-generated and shown as masked password inputs with a "Megjelenítés" reveal button — user can see/copy all DB passwords and keys before deploying - User-configurable inputs (admin password, language, storage path) remain editable - Section header prompts the user to note down any passwords they need 3. `checkBeforeDeploy()` JS guard fetches live state first (prevents double-deploy from another tab) 4. **Memory validation** uses real system memory from `/proc/meminfo`: - `usable_memory = total_ram - reserved_memory_mb` (default 384MB reserved) - `system.GetMemoryMB()` returns real-time total and used memory (not declared reservations) - Hard block if `used_mb + new_request > usable_memory` - `CommittedMemory()` (declared sum) still used for soft overcommit warning only - Deploy page shows real memory usage bar (not declared reservations) 5. Pre-generated secret values are submitted as hidden form inputs so the **same values** the user saw are saved to `app.yaml` (no silent re-generation on submit). Controller saves `app.yaml`, sets in-memory `Deployed` flag **before** `docker compose up -d` (avoids stale UI during slow image pulls), reverts on failure 6. 3-step progress panel polls `GET /api/stacks/{name}` every 3s: config saved → containers starting → health check passed 7. Post-deploy: locked fields (DB_PASSWORD, etc.) become read-only; the "Automatikusan generált értékek" section continues to show the saved values on the settings page #### App Info Pages Each app can define rich metadata in `.felhom.yml`: - `app_info`: tagline, use_cases, first_steps, prerequisites, default_creds, docs_url - `optional_config`: groups of post-deploy configurable env vars (e.g., API keys for metadata providers) - `resources`: mem_request, mem_limit, pi_compatible, needs_hdd, hungarian_ui The `/apps/{slug}` page renders hero section, screenshots, setup guide, and optional config form. #### Stack Operations | Operation | What it does | |-----------|-------------| | Start | `docker compose up -d` — pre-start memory check rejects with 409 if insufficient RAM | | Stop | `docker compose stop` (blocked for protected stacks) | | Restart | `docker compose restart` | | Update | `docker compose pull` + `docker compose up -d` | | Remove | `docker compose down --volumes` + remove `app.yaml` + optional HDD/backup cleanup; template preserved for redeploy | | Delete | `docker compose down --rmi local --volumes` + optional HDD data cleanup (orphaned stacks only) | **Remove vs Delete**: "Eltávolítás" (Remove) is for deployed catalog stacks — it reverts the stack to "Nincs telepítve" state while keeping the template for easy redeployment. "Törlés" (Delete) is for orphaned stacks — it removes the entire stack directory including templates. Both require stopping the stack first. **Remove modal** shows three sections: (1) always-removed items (Docker volumes, app.yaml, cross-drive schedule), (2) optional HDD data deletion with reimport warning, (3) optional backup data deletion (DB dumps + cross-drive rsync) with restic retention note. **Protected stacks** (traefik, cloudflared, felhom-controller) cannot be stopped, removed, or deleted from the UI. Restart is allowed. **Orphan detection**: Deployed stacks with no matching catalog template are marked as orphaned with an "Elavult" badge and can be safely deleted. #### Missing Field Injection (`deploy.go`) When app templates are updated (e.g., a new `APP_KEY` secret is added to `.felhom.yml`), existing deployed apps need the new field in their `app.yaml`. The controller handles this automatically: - **On startup**: `InjectMissingFields()` runs for all deployed stacks - **After sync**: the post-sync hook runs for stacks whose templates were updated - For each deployed stack, compares `.felhom.yml` `deploy_fields` against `app.yaml` env vars - Missing `secret` fields: auto-generated using the field's generator spec (`password:N`, `hex:N`, `base64key:N`) - Missing `domain` fields: filled with the customer's configured domain - Missing `subdomain` fields: filled with the field's default value or the `.felhom.yml` `subdomain:` metadata - Other field types (e.g., `text`, `select`): logged as warning for manual configuration - Locked fields are added to the locked list automatically **Generator types**: `password:N` (alphanumeric), `hex:N` (hex-encoded random bytes), `base64key:N` (`base64:` + N random bytes base64-encoded, for Laravel APP_KEY etc.), `static:VALUE` (literal value). #### Container State Display | State | Color | Label | Meaning | |-------|-------|-------|---------| | Running + healthy | Green | "Fut" | All containers running and healthy | | Running + starting | Orange | "Indulas..." | Healthcheck not yet passed | | Running + unhealthy | Yellow | "Nem egeszseges" | Healthcheck failing | | Stopped/exited | Red | "Leallitva" | All containers stopped | | Restarting | Yellow | "Ujrainditas..." | Restart loop | | Not deployed | Gray | "Nincs telepitve" | Compose file exists, not deployed | --- ### 2. Backup System The backup system implements a **3-2-1 backup architecture**. Each tier is a **complete, self-sufficient backup** — any single tier can fully restore an app. | Tier | Contents | Location | Can fully restore? | |------|----------|----------|--------------------| | **1. Nightly restic** | DB + Config + User data | Same drive as app | Yes (not against drive failure) | | **2. Cross-drive** | DB + Config + User data | Different physical device | Yes | | **3. Remote** | Everything | Cloud / remote server | Future | **Key principles:** - User data backup is **mandatory** — every app with HDD bind mounts is included automatically. There is no per-app toggle. - Each tier includes **everything** needed to restore: DB dumps, config, and user data. No tier depends on another tier's data. - **Tier 2 is configurable for ALL apps** — not just apps with HDD data. Non-HDD apps back up config + DB dumps to the secondary drive (small but protects against drive failure). - The `AppBackupPrefs.Enabled` field in settings.json is legacy and not read by any code. **Per-app Tier 2 contents by app type:** | App type | Tier 2 contents | Example | |----------|----------------|---------| | HDD + DB | Config + DB + User data | Immich, Paperless-ngx | | HDD, no DB | Config + User data | — | | DB, no HDD | Config + DB | Mealie, Vikunja | | Config only | Config | Gokapi, Homepage | #### Tier 1: Nightly Backup (mandatory, same drive) The nightly backup has two phases that run sequentially. All paths are **per-drive** — each physical drive gets its own restic repo and per-app DB dump directories. **Drive layout (v0.26.0):** ``` / ├── felhom-data/ ← all controller-managed data (namespace, v0.26.0+) │ ├── appdata// ← app user data │ └── backups/ │ ├── primary/ │ │ ├── restic/ ← one restic repo per drive (all apps on this drive) │ │ └── /db-dumps/ ← per-app DB dump files │ └── secondary/ │ ├── restic/ ← secondary restic repo (cross-drive) │ ├── _infra/ ← infra config mirror │ └── /rsync/ ← per-app rsync data ├── .felhom-infra-backup/ ← DR marker (stays at drive root for scanner) ├── Dokumentumok/ ← user files (not controller-managed) └── media/ ← user files (not controller-managed) ``` > **Note:** `HDD_PATH` env var in `app.yaml` is still the mount point (e.g., `/mnt/hdd_1`). The `felhom-data` segment is embedded in path helpers — not in `HDD_PATH`. > Pre-v0.26.0 installations use `/appdata/` and `/backups/` directly (no `felhom-data/` namespace). Path computation is centralized in `backup/paths.go` via the `FelhomDataDir = "felhom-data"` constant: - `PrimaryResticRepoPath(drivePath)` → `/felhom-data/backups/primary/restic/` - `AppDBDumpPath(drivePath, stackName)` → `/felhom-data/backups/primary//db-dumps/` - `AppDataDir(drivePath, stackName)` → `/felhom-data/appdata//` - `SecondaryResticRepoPath(drivePath)` → `/felhom-data/backups/secondary/restic/` - `AppSecondaryRsyncPath(drivePath, stackName)` → `/felhom-data/backups/secondary//rsync/` - `SecondaryInfraPath(drivePath)` → `/felhom-data/backups/secondary/_infra/` - `InfraBackupDir(mountPath)` → `/.felhom-infra-backup/` (**unchanged** — stays at drive root for DR scanner) **Phase 1 — Database Dumps** (`internal/backup/dbdump.go`, scheduled 02:30) - **Auto-discovery** of PostgreSQL and MariaDB containers via `docker ps` + `docker inspect` - Dumps via `docker exec pg_dump` / `docker exec mariadb-dump` with 5-minute timeout - Dumps are written to the app's **home drive**: `AppDBDumpPath(appDrive, stackName)` - Atomic writes (`.tmp` → `.sql`) to prevent corruption - **Validation** after each dump: checks file size, header presence, counts `CREATE TABLE` - Results cached in `settings.json` surviving container restarts **Phase 2 — Restic Snapshot** (`internal/backup/restic.go`, scheduled 03:00) - Apps are **grouped by drive** via `groupStacksByDrive()` — each drive's apps are backed up to that drive's restic repo - App drive resolution: `GetStackHDDPath()` (from `StackDataProvider`) → falls back to `SystemDataPath` - Auto-generated repository password (32 random bytes, base64url), shared across all repos, synced to hub - **Paths included in every per-drive snapshot:** - Per-app DB dump dirs on that drive - Per-app HDD mount paths (user data) - Stacks dir (compose.yml + app.yaml + .felhom.yml for all apps) - `controller.yaml` (controller config) - Auto-detects and unlocks stale locks (restic repo lock) - Weekly prune on Sundays with configurable retention (keep-daily, keep-weekly, keep-monthly) - Weekly integrity check (`restic check`) on Sunday 04:00 — checks **all** primary repos **Protects against:** accidental deletion, data corruption, point-in-time rollback. Does NOT protect against drive failure (backup is on the same physical drive). #### Tier 2: Cross-Drive Backup (opt-in, different device) (`internal/backup/crossdrive.go`) **Complete backup** to a different physical drive. Available for **all apps** — apps with HDD data back up config + DB + user data; apps without HDD back up config + DB dumps only. - **Auto-enable for small apps (v0.14.1):** Apps without HDD mounts (config-only, DB-only) are automatically configured for daily rsync Tier 2 when ≥2 storage paths are registered. `AutoEnableSmallApps()` runs at the start of each nightly backup cycle. Never overwrites existing user-configured cross-drive settings (even disabled ones). - **Infrastructure config backup (v0.14.1):** `syncInfraConfig()` rsyncs the stacks directory and `controller.yaml` to `/backups/secondary/_infra/` on every secondary destination drive. Runs before per-app backups. Cross-drive restic also includes infra paths. - **Two methods:** - **rsync** — Simple mirror with `--delete` (fast, no versioning, **browsable** on disk) - **restic** — Versioned, deduplicated, encrypted (shared repo across apps, not browsable) - Per-app configuration in settings.json: destination path, method, schedule (daily/weekly/manual) - **Pre-backup DB dump:** `DumpStackDB()` runs fresh pg_dump/mariadb-dump before each cross-drive backup; non-fatal on failure (wired via `DBDumper` interface to avoid circular imports) - **Empty mounts allowed:** `RunAppBackup` accepts apps with no HDD mounts — the rsync mount loop simply doesn't execute, but DB + config copy still runs - **Drive-type-aware validation** (`ValidateDestination`): | Destination type | Space checks | |-----------------|--------------| | External mount (different device than `/`) | Block if <100 MB free | | System drive (same device as `/`) | Require ≥10 GB free AND <90% used; logged warning | - **Secondary drive layout (v0.14.1):** ``` /backups/secondary/ ├── _infra/ ← infrastructure config mirror (v0.14.1) │ ├── controller.yaml │ └── stacks/ ← full stacks dir (all app configs) ├── /rsync/ ← per-app rsync mirror │ ├── _db/ ← DB dump files │ ├── _config/ ← compose.yml, app.yaml, .felhom.yml │ └── ← HDD mount contents (if app has HDD data) └── restic/ ← shared restic repo (all cross-drive apps) ``` - DB dump files read from **per-app home drive** path (`AppDBDumpPath`) - `_` prefix directories prevent collision with user data - For non-HDD apps, only `_db/` and `_config/` are present (no user data directory) - **Restic backup paths:** includes HDD mounts (if any) + config dir + per-app DB dump dir from home drive + stacks dir + controller.yaml (infra, v0.14.1) - Safety guards: destination ≠ source, path-overlap check (HDD mounts only), writable check - **Chained execution:** runs immediately after nightly restic — daily apps every night, weekly apps on Sundays - **Hub reporting after manual triggers (v0.27.2):** `OnCrossDriveComplete` callback on Router pushes infra backup snapshot to Hub + writes local infra backup after both single-app and run-all manual triggers complete (previously only automatic scheduled runs reported) - Per-app concurrency lock prevents overlapping runs - Status (last_run, duration, size, error) persisted to settings.json **Protects against:** primary drive failure, drive theft/damage. #### Tier 3: Remote Backup (future) Complete offsite backup for disaster recovery. Not yet implemented. Placeholder shown in UI ("3. mentés — Hamarosan"). #### Restore (`internal/backup/restore.go`) All deployed apps appear in the restore dropdown — every app has restic snapshot data (stacks dir + DB dumps are always backed up). | App type | Config restored | DB restored | User data restored | |----------|----------------|------------|-------------------| | Has HDD data | Yes | Yes | Yes (always — backup is mandatory) | | DB only, no HDD | Yes | Yes | n/a | | No DB, no HDD | Yes | — | n/a | - **Snapshot API** returns ALL snapshots unfiltered — older snapshots still allow config+DB restore; `RestoreApp` extracts whatever paths are available - **Restore type info** shown per-app when selected in dropdown (Hungarian banners): - Has HDD: "Teljes visszaállitas: adatbazis + konfiguracio + felhasznaloi adatok" - Has DB, no HDD: "Adatbazis es konfiguracio visszaallitasa" - No DB, no HDD: "Csak konfiguracio visszaallitasa" - **Execution flow:** stop app → resolve app's home drive → `restic restore --target / --include ...` from per-drive repo → restart app - Restic repo resolved via `PrimaryResticRepoPath(appDrivePath)` - DB dumps restored from `AppDBDumpPath(appDrivePath, stackName)` - Running flag prevents concurrent backup/restore operations - Snapshot ID validated (8-64 lowercase hex) **Note:** Restore currently uses Tier 1 (primary restic repo on app's home drive) only. Restoring from Tier 2 (cross-drive) is a future enhancement. #### Backup Page UI (`internal/web/templates/backups.html`) Unified per-app status table with expandable rows showing **per-tier** backup status: **Status dot per app:** | Dot color | Meaning | |-----------|---------| | Green | 2+ tiers configured with successful backups + destination healthy | | Yellow | Only 1 tier, or Tier 2 failing, or Tier 2 configured but never run | | Red | Tier 2 destination blocked or inaccessible | Every app starts as yellow (1 tier only). Green requires Tier 2 configured with successful backup. **Per-app backup tiers (3 rows per app):** - **1. mentes** (Tier 1, always present) — Auto badge + "helyi" + last run + contents (e.g., "DB + Konfig + Adatok") - **2. mentes** (Tier 2, configurable for ALL apps) — one of: - Configured: method (rsync/restic) + destination + schedule + last run + status + contents + browsable indicator (folder icon for rsync) + action buttons - Not configured: "1. mentes auto" + "Nincs 2. masolat" + settings link - **3. mentes** (Tier 3, placeholder) — grayed out "Hamarosan" + "tavoli (offsite)" + future note **Backup contents per app** (shown per tier): - Apps with DB + HDD: "DB + Konfig + Adatok" - Apps with DB only: "DB + Konfig" - Apps with HDD, no DB: "Konfig + Adatok" - Apps with neither: "Konfig" **Deploy page** shows cross-drive (Tier 2) configuration form for **all deployed apps**, not just those with HDD data. Non-HDD apps can configure destination, method, and schedule. **Other sections:** - Schedule overview with next run times for DB dump, restic, prune - Snapshot history table (last 20 snapshots aggregated from all per-drive repos, sorted by time) - Storage overview card (total size across repos, snapshot count, DB dump count/size, encryption key with show/copy) - Restore section: app dropdown → snapshot dropdown → restore type info → confirmation checkbox → execute --- ### 3. Storage Management The storage subsystem handles the full lifecycle of external storage: detection, initialization, path registration, and data migration. #### Disk Scanning (`internal/storage/scan.go`) - `ScanDisks()` uses `lsblk -J -b` for block device enumeration - System disk detection via host fstab parsing (`/host-fstab`) + UUID resolution via `blkid` - Partitions enriched with filesystem type, UUID, and label from direct `blkid` probing (Docker containers have incomplete udev cache) - Returns `AvailableDisks` (non-system, non-loop, non-CDROM) and `SystemDisks` separately - Handles NVMe (`nvme0n1p1`), SCSI (`sdb1`), and eMMC (`mmcblk0p1`) naming #### Disk Initialization Wizard (`internal/storage/format.go`) A step-by-step UI at `/settings/storage/init`: 1. **Scan** — Lists available disks with model, size, partition info 2. **Select** — User picks a disk and enters a mount name (e.g., `hdd_1`) 3. **Confirm** — User types "FORMAZAS" to confirm destructive operation 4. **Format pipeline**: `wipefs` → `sfdisk` (GPT) → `mkfs.ext4` → `blkid` UUID → backup fstab → append UUID-based fstab entry → mount → `findmnt` verification → `chown 1000:1000` → create `felhom-data/` and `Dokumentumok/` subdirectories 5. Auto-registers new storage path in settings.json 6. Smart partition detection: skips repartitioning for existing empty partitions Safety guards: system disk detection, mount path conflict check, confirmation required, progress channel for real-time UI feedback. #### Attach Existing Drive Wizard (`internal/storage/attach.go`) A step-by-step UI at `/settings/storage/attach` for drives that already have a filesystem (e.g., a previously used ext4 drive). Unlike the init wizard, this does **not** format the drive — existing data is preserved. **Problem solved:** Mounting a whole drive at `/mnt/` would mix existing user data with the controller's directory structure (`felhom-data/`, `Dokumentumok/`, etc.). The bind-mount approach isolates the controller's working directory from other data on the drive. 1. **Scan** — Lists available disks, filtered to partitions that have an existing filesystem (FSType != "") 2. **Mount raw** — Partition is mounted read-only at a hidden staging path (`/mnt/.felhom-raw/