467 lines
17 KiB
Markdown
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 |