controller v0.92.0: NAS network storage Part A2 (registry + UI + per-share health)
Controller-side of NAS network storage, proxying to agent A1 /netstorage/*. Distinct 'network' storage kind (no drive lifecycle); add/list/remove + per-share health UI; unreachable NAS is a recoverable warning, never the drive missing/stop cascade; SMB creds pass through to the agent, never persisted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
@@ -155,6 +155,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["SettingsWarning"] = s.settings.LoadWarning // non-empty if settings.json was recovered from corruption
|
||||
data["Stacks"] = deployedStacks
|
||||
data["MissingStorage"] = s.missingStorageMap(deployedStacks)
|
||||
data["NetworkWarnings"] = s.networkStorageWarnings(deployedStacks) // NAS unreachable — recoverable warning, not "missing"
|
||||
data["RunningCount"] = running
|
||||
data["StoppedCount"] = stopped
|
||||
data["TotalCount"] = len(stackList)
|
||||
@@ -202,6 +203,7 @@ func (s *Server) stacksHandler(w http.ResponseWriter, r *http.Request) {
|
||||
allStacks := s.stackMgr.GetStacks()
|
||||
data["Stacks"] = allStacks
|
||||
data["MissingStorage"] = s.missingStorageMap(allStacks)
|
||||
data["NetworkWarnings"] = s.networkStorageWarnings(allStacks) // NAS unreachable — recoverable warning, not "missing"
|
||||
|
||||
// Build storage label lookup for deployed apps
|
||||
storageLabels := make(map[string]string) // stack name → storage label
|
||||
@@ -915,6 +917,11 @@ func (s *Server) settingsData() map[string]interface{} {
|
||||
}
|
||||
var storageViews []StoragePathView
|
||||
for _, sp := range storagePaths {
|
||||
// NAS network shares are rendered in their OWN section (NetworkStoragePaths) with the agent's
|
||||
// per-share health — never in the physical-drive list (which offers eject/decommission/wipe).
|
||||
if sp.IsNetwork() {
|
||||
continue
|
||||
}
|
||||
view := StoragePathView{
|
||||
StoragePath: sp,
|
||||
StoppedApps: sp.StoppedStacks,
|
||||
@@ -942,6 +949,9 @@ func (s *Server) settingsData() map[string]interface{} {
|
||||
storageViews = append(storageViews, view)
|
||||
}
|
||||
data["StoragePaths"] = storageViews
|
||||
// NAS network storage (Part A2) — separate section with the agent's per-share health (ok/idle/
|
||||
// unreachable/unknown). Distinct from the physical-drive list above; no drive lifecycle actions.
|
||||
data["NetworkStoragePaths"] = s.networkStorageItems(context.Background())
|
||||
|
||||
// Recovery info for emergency section
|
||||
data["RetrievalPassword"] = s.settings.GetRetrievalPassword()
|
||||
@@ -1210,6 +1220,12 @@ func (s *Server) missingStorageLabel(hddPath string) (string, bool) {
|
||||
}
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
if sp.Path == hddPath {
|
||||
// A NAS network path is NEVER "missing": its availability is the agent's per-share liveness
|
||||
// (surfaced as a recoverable warning by networkStorageWarnings), NOT the drive missing/stop
|
||||
// cascade. An `unreachable` NAS must not look like a removed drive.
|
||||
if sp.IsNetwork() {
|
||||
return "", false
|
||||
}
|
||||
if sp.Decommissioned || sp.Disconnected {
|
||||
return s.settings.GetStorageLabel(hddPath), true
|
||||
}
|
||||
@@ -1219,6 +1235,65 @@ func (s *Server) missingStorageLabel(hddPath string) (string, bool) {
|
||||
return s.settings.GetStorageLabel(hddPath), true // not in registry → its drive is gone
|
||||
}
|
||||
|
||||
// networkStorageWarnings returns stack-name → share label for every deployed app whose HDD_PATH is a NAS
|
||||
// network path the agent currently reports `unreachable`. This is a RECOVERABLE warning ("hálózati
|
||||
// tárhely nem elérhető"), explicitly NOT the drive missing/stop-cascade — the app keeps running and the
|
||||
// badge clears when the NAS returns. Best-effort: an agent error or no network paths → no warnings.
|
||||
func (s *Server) networkStorageWarnings(list []stacks.Stack) map[string]string {
|
||||
out := map[string]string{}
|
||||
if s.settings == nil || s.stackMgr == nil {
|
||||
return out
|
||||
}
|
||||
netPaths := map[string]settings.StoragePath{}
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
if sp.IsNetwork() {
|
||||
netPaths[sp.Path] = sp
|
||||
}
|
||||
}
|
||||
if len(netPaths) == 0 {
|
||||
return out
|
||||
}
|
||||
agent, err := s.agentClient()
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
mounts, err := agent.ListNetStorage(ctx)
|
||||
if err != nil {
|
||||
s.logger.Printf("[WARN] [web] network storage health unavailable for warnings: %v", err)
|
||||
return out
|
||||
}
|
||||
unreachable := map[string]string{} // path → label
|
||||
for _, m := range mounts {
|
||||
if !m.Unreachable() { // only `unreachable` is degraded; `idle`/`ok` are benign
|
||||
continue
|
||||
}
|
||||
p := settings.NetworkMountRoot + "/" + m.Name
|
||||
if sp, ok := netPaths[p]; ok {
|
||||
lbl := sp.Label
|
||||
if lbl == "" {
|
||||
lbl = m.Name
|
||||
}
|
||||
unreachable[p] = lbl
|
||||
}
|
||||
}
|
||||
if len(unreachable) == 0 {
|
||||
return out
|
||||
}
|
||||
for _, st := range list {
|
||||
if !st.Deployed {
|
||||
continue
|
||||
}
|
||||
if cfg := s.stackMgr.LoadAppConfigByName(st.Name); cfg != nil {
|
||||
if lbl, bad := unreachable[cfg.Env["HDD_PATH"]]; bad {
|
||||
out[st.Name] = lbl
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// missingStorageMap returns stack-name → storage label for every deployed app whose data drive is
|
||||
// currently unavailable (drives the "Hiányzó tárhely" dashboard/stacks/app-card indicator).
|
||||
func (s *Server) missingStorageMap(list []stacks.Stack) map[string]string {
|
||||
|
||||
Reference in New Issue
Block a user