17 KiB
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:
- Health severity fix — mount-point check: warning not issue
- Success flash messages — storage operations only show errors, never success
- Edit labels — can't rename a storage path after adding it
- App names per storage path — settings page shows count, not which apps
- Per-app storage info on stacks page — no visibility into which storage each app uses
- Deploy dropdown enhancements — show free space, disk usage warning
- Filesystem & disk info — show ext4/btrfs, device, model on settings page
- 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:
// 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
// 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:
// 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:
{{range .Warnings}}
<div class="monitoring-banner monitoring-banner-warn">
⚠️ {{.}}
</div>
{{end}}
Check if the monitoring template already differentiates issue vs warning banners. If not, add CSS class:
.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:
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):
{{if .StorageSuccess}}<div class="alert alert-info">{{.StorageSuccess}}</div>{{end}}
2. Edit Storage Path Labels
2.1 UI: Inline edit
Add edit button next to label. JS toggles between display and inline form:
<div class="storage-path-label-wrap" id="label-wrap-{{$idx}}">
<span class="storage-path-label" id="label-display-{{$idx}}">{{.Label}}</span>
<button class="btn btn-xs btn-ghost" onclick="editStorageLabel({{$idx}}, '{{.Path}}', '{{.Label}}')" title="Átnevezés">✏️</button>
</div>
JS editStorageLabel() replaces content with:
<form method="POST" action="/settings/storage/label" style="display:inline-flex;gap:.5rem;align-items:center">
<input type="hidden" name="storage_path" value="...">
<input type="text" name="storage_label" class="form-control form-control-sm" value="..." style="width:200px">
<button type="submit" class="btn btn-xs btn-primary">OK</button>
<button type="button" class="btn btn-xs btn-outline" onclick="cancelEditLabel({{$idx}})">✕</button>
</form>
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
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:
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:
<div class="storage-path-meta">
{{if .AppDetails}}
<details class="storage-app-details">
<summary class="form-hint" style="cursor:pointer">
{{.AppCount}} alkalmazás használja
</summary>
<div class="storage-app-list">
{{range .AppDetails}}
<div class="storage-app-row">
<a href="/stacks/{{.Stack}}/deploy" class="storage-app-link">{{.Name}}</a>
<span class="mono form-hint">{{.SizeHuman}}</span>
</div>
{{end}}
</div>
</details>
{{else}}
<span class="form-hint">Nincs alkalmazás ezen a tárolón</span>
{{end}}
</div>
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
{{if and .Deployed .StoragePath}}
<span class="meta-badge meta-badge-storage" title="Adattároló: {{.StoragePath}}">
💾 {{.StorageLabel}}
</span>
{{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
.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
<option value="{{.Path}}" data-free-percent="{{printf "%.0f" .FreePercent}}"
{{if .IsDefault}}selected{{end}}>
{{.Label}} — {{.FreeHuman}} szabad
{{if .IsDefault}} ★{{end}}
</option>
5.3 Enhancement B: Low-space warning on selection
<div id="storage-space-warn" class="form-hint" style="color:var(--yellow);display:none">
⚠️ A kiválasztott tárhely majdnem megtelt.
</div>
JS: on dropdown change, read data-free-percent from selected option. Show warning if < 20%.
5.4 Data struct
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:
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 <path>
// /sys/block/<dev>/device/model for disk model
}
Non-Linux stub returns nil.
6.2 Template
Below the disk usage bar:
{{if .FSInfo}}
<div class="storage-path-fsinfo mono form-hint">
{{.FSInfo.FSType}} · {{.FSInfo.Device}}{{if .FSInfo.Model}} · {{.FSInfo.Model}}{{end}}
</div>
{{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:
type AppBackupInfo struct {
// ... existing ...
StorageLabel string // NEW: resolved from registered storage paths
}
In template (backup section):
<div class="app-backup-header-right">
{{if .StorageLabel}}<span class="meta-badge meta-badge-storage">{{.StorageLabel}}</span>{{end}}
<span class="app-backup-size mono">{{.HDDSizeHuman}}</span>
</div>
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 fromissuestowarnings - Translate all storage health messages to Hungarian
- Verify monitoring page template differentiates warning vs issue banner colors
- Add
.monitoring-banner-warnCSS 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/labelroute + 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
StoragePathViewwithAppDetails []StorageAppDetail - Populate in settings handler (scan app.yaml + HDD data)
- Replace count with expandable
<details>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/StorageLabelto 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
DeployStoragePathwith free space data - Populate via
GetDiskUsagein deploy handler - Update dropdown option text +
data-free-percentattr - Add JS for low-space warning
- Test: Dropdown shows "234 GB szabad" → select near-full → warning
Step 6: Filesystem info
- Add
GetFSInfo()tomounts_linux.go(usingfindmnt) - Add non-Linux stub
- Add to
StoragePathView+ template - Test: Settings → "ext4 · /dev/sdb1 · WD Elements"
Step 7: Backup page storage context
- Add
StorageLabeltoAppBackupInfo - 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 messagescontroller/internal/settings/settings.go— Step 2:SetStorageLabel()controller/internal/system/mounts_linux.go— Step 6:GetFSInfo(),FSInfostructcontroller/internal/system/mounts_other.go— Step 6:GetFSInfo()stubcontroller/internal/backup/appdata.go— Step 7:StorageLabelinAppBackupInfocontroller/internal/web/handlers.go— Steps 1-7: flash parsing, label handler, deploy/settings/stacks/backup data enrichmentcontroller/internal/web/server.go— Step 2: register/settings/storage/labelroutecontroller/internal/web/templates/settings.html— Steps 1-3,6: flash, edit UI, app details, FS infocontroller/internal/web/templates/stacks.html— Step 4: storage badgecontroller/internal/web/templates/deploy.html— Step 5: free space in dropdown, warningcontroller/internal/web/templates/backups.html— Step 7: storage label badgecontroller/internal/web/templates/monitoring.html— Step 0: warning vs issue banner differentiationcontroller/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