Files
deploy-felhom-compose/TASK.md
T

467 lines
17 KiB
Markdown

# 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}}
<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:
```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}}<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:
```html
<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:
```html
<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
```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
<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
```html
{{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
```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
<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
```html
<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
```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 <path>
// /sys/block/<dev>/device/model for disk model
}
```
Non-Linux stub returns nil.
### 6.2 Template
Below the disk usage bar:
```html
{{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`:
```go
type AppBackupInfo struct {
// ... existing ...
StorageLabel string // NEW: resolved from registered storage paths
}
```
In template (backup section):
```html
<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 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 `<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`/`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