Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
6.8 KiB
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.goProcessEvent: routed onlywarning/error→ acritical-severity event was silently dropped. Now routeswarning/error/critical(severityNotifies); an unrecognized severity is logged, not dropped;infostays a deliberate non-notify.host_disk_criticalnow emits its naturalcriticalseverity (FormatOperatorEmailstyles it 🔴).- Latent crash guard:
processCustomerdereferencedGetNotificationPrefs, which returns(nil,nil)for a customer with no notification row — an event for such a customer would have panicked/crashed the hub. Now guardsprefs == 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 dropscritical), ProcessEvent routescritical, unknown logged, info silent.
A2 — controller (v0.92.0, commit 364dc50)
internal/agentapi/client.go:AddNetStorage/ListNetStorage/RemoveNetStorage+NetworkMountStatusmirror. SMB credential passes straight through to the agent (0600 file); never persisted by the controller.internal/settings/settings.go:StoragePath.Kinddiscriminator + network descriptors (no password);IsNetwork()/IsNetworkStoragePath();NetworkMountRoot.internal/web/netstorage_handlers.go(NEW):POST /api/storage/netstorage/{add,remove}+GET; registers a Kind=networkStoragePath; merges the agent's live per-share health.- Kind-gating (safety centerpiece):
refuseNetworkLifecycleblocks the drive ops (eject/decommission/ migrate/wipe) on a network path; the drive-absent gate (planDriveGates) and the missing-storage surface skip network paths → anunreachableNAS is a recoverable warning, never the drive stop-cascade.networkStorageWarningsdrives 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'sHDD_PATH.
Tests — both repos go build ./... && go vet ./... && go test ./... GREEN
- agentapi: round-trip (creds forwarded, mapping/protocol forwarded, health states;
idle/oknot degraded,unreachabledegraded). - 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≠missingcompanion: 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;GETshowed healthidlethenokafter 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).mkvowned1000:1000via the +100000 idmap, plusreadme.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/netstoragehealthunreachablein ~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 → healthok, 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_disknow emits naturalcritical. (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.euis 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.