# REPORT — NAS network storage Part A2 (controller registry/UI/health), v0.92.0 **Date:** 2026-06-30 **Task:** `TASK.md` — NAS Part A2 (+ hub dispatcher critical-severity fix, "Part 0"). Implementation class. ## Baselines | Repo | before → after | |------|----------------| | felhom-controller | v0.91.0 → **v0.92.0** (commit `364dc50`) | | felhom.eu (hub, Part 0) | v0.23.0 → **v0.24.0** (code `0ff1d3c`, manifest `87f0794`) | | felhom-agent | v0.50.0 (A1, unchanged) | ## What was built The controller side of NAS network storage: an operator adds a customer's NAS share and points a media app at it, all via the UI — proxying to the validated agent foundation (A1 `/netstorage/*`). A NAS is a **distinct "network" storage kind**, NOT a drive: no enroll/eject/decommission/migrate/wipe/SMART. ### Part 0 — hub dispatcher (felhom.eu, v0.24.0, commit `0ff1d3c`) - `internal/notify/dispatcher.go` `ProcessEvent`: routed only `warning`/`error` → a `critical`-severity event was **silently dropped**. Now routes `warning`/`error`/`critical` (`severityNotifies`); an unrecognized severity is **logged**, not dropped; `info` stays a deliberate non-notify. - `host_disk_critical` now emits its **natural `critical` severity** (`FormatOperatorEmail` styles it 🔴). - **Latent crash guard:** `processCustomer` dereferenced `GetNotificationPrefs`, which returns `(nil,nil)` for a customer with no notification row — an event for such a customer would have **panicked/crashed the hub**. Now guards `prefs == nil`. - Verified safe first (§9 rule 5): **no** controller event emits `critical` (all info/warning/error), so no surprise alert volume. Tests: `severityNotifies` + companion (pre-fix predicate drops `critical`), ProcessEvent routes `critical`, unknown logged, info silent. ### A2 — controller (v0.92.0, commit `364dc50`) - `internal/agentapi/client.go`: `AddNetStorage`/`ListNetStorage`/`RemoveNetStorage` + `NetworkMountStatus` mirror. **SMB credential passes straight through to the agent (0600 file); never persisted by the controller.** - `internal/settings/settings.go`: `StoragePath.Kind` discriminator + network descriptors (no password); `IsNetwork()`/`IsNetworkStoragePath()`; `NetworkMountRoot`. - `internal/web/netstorage_handlers.go` (NEW): `POST /api/storage/netstorage/{add,remove}` + `GET`; registers a Kind=network `StoragePath`; merges the agent's live per-share health. - **Kind-gating (safety centerpiece):** `refuseNetworkLifecycle` blocks the drive ops (eject/decommission/ migrate/wipe) on a network path; the drive-absent gate (`planDriveGates`) and the missing-storage surface **skip network paths** → an `unreachable` NAS is a recoverable warning, never the drive stop-cascade. `networkStorageWarnings` drives a distinct app-card badge. - UI (`settings.html`): a "Hálózati tárhely (NAS)" section (add form NFS/SMB, health badges, remove); shares auto-selectable as a media app's `HDD_PATH`. ## Tests — both repos `go build ./... && go vet ./... && go test ./...` GREEN - agentapi: round-trip (creds forwarded, mapping/protocol forwarded, health states; `idle`/`ok` not degraded, `unreachable` degraded). - registry **Kind-gate companion**: the drive-lifecycle handlers refuse a network path (400 before any agent/stackMgr use); a DRIVE path is NOT gate-refused (proves the gate keys on Kind, not the path). - **`unreachable`≠`missing` companion**: a network path is never "missing"; the drive gate produces NO Stop action for an absent network path, while it DOES for an absent drive path under the same parent. - Part 0 dispatcher tests (above). ## Live e2e validation (against the isolated sim NAS) **Method:** the demo controller has no password set, so CSRF is skipped — I drove the **exact UI endpoints** (`/api/storage/netstorage/*`) via curl from inside guest 9201's controller container (the full controller server pipeline runs; an acceptable proxy per CLAUDE.md). The storage-page badge was verified from the rendered HTML. **Sim NAS** = a throwaway privileged LXC (NFS, `anonuid/anongid=101000`, own config) on felhom-pve — never touched production storage; destroyed after. - **A — add via controller:** `POST /api/storage/netstorage/add` (nfs) → `registered:true`, registered as `/mnt/felhom-drives/media kind=network sched=True proto=nfs`; the agent mounted it; `GET` showed health `idle` then `ok` after first access. The share is absent from `/api/disks` (distinct class). - **B — media app reads from NAS:** jellyfin (uid 1000, healthy) bound to the controller-registered NAS path **read the media library** (`Movies/SampleMovie (2024).mkv` owned `1000:1000` via the +100000 idmap, plus `readme.txt`). (jellyfin isn't in the app catalog — run as a raw uid-1000 container to exercise the real A1 chain; see Observations.) - **C — NAS disappears (no cascade):** black-holed the sim → `GET /api/storage/netstorage` health `unreachable` in ~2s; the settings page rendered the "Nem elérhető" badge in the "Hálózati tárhely (NAS)" section; **guest + controller + jellyfin stayed healthy**; over a drive-gate tick the network path was **NOT** marked disconnected and **no app was stopped** (cascade prevented). On NAS return → health `ok`, no remount. - **D — remove via controller:** `POST /api/storage/netstorage/remove` → `removed:true`, list empty, host units gone + unmounted. - **E — Part 0:** unit-validated (critical routes; companion proves the pre-fix drop); hub v0.24.0 deployed via ArgoCD (Synced/Healthy), `host_disk` now emits natural `critical`. (Not re-fired live — the demo host root is at 24% after the prior task's prune, below threshold.) Deployed: hub v0.24.0 (ArgoCD sync, Synced/Healthy); controller v0.92.0 on guest 9201 (`docker ps … :0.92.0 Up (healthy)`, clean logs). Sim NAS + test jellyfin torn down; `/mnt/felhom-drives` back to only the real drives. ## Observations / limitations - **Part B (restic-over-SFTP NAS backup target)** still pending — separate task. - **No media app in the catalog** (jellyfin/immich/…): A2's HDD_PATH selectability is wired (the NAS path is Schedulable and appears in the dropdown), but the full "deploy a catalog media app onto the NAS via the deploy flow" couldn't run; jellyfin was a raw uid-1000 container for the read proof. Adding jellyfin to `app-catalog-felhom.eu` is a small follow-up that would make the app-card badge live-exercisable too. - **Mixed-uid share sharing** is a documented v1 limit (a share's +100000 uid mapping is fixed at add-time → one app or same-uid apps). - **Write apps on a soft NFS NAS:** a mid-disappearance in-flight file can truncate (spike) → prefer atomic-write apps or SSD-staging; v1 test used a READ app (jellyfin). - Per-storage worst-fill alerting (hub host_disk follow-on) still pending. No secrets in any committed file. SMB/NFS test credentials were throwaway and out-of-band only.