B2b: decommission orchestration + missing-storage indicator + re-enroll fix (v0.65.0)

agentapi.Decommission + handleStorageDecommission (migrate-all-or-none, Change 2):
migrate-then-decommission via the migration done-hook, or decommission-anyway (stop
apps, keep HDD_PATH). 'Hiányzó tárhely' badge on dashboard/stacks/app card when an
app's drive is decommissioned/disconnected/absent. Change 4: registerStoragePath
clears the decommissioned marker on re-enroll (ClearDecommissioned had no callers).
Non-hollow tests incl. mutation-proven Change-4 companion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 20:08:52 +02:00
parent 16a4c3e878
commit f2596ea433
13 changed files with 435 additions and 35 deletions
+42 -1
View File
@@ -151,6 +151,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
data := s.baseData("dashboard", "Vezérlőpult")
data["Stacks"] = deployedStacks
data["MissingStorage"] = s.missingStorageMap(deployedStacks)
data["RunningCount"] = running
data["StoppedCount"] = stopped
data["TotalCount"] = len(stackList)
@@ -195,7 +196,9 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
func (s *Server) stacksHandler(w http.ResponseWriter, r *http.Request) {
data := s.baseData("stacks", "Alkalmazások")
data["Stacks"] = s.stackMgr.GetStacks()
allStacks := s.stackMgr.GetStacks()
data["Stacks"] = allStacks
data["MissingStorage"] = s.missingStorageMap(allStacks)
// Build storage label lookup for deployed apps
storageLabels := make(map[string]string) // stack name → storage label
@@ -486,6 +489,9 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
}
data["MigrateTargets"] = targets
data["MigrateCurrent"] = current
if label, missing := s.missingStorageLabel(current); missing {
data["MissingStorageLabel"] = label
}
}
s.executeTemplate(w, r, "app_info", data)
@@ -1116,6 +1122,41 @@ func (s *Server) appsUsingPath(storagePath string) []string {
return appsUsingPathIn(s.stackMgr.GetStacks(), s.stackMgr.LoadAppConfigByName, storagePath)
}
// missingStorageLabel reports whether a deployed app's HDD_PATH resolves to an UNAVAILABLE registry
// path (decommissioned, disconnected, or no longer registered) and returns its human label. An app
// with no HDD_PATH (SSD-resident) is never "missing".
func (s *Server) missingStorageLabel(hddPath string) (string, bool) {
if hddPath == "" {
return "", false
}
for _, sp := range s.settings.GetStoragePaths() {
if sp.Path == hddPath {
if sp.Decommissioned || sp.Disconnected {
return s.settings.GetStorageLabel(hddPath), true
}
return "", false // present + available
}
}
return s.settings.GetStorageLabel(hddPath), true // not in registry → its drive is gone
}
// 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 {
out := map[string]string{}
for _, st := range list {
if !st.Deployed {
continue
}
if cfg := s.stackMgr.LoadAppConfigByName(st.Name); cfg != nil {
if label, missing := s.missingStorageLabel(cfg.Env["HDD_PATH"]); missing {
out[st.Name] = label
}
}
}
return out
}
// appsUsingPathIn is the pure core of appsUsingPath (testable without a live stacks.Manager): the
// deployed apps whose data dir (app.yaml HDD_PATH) is exactly storagePath, by display name. This is
// the "name the apps that break" list for the type-to-confirm wipe/eject UI.