hub: dead-host roll-up honesty - customer status folds worst expected host

Customer status (dashboard row, /configs list, detail header + strip) is
now worst(controllerDerived, hostStatusOf(each expected host)) via the ONE
staleness definition (Server.hostStatus, hosts.go - shared with the
HostStalenessChecker; no second threshold). Any host down/stale caps the
customer at WARN with a cause chip naming the host ("host down: <id>");
pending (never-reported) hosts worsen only once the customer has reported
(onboarding exclusion). The three previously-inlined controller-status
chains collapse into controllerStatus() (rollup.go). Display + derivation
only - checker alerting untouched.

Live shape pinned (drill-1 / Peti cluster): host down 23h + controller
report minutes old rendered a GREEN row - TestRollup_DeadHostMasking now
fails that exact outcome. Red-proof: short-circuiting foldHostStatus to
controller-only flips C + two D subtests red ("dashboard row is GREEN
over a 23h-dead host").
This commit is contained in:
2026-07-13 14:52:04 +02:00
parent 04861a7ed3
commit 36c72138f1
8 changed files with 305 additions and 34 deletions
+21 -22
View File
@@ -53,6 +53,7 @@ type customerListEntry struct {
HasConfig bool
IsBlocked bool
OverallStatus string // ok, warn, down, disabled, pending, "" if no reports
HostCause string // v0.53.0 roll-up: "" or "host down|stale|pending: <id>"
ControllerVersion string
TimeSinceReport time.Duration
ConfigCreatedAt time.Time
@@ -95,20 +96,13 @@ func (s *Server) handleConfigList(w http.ResponseWriter, r *http.Request) {
}
for _, c := range customers {
status := "ok"
if c.HealthStatus == "disabled" {
status = "disabled"
} else if c.TimeSinceReport > time.Hour {
status = "down"
} else if c.TimeSinceReport > 30*time.Minute || c.HealthStatus == "warn" {
status = "warn"
} else if c.HealthStatus == "fail" {
status = "down"
}
// Controller-derived status + the v0.53.0 dead-host roll-up (rollup.go).
status, hostCause := s.foldHostStatus(c.CustomerID, controllerStatus(&c), true)
if entry, ok := merged[c.CustomerID]; ok {
// Config exists — enrich with report data
entry.OverallStatus = status
entry.HostCause = hostCause
entry.ControllerVersion = c.ControllerVersion
entry.TimeSinceReport = c.TimeSinceReport
if entry.CustomerName == "" {
@@ -120,12 +114,21 @@ func (s *Server) handleConfigList(w http.ResponseWriter, r *http.Request) {
CustomerID: c.CustomerID,
CustomerName: c.CustomerName,
OverallStatus: status,
HostCause: hostCause,
ControllerVersion: c.ControllerVersion,
TimeSinceReport: c.TimeSinceReport,
}
}
}
// Config-only customers (no reports yet): the roll-up still applies — a down/stale host
// must not hide behind the muted no-reports dash; only never-reported hosts are excluded.
for _, e := range merged {
if e.OverallStatus == "" {
e.OverallStatus, e.HostCause = s.foldHostStatus(e.CustomerID, "", false)
}
}
// Phase 2 floor: resolve each customer's effective floor (override else global) + below-floor flag.
globalFloor := s.store.GetGlobalMinControllerVersion()
for _, e := range merged {
@@ -198,21 +201,15 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
json.Unmarshal([]byte(cfg.ConfigJSON), &overrides)
}
// Overall status
// Overall status: controller-derived + the v0.53.0 dead-host roll-up (rollup.go). The
// blocked override stays LAST (administrative state wins the token); the host cause chip
// renders regardless so the header says WHICH host is the problem.
overallStatus := "pending"
if customer != nil {
if customer.HealthStatus == "disabled" {
overallStatus = "disabled"
} else if customer.TimeSinceReport > time.Hour {
overallStatus = "down"
} else if customer.TimeSinceReport > 30*time.Minute || customer.HealthStatus == "warn" {
overallStatus = "warn"
} else if customer.HealthStatus == "fail" {
overallStatus = "down"
} else {
overallStatus = "ok"
}
overallStatus = controllerStatus(customer)
}
var hostCause string
overallStatus, hostCause = s.foldHostStatus(customerID, overallStatus, customer != nil)
if cfg != nil && cfg.Status == "blocked" {
overallStatus = "blocked"
}
@@ -290,6 +287,7 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
Customer *store.CustomerSummary
Report map[string]interface{}
OverallStatus string
HostCause string // v0.53.0 roll-up: "" or "host down|stale|pending: <id>"
LatestVersion string
UpdateAvailable bool
@@ -386,6 +384,7 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
Customer: customer,
Report: report,
OverallStatus: overallStatus,
HostCause: hostCause,
LatestVersion: latestVersion,
UpdateAvailable: updateAvailable,