diff --git a/TASK.md b/TASK.md
index b42d91d..731de42 100644
--- a/TASK.md
+++ b/TASK.md
@@ -1,686 +1,467 @@
-# TASK: Phase A — Storage Paths Foundation & Backup Toggle Fix
+# TASK: Phase B — Storage Management UI Polish & Health Severity Fix
-**Version target:** controller 0.9.0
-**Repos:** `deploy-felhom-compose` (controller), `app-catalog-felhom.eu` (compose mount change)
+**Version target:** controller 0.10.0
+**Repo:** `deploy-felhom-compose` (controller)
## Overview
-Per-app backup toggles (implemented in v0.8.0) don't appear on the backup page. Root cause is two-layered:
+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.
-1. **`controller.yaml` has no `paths.hdd_path`** → `m.cfg.Paths.HDDPath` is `""` → `ParseComposeHDDMounts` returns nil → `DiscoverAppData` finds zero HDD data → no backup toggles shown
-2. **Even with a global hdd_path, the parser uses ONE path** → apps deployed with a different `HDD_PATH` (e.g., `/mnt/hdd_placeholder` vs `/mnt/hdd_1`) won't match
+**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.
-The fix: stop relying on a single global HDD path. Instead, read each app's **own** `HDD_PATH` from its `app.yaml` env section, and introduce a storage paths registry in `settings.json` that auto-discovers paths from deployed apps.
+Phase B then polishes the UI and fills gaps:
-This phase also lays the foundation for multi-storage management (Phase B: UI polish, Phase C: migration wizard).
+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
---
-## 1. Root Cause Analysis
+## 0. Health Severity Fix (URGENT — do first)
-### Current broken flow
+### 0.1 Problem
-```
-controller.yaml → paths.hdd_path = "" (missing)
- ↓
-backup.Manager.stackProvider.GetStackHDDMounts(name)
- → stacks.ParseComposeHDDMounts(composePath, hddPath="")
- → hddPath == "" → return nil ← BUG: exits early
- ↓
-DiscoverAppData → info.HasHDDData = false for ALL apps
- ↓
-backups.html → {{if .HasHDDData}} → false → no checkbox rendered
-```
-
-### Second layer (would hit even with hdd_path set)
-
-```
-controller.yaml → paths.hdd_path = "/mnt/hdd_1"
- ↓
-ParseComposeHDDMounts(composePath, "/mnt/hdd_1")
- → reads compose: "- ${HDD_PATH}/storage/immich:/usr/src/app/upload"
- → resolves ${HDD_PATH} → "/mnt/hdd_1/storage/immich"
- → BUT app.yaml has HDD_PATH="/mnt/hdd_placeholder"
- → actual data is at /mnt/hdd_placeholder/storage/immich
- → os.Stat("/mnt/hdd_1/storage/immich") → not found
- → path.Exists = false, SizeBytes = 0
-```
-
-### Correct approach
-
-Read each app's actual `HDD_PATH` from its `app.yaml` env section. The compose template uses `${HDD_PATH}` which gets resolved at `docker compose up` time from the environment. We need to resolve it the same way during discovery.
-
----
-
-## 2. Storage Paths Registry
-
-### 2.1 New struct in `settings.go`
+`checkStoragePaths()` in `healthcheck.go` currently classifies non-mount-point as an **issue**:
```go
-// StoragePath represents a registered external storage location.
-type StoragePath struct {
- Path string `json:"path"` // e.g., "/mnt/hdd_1"
- Label string `json:"label,omitempty"` // e.g., "Külső HDD 1TB"
- IsDefault bool `json:"is_default,omitempty"` // new apps use this by default
- Schedulable bool `json:"schedulable"` // whether new apps can be deployed here
- AddedAt string `json:"added_at"` // RFC3339
+// 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))
}
```
-### 2.2 Add to Settings struct
+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
-type Settings struct {
- // ... existing fields ...
-
- // Storage paths registry
- StoragePaths []StoragePath `json:"storage_paths,omitempty"`
+// 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))
}
```
-### 2.3 Helper methods on Settings
+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
-// GetStoragePaths returns all registered storage paths.
-func (s *Settings) GetStoragePaths() []StoragePath
+// Path not accessible
+warnings = append(warnings, fmt.Sprintf("Adattároló nem elérhető: %s", sp.Path))
-// GetDefaultStoragePath returns the default storage path, or "" if none.
-func (s *Settings) GetDefaultStoragePath() string
+// 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))
-// GetSchedulableStoragePaths returns paths where new apps can be deployed.
-func (s *Settings) GetSchedulableStoragePaths() []StoragePath
+// Disk usage critical (≥95%) — this stays as issue
+issues = append(issues, fmt.Sprintf("Adattároló majdnem megtelt: %s (%.0f%%)", sp.Path, di.UsedPercent))
-// AddStoragePath registers a new storage path after validation.
-func (s *Settings) AddStoragePath(path, label string, isDefault bool) error
-
-// RemoveStoragePath removes a path only if no apps reference it.
-// Returns error with list of apps using this path if removal is blocked.
-func (s *Settings) RemoveStoragePath(path string, appChecker func(string) []string) error
-
-// SetDefaultStoragePath changes which path is the default.
-func (s *Settings) SetDefaultStoragePath(path string) error
-
-// SetSchedulable enables/disables a path for new deployments.
-func (s *Settings) SetSchedulable(path string, schedulable bool) error
+// Disk usage high (≥90%) — warning
+warnings = append(warnings, fmt.Sprintf("Adattároló használat magas: %s (%.0f%%)", sp.Path, di.UsedPercent))
```
-### 2.4 Validation rules (in AddStoragePath)
+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.
-1. **Must be a real mount point** — compare device ID of path vs parent using `syscall.Stat`. Reject paths that are just directories on the boot SSD. Error message: "Ez az útvonal nem külön csatlakoztatott meghajtó. Adatok az SSD-re kerülnének."
-2. **Must exist and be a directory** — `os.Stat` check
-3. **Must be writable** — attempt to create + remove a temp file
-4. **No overlapping paths** — reject if new path is a parent or child of an existing path (e.g., `/mnt/hdd_1` and `/mnt/hdd_1/subdir`)
-5. **No duplicates** — reject if path already registered (normalized with `filepath.Clean`)
-6. **Must be under `/mnt/`** — soft warning, not hard block (log a WARN for edge cases)
+### 0.5 Monitoring page banner color
-### 2.5 Auto-discovery on startup
-
-On controller startup, if `StoragePaths` is empty in settings.json, auto-discover from deployed apps:
-
-```go
-func (s *Settings) AutoDiscoverStoragePaths(stacksDir string, fallbackHDDPath string, logger *log.Logger) {
- if len(s.StoragePaths) > 0 {
- return // already configured, don't override
- }
-
- // Scan all deployed apps' app.yaml for HDD_PATH values
- seen := map[string]bool{}
- entries, _ := os.ReadDir(stacksDir)
- for _, e := range entries {
- if !e.IsDir() { continue }
- appCfg := LoadAppConfig(filepath.Join(stacksDir, e.Name()))
- if appCfg == nil || !appCfg.Deployed { continue }
- if hddPath, ok := appCfg.Env["HDD_PATH"]; ok && hddPath != "" {
- cleaned := filepath.Clean(hddPath)
- if !seen[cleaned] {
- seen[cleaned] = true
- }
- }
- }
-
- // Also use controller.yaml paths.hdd_path as fallback seed
- if fallbackHDDPath != "" {
- cleaned := filepath.Clean(fallbackHDDPath)
- if !seen[cleaned] {
- seen[cleaned] = true
- }
- }
-
- // Register discovered paths
- isFirst := true
- for path := range seen {
- sp := StoragePath{
- Path: path,
- Label: inferStorageLabel(path),
- IsDefault: isFirst,
- Schedulable: true,
- AddedAt: time.Now().UTC().Format(time.RFC3339),
- }
- s.StoragePaths = append(s.StoragePaths, sp)
- isFirst = false
- }
-
- if len(s.StoragePaths) > 0 {
- s.Save()
- logger.Printf("[INFO] Auto-discovered %d storage path(s)", len(s.StoragePaths))
- for _, sp := range s.StoragePaths {
- logger.Printf("[INFO] %s (%s) default=%v", sp.Path, sp.Label, sp.IsDefault)
- }
- }
-}
-```
-
-Helper `inferStorageLabel(path)`:
-- `/mnt/hdd_1` → "Külső tárhely (hdd_1)"
-- `/mnt/hdd_placeholder` → "Külső tárhely (hdd_placeholder)"
-- Anything else → path basename
-
-Note: `LoadAppConfig` is in the `stacks` package. To avoid circular imports, either pass a scanner function or do the scanning in `main.go` and pass the discovered paths in. Use whichever approach avoids import issues.
-
----
-
-## 3. Fix `GetStackHDDMounts` — Per-App HDD_PATH Resolution
-
-### 3.1 Change the adapter
-
-Currently:
-```go
-func (a *stackAdapter) GetStackHDDMounts(name string) []string {
- s, ok := a.mgr.GetStack(name)
- if !ok { return nil }
- return stacks.ParseComposeHDDMounts(s.ComposePath, a.hddPath) // ← uses global path
-}
-```
-
-Change to:
-```go
-func (a *stackAdapter) GetStackHDDMounts(name string) []string {
- s, ok := a.mgr.GetStack(name)
- if !ok { return nil }
-
- // Priority 1: Read the app's own HDD_PATH from its app.yaml
- stackDir := filepath.Dir(s.ComposePath)
- appCfg := stacks.LoadAppConfig(stackDir)
- if appCfg != nil && appCfg.Env["HDD_PATH"] != "" {
- return stacks.ParseComposeHDDMounts(s.ComposePath, appCfg.Env["HDD_PATH"])
- }
-
- // Priority 2: Try all registered storage paths (fallback)
- var allMounts []string
- seen := map[string]bool{}
- for _, sp := range a.getStoragePaths() {
- mounts := stacks.ParseComposeHDDMounts(s.ComposePath, sp.Path)
- for _, m := range mounts {
- if !seen[m] {
- seen[m] = true
- allMounts = append(allMounts, m)
- }
- }
- }
- return allMounts
-}
-```
-
-### 3.2 Update adapter struct
-
-```go
-type stackAdapter struct {
- mgr *stacks.Manager
- getStoragePaths func() []settings.StoragePath // getter from settings
-}
-```
-
-Wire in `main.go`:
-```go
-adapter := &stackAdapter{
- mgr: stackMgr,
- getStoragePaths: func() []settings.StoragePath { return settingsMgr.GetStoragePaths() },
-}
-```
-
-Remove the old `hddPath string` field from the adapter.
-
-### 3.3 `resolveAppBackupPaths()` in backup Manager
-
-No changes needed — it already calls `m.stackProvider.GetStackHDDMounts(stackName)` which we're fixing above. The fix propagates automatically.
-
----
-
-## 4. Controller Docker Compose Mount Change
-
-### 4.1 Change in `controller/docker-compose.yml`
-
-Replace:
-```yaml
- - ${HDD_PATH:-/mnt/hdd_placeholder}:${HDD_PATH:-/mnt/hdd_placeholder}:ro
-```
-
-With:
-```yaml
- # All external storage — covers /mnt/* paths for multi-storage support + restore
- - /mnt:/mnt:rw
-```
-
-**Why `:rw`:** Required for restore feature (already implemented). Also needed for mount-point validation (write test in AddStoragePath).
-
-**Why `/mnt:/mnt`:** All standard storage mounts are under `/mnt`. Covers current and future drives with one entry. No controller restart when adding new drives.
-
-### 4.2 Also update `docker-setup.sh`
-
-If `docker-setup.sh` generates the controller compose, update it to emit `/mnt:/mnt:rw` instead of the old `${HDD_PATH}` line.
-
----
-
-## 5. Deploy Page: Storage Path Dropdown
-
-### 5.1 When app has a `path` type deploy field (HDD_PATH)
-
-Currently renders as a plain text input. Change to dropdown when storage paths exist:
+Currently the monitoring page shows issues as red banners. Warnings should be yellow/amber:
```html
-{{if eq .Type "path"}}
- {{if $.StoragePaths}}
-
- {{else}}
-
-
- Nincs regisztrált adattároló. Adjon hozzá egyet a Beállítások oldalon.
-
- {{end}}
+{{range .Warnings}}
+
+ ⚠️ {{.}}
+
{{end}}
```
-### 5.2 Pass storage paths to deploy page template data
+Check if the monitoring template already differentiates issue vs warning banners. If not, add CSS class:
-In the deploy handler:
-```go
-data["StoragePaths"] = settingsMgr.GetSchedulableStoragePaths()
-```
-
-### 5.3 Already-deployed: show current path read-only
-
-When `AlreadyDeployed == true`, the field is disabled. Show the actual `HDD_PATH` from `app.yaml` as the displayed value (existing behavior works as-is with a disabled select or text input).
-
-### 5.4 Edge case: no schedulable paths but app needs HDD
-
-If `resources.needs_hdd: true` and zero schedulable paths:
-- Show warning: "Nincs elérhető adattároló. Csatlakoztasson külső meghajtót és adja hozzá a Beállítások oldalon."
-- Disable deploy button (add `data-needs-storage="true"` to form, JS disables submit)
-
----
-
-## 6. Storage Path Monitoring Integration
-
-### 6.1 Periodic mount-point check
-
-Add to existing system health check (runs every `system_health_interval`, default 5m):
-
-```go
-func checkStoragePaths(paths []settings.StoragePath, logger *log.Logger) []string {
- var warnings []string
- for _, sp := range paths {
- // Check 1: path exists and is directory
- fi, err := os.Stat(sp.Path)
- if err != nil {
- warnings = append(warnings, fmt.Sprintf("Adattároló nem elérhető: %s", sp.Path))
- continue
- }
- if !fi.IsDir() {
- warnings = append(warnings, fmt.Sprintf("Adattároló nem mappa: %s", sp.Path))
- continue
- }
-
- // Check 2: still a real mount point (not writing to SSD)
- if !isMountPoint(sp.Path) {
- warnings = append(warnings,
- fmt.Sprintf("FIGYELEM: %s nincs felcsatolva (leválasztva?) — "+
- "az adatok az SSD-re íródnának!", sp.Path))
- }
-
- // Check 3: disk space (reuse existing threshold config)
- usage := getDiskUsage(sp.Path)
- if usage.UsedPercent >= 90 {
- warnings = append(warnings,
- fmt.Sprintf("Adattároló majdnem tele: %s (%.0f%%)", sp.Path, usage.UsedPercent))
- }
- }
- return warnings
+```css
+.monitoring-banner-warn {
+ background: rgba(255, 193, 7, 0.15);
+ border-left: 4px solid var(--yellow);
+ color: var(--yellow);
}
```
-### 6.2 `isMountPoint()` implementation
+---
-New file: `controller/internal/system/mounts.go`
+## 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
-// IsMountPoint checks if a path is a mount point by comparing device IDs.
-// Returns true if path is on a different device than its parent.
-func IsMountPoint(path string) bool {
- var pathStat, parentStat syscall.Stat_t
- if err := syscall.Stat(path, &pathStat); err != nil {
- return false
- }
- parent := filepath.Dir(path)
- if err := syscall.Stat(parent, &parentStat); err != nil {
- return false
- }
- return pathStat.Dev != parentStat.Dev
+if msg := r.URL.Query().Get("storage_msg"); msg == "success" {
+ data["StorageSuccess"] = r.URL.Query().Get("storage_detail")
}
```
-Platform-specific: needs build tag `//go:build linux` (consistent with existing system package). Add stub for non-Linux that always returns true.
+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"
-### 6.3 Surface warnings
+### 1.3 Template
-- Add storage warnings to monitoring page warnings section (existing infrastructure)
-- Include in hub report `SystemWarnings` field (already exists)
-- Fire `disk_warning` notification event for enabled customers
+Add to settings.html (after `StorageError`):
+```html
+{{if .StorageSuccess}}
{{.StorageSuccess}}
{{end}}
+```
---
-## 7. Beállítások Page: Storage Section
+## 2. Edit Storage Path Labels
-Add between "Rendszer konfiguráció" and "Jelszó módosítás" sections.
+### 2.1 UI: Inline edit
-### 7.1 Template: "Adattárolók" section
+Add edit button next to label. JS toggles between display and inline form:
```html
-
-
-
Adattárolók
-
- Külső meghajtók, ahol az alkalmazások felhasználói adatai tárolódnak.
-
+```
-### Step 5: Controller compose mount change
-- Change `controller/docker-compose.yml`: `/mnt:/mnt:rw`
-- Update `docker-setup.sh` controller compose generation
-- **Test:** Recreate controller container → verify both `/mnt/hdd_1` and `/mnt/hdd_placeholder` accessible inside container
-
-### Step 6: Beállítások storage section
-- Add "Adattárolók" section to `settings.html`
-- Add handlers: `POST /settings/storage/add`, `/remove`, `/default`, `/schedulable`
-- Add `StoragePathView` type for template data with disk info + app count
-- Pass storage data to settings page handler
-- Add CSS for storage path items
-- **Test:** Add path via UI → validation catches non-mount-point. Remove path with apps → blocked with app list. Set default → badge updates.
-
-### Step 7: Deploy page dropdown
-- Modify `deploy.html`: path field becomes dropdown when storage paths exist
-- Pass `StoragePaths` to deploy page template data
-- Fall back to text input if no paths registered
-- Block deploy if app needs HDD but no schedulable paths
-- **Test:** Deploy page for Immich → dropdown shows registered paths. Already-deployed shows current path read-only.
-
-### Step 8: Storage monitoring integration
-- Add `checkStoragePaths()` to system health check
-- Surface warnings on monitoring page
-- Include in hub report
-- Fire `disk_warning` notification for unmounted drives
-- **Test:** (Simulated) If path doesn't exist → warning appears on monitoring page within one health check cycle
-
-### Step 9: Cleanup & version bump
-- Deprecate `paths.hdd_path` in controller.yaml.example
-- Create new CHANGELOG.md, changelogs will be updated there, not in CONTEXT.md. CONTEXT.md will only have information about architecture decisions, roadmap, information about the project
-- Regenerate CONTEXT.md with current architecture, deparating different sections, with detailed descriptions how it should work, what is planned, what is the architecture. Sections like: Storage management, Notifications, Backup management, App management, Monitoring, Infra, Settings management, etc-etc..
-- Update CONTEXT.md / CHANGELOG.md / CLAUDE.md
-- Bump to 0.9.0
-- Build + deploy
-- **Test end-to-end:** Auto-discover → backup toggles visible → enable Immich backup → manual backup → verify HDD data in restic snapshot → storage management in Beállítások works
+Populate by matching each app's HDD_PATH prefix against registered paths during `DiscoverAppData()`.
---
-## 10. Files to Create / Modify
+## 8. Implementation Steps
-### New files:
-- `controller/internal/system/mounts.go` — `IsMountPoint()`, `IsWritable()`, `PathsOverlap()` + non-Linux stub
-- (No new packages — StoragePath goes in existing `settings` package)
+### 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/settings/settings.go` — `StoragePath` struct, `StoragePaths` field, all getter/setter methods, `AutoDiscoverStoragePaths()`, `inferStorageLabel()`, validation logic
-- `controller/cmd/controller/main.go` — Wire auto-discovery, update stackAdapter struct (replace `hddPath` with `getStoragePaths`), update adapter constructor
-- `controller/internal/web/handlers.go` — Storage management handlers (`/settings/storage/*`), pass StoragePaths to deploy + settings templates
-- `controller/internal/web/server.go` — Register new storage routes
-- `controller/internal/web/templates/settings.html` — New "Adattárolók" section
-- `controller/internal/web/templates/deploy.html` — Path field → dropdown, edge case warnings
-- `controller/internal/web/templates/style.css` — Storage path item styles, badges
-- `controller/internal/monitoring/health.go` (or wherever health checks live) — Add `checkStoragePaths()`
-- `controller/docker-compose.yml` — `/mnt:/mnt:rw`
-- `controller/configs/controller.yaml.example` — Deprecation comment
-- `docker-setup.sh` — Update controller compose generation
+- `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
---
-## 11. Design Decisions & Notes
+## 10. Design Decisions
-### Why settings.json, not controller.yaml?
-controller.yaml is operator-controlled (read-only from customer perspective). User-configurable state lives in settings.json. Clear separation: operator configures infrastructure, customer configures preferences.
+### 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 auto-discover from app.yaml?
-Existing deployments already have apps with HDD_PATH set. Auto-discovery makes the upgrade seamless — backup toggles appear without manual intervention after the update.
+### 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 `/mnt:/mnt:rw` instead of individual mounts?
-Single mount covers all current and future storage devices. No controller restart when adding new drives. Required for restore + validation. Trade-off: broader access, but controller already has Docker socket access and is a trusted component.
+### Why query param flashes instead of session-based?
+No session store — consistent with backup page pattern. Stateless, simple.
-### Why validate mount points?
-If a USB drive is disconnected but the empty directory remains (created by Docker or `nofail` fstab), data silently writes to the SSD boot drive. Mount-point check (device ID comparison) catches this. This is the #1 gotcha for home server setups.
-
-### Why no overlapping paths?
-If `/mnt/hdd_1` and `/mnt/hdd_1/storage` are both registered, discovery counts files twice and backup may include data twice. One entry per physical mount point keeps it simple.
-
-### Why fallback chain in GetStackHDDMounts?
-1. App's own HDD_PATH from app.yaml (most accurate — the actual deployed value)
-2. Try all registered storage paths (handles edge cases: missing app.yaml, new template before deploy)
-Ensures backup discovery works even in degraded states.
-
-### Why `Schedulable` toggle?
-An "unschedulable" path means existing apps stay put but no new apps can use it. Useful for drives that are filling up or being deprecated — prevents new deployments while allowing existing apps to continue operating.
-
-### Future phases (NOT in scope)
-- **Phase B:** Storage management UI polish — disk usage bars per path, per-app storage column on apps page
-- **Phase C:** Migration wizard — "Mozgatás" button per app, rsync with progress reporting, automatic app.yaml HDD_PATH update + restart, old data cleanup option
\ No newline at end of file
+### 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
\ No newline at end of file