# TASK: Phase B — Storage Management UI Polish & Health Severity Fix **Version target:** controller 0.10.0 **Repo:** `deploy-felhom-compose` (controller) ## Overview Phase A (v0.9.0) delivered the storage paths foundation: registry in settings.json, auto-discovery, per-app HDD_PATH resolution, settings UI with CRUD, deploy dropdown, and health monitoring. All functional — but health check now FAILS on demo-felhom because `/mnt/hdd_placeholder` is (correctly) detected as not a real mount point. **Immediate fix:** Health severity reclassification — non-mount-point is a **warning**, not an **issue** that causes FAIL. The FAIL status should be reserved for genuinely broken things (services down, disk critically full, backup failing), not informational findings. Phase B then polishes the UI and fills gaps: 1. **Health severity fix** — mount-point check: warning not issue 2. **Success flash messages** — storage operations only show errors, never success 3. **Edit labels** — can't rename a storage path after adding it 4. **App names per storage path** — settings page shows count, not which apps 5. **Per-app storage info on stacks page** — no visibility into which storage each app uses 6. **Deploy dropdown enhancements** — show free space, disk usage warning 7. **Filesystem & disk info** — show ext4/btrfs, device, model on settings page 8. **Backup page: storage path context** — show which storage path each app is on --- ## 0. Health Severity Fix (URGENT — do first) ### 0.1 Problem `checkStoragePaths()` in `healthcheck.go` currently classifies non-mount-point as an **issue**: ```go // CURRENT (line ~6751): if !system.IsMountPoint(sp.Path) { issues = append(issues, fmt.Sprintf("Storage path %s is NOT a mount point — data writes to SSD!", sp.Path)) } ``` Issues → `status = "fail"` → Healthchecks shows FAIL → Healthchecks triggers alert → hub shows "STATUS: FAIL". This cascades into a false alarm for any setup where the storage path is intentionally on SSD (demo environments, test environments, customers who haven't connected an external drive yet). ### 0.2 Fix: Warning + Hungarian message ```go // FIXED: if !system.IsMountPoint(sp.Path) { warnings = append(warnings, fmt.Sprintf( "Storage path %s is not a separate mount point — data is stored on the system drive", sp.Path)) } ``` Health status becomes `"warn"` instead of `"fail"`. The warning still appears on: - Controller monitoring page (red banner → yellow banner) - Hub customer detail page (Issues → Warnings section) - Healthchecks ping body (status: WARN instead of FAIL) ### 0.3 When should non-mount-point be an ISSUE? In the future (Phase C or later), consider an "acknowledged" flag per storage path: - When adding a path that's not a mount point, show a confirmation dialog: "Ez az útvonal a rendszermeghajtón van. Biztosan folytatja?" - If acknowledged, the health check uses warning level - If a previously-mount-point path STOPS being a mount point (drive disconnected), that IS an issue — it means something changed unexpectedly For now, the simple severity downgrade to warning is sufficient. The informational value is preserved, without false alarms. ### 0.4 Also: Hungarian messages in health check Currently health messages are in English: - "Storage path /mnt/hdd_placeholder is NOT a mount point — data writes to SSD!" - "Storage path not accessible: ..." - "Storage ... nearly full: ..." These appear on the customer-facing monitoring page. Change to Hungarian: ```go // Path not accessible warnings = append(warnings, fmt.Sprintf("Adattároló nem elérhető: %s", sp.Path)) // Not a mount point warnings = append(warnings, fmt.Sprintf( "Az adattároló (%s) nem külön meghajtón van — az adatok a rendszermeghajtóra íródnak", sp.Path)) // Disk usage critical (≥95%) — this stays as issue issues = append(issues, fmt.Sprintf("Adattároló majdnem megtelt: %s (%.0f%%)", sp.Path, di.UsedPercent)) // Disk usage high (≥90%) — warning warnings = append(warnings, fmt.Sprintf("Adattároló használat magas: %s (%.0f%%)", sp.Path, di.UsedPercent)) ``` Note: Hub and Healthchecks receive the raw text. Hub is operator-facing (English would also be fine there), but since the same messages show on the customer controller, Hungarian is better for consistency. ### 0.5 Monitoring page banner color Currently the monitoring page shows issues as red banners. Warnings should be yellow/amber: ```html {{range .Warnings}}
⚠️ {{.}}
{{end}} ``` Check if the monitoring template already differentiates issue vs warning banners. If not, add CSS class: ```css .monitoring-banner-warn { background: rgba(255, 193, 7, 0.15); border-left: 4px solid var(--yellow); color: var(--yellow); } ``` --- ## 1. Success Flash Messages for Storage Operations ### 1.1 Problem All storage handlers (`/settings/storage/add`, `/remove`, `/default`, `/schedulable`) only set `StorageError` on failure. On success they redirect without feedback. ### 1.2 Fix: Query param flash (consistent with backup page) Use the existing backup page pattern: redirect with query params `?storage_msg=success&storage_detail=...` In settings handler, parse: ```go if msg := r.URL.Query().Get("storage_msg"); msg == "success" { data["StorageSuccess"] = r.URL.Query().Get("storage_detail") } ``` Success messages: - **Add:** "Adattároló sikeresen hozzáadva: /mnt/hdd_1" - **Remove:** "Adattároló eltávolítva: /mnt/hdd_1" - **Set default:** "Alapértelmezett adattároló beállítva: /mnt/hdd_1" - **Toggle schedulable:** "Adattároló állapot módosítva: /mnt/hdd_1" ### 1.3 Template Add to settings.html (after `StorageError`): ```html {{if .StorageSuccess}}
{{.StorageSuccess}}
{{end}} ``` --- ## 2. Edit Storage Path Labels ### 2.1 UI: Inline edit Add edit button next to label. JS toggles between display and inline form: ```html
{{.Label}}
``` JS `editStorageLabel()` replaces content with: ```html
``` ### 2.2 Route & handler | Method | Path | Auth? | |--------|------|-------| | POST | `/settings/storage/label` | Yes | Handler: parse `storage_path` + `storage_label`, validate (non-empty, max 50 chars), call `settings.SetStorageLabel()`, redirect with success flash. ### 2.3 Settings method ```go func (s *Settings) SetStorageLabel(path, label string) error { s.mu.Lock() defer s.mu.Unlock() for i := range s.StoragePaths { if s.StoragePaths[i].Path == path { s.StoragePaths[i].Label = label return s.save() } } return fmt.Errorf("storage path %q not found", path) } ``` --- ## 3. App Names Per Storage Path (Settings Page) ### 3.1 Current: "3 alkalmazás használja" — no names ### 3.2 Enhancement: Expandable list with names + sizes Extend `StoragePathView`: ```go type StorageAppDetail struct { Name string // Display name (e.g., "Immich") Stack string // Stack name (for link) SizeHuman string // Data size on this path } type StoragePathView struct { // ... existing fields ... AppDetails []StorageAppDetail // NEW } ``` Template: ```html
{{if .AppDetails}}
{{.AppCount}} alkalmazás használja
{{range .AppDetails}}
{{.Name}} {{.SizeHuman}}
{{end}}
{{else}} Nincs alkalmazás ezen a tárolón {{end}}
``` ### 3.3 Populate in handler Scan deployed app.yaml files, match `HDD_PATH` against registered paths, collect display names + data sizes via `GetStackHDDData()`. --- ## 4. Per-App Storage Badge on Stacks Page ### 4.1 Current: No info about which storage path a deployed app uses ### 4.2 Enhancement: Badge on deployed app cards ```html {{if and .Deployed .StoragePath}} 💾 {{.StorageLabel}} {{end}} ``` ### 4.3 Data source Add `StoragePath` and `StorageLabel` to stack view model. Populate from `app.yaml` `HDD_PATH` + lookup against registered storage paths for the label. ### 4.4 CSS ```css .meta-badge-storage { background: rgba(0, 136, 204, 0.12); color: var(--accent-light); } ``` --- ## 5. Deploy Dropdown Enhancements ### 5.1 Current: "Külső tárhely (hdd_placeholder) (/mnt/hdd_placeholder)" ### 5.2 Enhancement A: Show free space in option text ```html ``` ### 5.3 Enhancement B: Low-space warning on selection ```html ``` JS: on dropdown change, read `data-free-percent` from selected option. Show warning if < 20%. ### 5.4 Data struct ```go type DeployStoragePath struct { settings.StoragePath FreeHuman string // "234.5 GB" FreePercent float64 // 67.5 } ``` Populate via `system.GetDiskUsage(sp.Path)` in deploy handler. --- ## 6. Filesystem & Disk Info on Settings Page ### 6.1 New function: `GetFSInfo(path)` In `system/mounts_linux.go`: ```go type FSInfo struct { FSType string // "ext4", "btrfs" Device string // "/dev/sda1" Model string // "WD Elements 25A2" (from sysfs, best-effort) } func GetFSInfo(path string) *FSInfo { // findmnt -n -o SOURCE,FSTYPE --target // /sys/block//device/model for disk model } ``` Non-Linux stub returns nil. ### 6.2 Template Below the disk usage bar: ```html {{if .FSInfo}}
{{.FSInfo.FSType}} · {{.FSInfo.Device}}{{if .FSInfo.Model}} · {{.FSInfo.Model}}{{end}}
{{end}} ``` --- ## 7. Backup Page: Storage Path Context ### 7.1 Current: Paths shown like `/mnt/hdd_placeholder/storage/immich (92 MB)` — no context about which registered storage path ### 7.2 Enhancement: Storage label badge per app Add `StorageLabel` to `AppBackupInfo`: ```go type AppBackupInfo struct { // ... existing ... StorageLabel string // NEW: resolved from registered storage paths } ``` In template (backup section): ```html
{{if .StorageLabel}}{{.StorageLabel}}{{end}} {{.HDDSizeHuman}}
``` Populate by matching each app's HDD_PATH prefix against registered paths during `DiscoverAppData()`. --- ## 8. Implementation Steps ### Step 0: Health severity fix (URGENT) - In `checkStoragePaths()`: move mount-point check from `issues` to `warnings` - Translate all storage health messages to Hungarian - Verify monitoring page template differentiates warning vs issue banner colors - Add `.monitoring-banner-warn` CSS if missing - **Test:** Restart controller → monitoring page shows yellow warning instead of red. Healthchecks goes back to OK/WARN instead of FAIL. Hub shows warning under Warnings section, not Issues. ### Step 1: Success flash messages - Add query param flash parsing in settings handler - Set success flash in all 4 storage handlers on successful redirect - Add `{{if .StorageSuccess}}` to settings.html - **Test:** Add storage path → green "Sikeresen hozzáadva" flash ### Step 2: Edit labels - Add `SetStorageLabel()` to settings.go - Add `POST /settings/storage/label` route + handler - Add JS `editStorageLabel()` / `cancelEditLabel()` to settings.html - Update template with inline edit UI - **Test:** Click ✏️ → input appears → change → save → label updated, flash shown ### Step 3: App details per storage path - Extend `StoragePathView` with `AppDetails []StorageAppDetail` - Populate in settings handler (scan app.yaml + HDD data) - Replace count with expandable `
` list in settings.html - **Test:** Click "3 alkalmazás" → expands to show names + sizes with links ### Step 4: Storage badge on stacks page - Add `StoragePath`/`StorageLabel` to stack view model - Populate from app.yaml + registered paths lookup - Add badge to stacks.html + CSS - **Test:** Immich card shows "💾 Külső tárhely" ### Step 5: Deploy dropdown enhancements - Create `DeployStoragePath` with free space data - Populate via `GetDiskUsage` in deploy handler - Update dropdown option text + `data-free-percent` attr - Add JS for low-space warning - **Test:** Dropdown shows "234 GB szabad" → select near-full → warning ### Step 6: Filesystem info - Add `GetFSInfo()` to `mounts_linux.go` (using `findmnt`) - Add non-Linux stub - Add to `StoragePathView` + template - **Test:** Settings → "ext4 · /dev/sdb1 · WD Elements" ### Step 7: Backup page storage context - Add `StorageLabel` to `AppBackupInfo` - Populate during `DiscoverAppData()` - Add badge to backups.html - **Test:** Backup page → Immich shows storage label badge ### Step 8: Version bump & cleanup - Update CHANGELOG.md / CONTEXT.md / CLAUDE.md / README.md - Bump to 0.10.0 - Build + deploy - **Full test:** Health OK, all pages show storage context, deploy warns on low space --- ## 9. Files to Create / Modify ### Modified files: - `controller/internal/monitor/healthcheck.go` — **Step 0**: severity fix, Hungarian messages - `controller/internal/settings/settings.go` — **Step 2**: `SetStorageLabel()` - `controller/internal/system/mounts_linux.go` — **Step 6**: `GetFSInfo()`, `FSInfo` struct - `controller/internal/system/mounts_other.go` — **Step 6**: `GetFSInfo()` stub - `controller/internal/backup/appdata.go` — **Step 7**: `StorageLabel` in `AppBackupInfo` - `controller/internal/web/handlers.go` — **Steps 1-7**: flash parsing, label handler, deploy/settings/stacks/backup data enrichment - `controller/internal/web/server.go` — **Step 2**: register `/settings/storage/label` route - `controller/internal/web/templates/settings.html` — **Steps 1-3,6**: flash, edit UI, app details, FS info - `controller/internal/web/templates/stacks.html` — **Step 4**: storage badge - `controller/internal/web/templates/deploy.html` — **Step 5**: free space in dropdown, warning - `controller/internal/web/templates/backups.html` — **Step 7**: storage label badge - `controller/internal/web/templates/monitoring.html` — **Step 0**: warning vs issue banner differentiation - `controller/internal/web/templates/style.css` — **Steps 0,2,3,4**: warn banner, edit label, app list, storage badge --- ## 10. Design Decisions ### Why downgrade mount-point to warning instead of removing the check? The check is genuinely useful — it detects USB drives that got disconnected, or misconfigured storage. But it's informational, not a service-affecting failure. The customer can still use the system; they just should know their data location. A FAIL status implies something is actively broken and needs immediate attention. ### Why not add an "acknowledge" mechanism now? It adds UI complexity (modal confirmation, per-path flag in settings.json, conditional severity logic). The warning downgrade solves the immediate false-alarm problem. An acknowledge system can be added in Phase C if needed, especially for the scenario where a previously-mounted drive disappears. ### Why query param flashes instead of session-based? No session store — consistent with backup page pattern. Stateless, simple. ### What's NOT in Phase B - **Migration wizard (Phase C):** "Mozgatás" button, rsync with progress - **Disk SMART health:** Needs smartmontools - **Auto-detect new mounts:** inotify/polling for new /mnt/* — future - **Acknowledge mechanism:** For known non-mount-point paths — future