hub v0.51.0: DR-tier-by-default — per-customer dr_tier flag (default ON, legacy backfill from reality), cascade stages, WG-registration auto-provision hook, offsite-requires-DR guard (F-6 policy), host-page capability chips (inactive=neutral)
Claude-Session: https://claude.ai/code/session_01NptTCFtu7dz2Ru89qHRagN
This commit is contained in:
@@ -107,6 +107,70 @@ func parseHostVitals(reportJSON string) hostVitals {
|
||||
return v
|
||||
}
|
||||
|
||||
// capabilityView is one privileged-capability chip on the host detail page (v0.51.0 — the agent
|
||||
// has reported these since v0.44.0; the hub now renders them). Class maps the agent's status to
|
||||
// a badge: ok → badge-ok, degraded → badge-fail (critical) / badge-warn, inactive → badge-neutral
|
||||
// (disabled ≠ degraded — the DR-tier-by-default rule; agent v0.86.0 emits "inactive").
|
||||
type capabilityView struct {
|
||||
Name string
|
||||
Feature string
|
||||
Status string
|
||||
Reason string
|
||||
Critical bool
|
||||
Class string
|
||||
}
|
||||
|
||||
// parseHostCapabilities extracts the capabilities array from a host-report body. Missing or
|
||||
// malformed → nil (the "waiting for first report" path — the section hides).
|
||||
func parseHostCapabilities(reportJSON string) []capabilityView {
|
||||
if reportJSON == "" {
|
||||
return nil
|
||||
}
|
||||
var body struct {
|
||||
Capabilities []struct {
|
||||
Name string `json:"name"`
|
||||
Feature string `json:"feature"`
|
||||
Critical bool `json:"critical"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason"`
|
||||
} `json:"capabilities"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(reportJSON), &body); err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]capabilityView, 0, len(body.Capabilities))
|
||||
for _, c := range body.Capabilities {
|
||||
v := capabilityView{Name: c.Name, Feature: c.Feature, Status: c.Status, Reason: c.Reason, Critical: c.Critical}
|
||||
switch c.Status {
|
||||
case "ok":
|
||||
v.Class = "badge-ok"
|
||||
case "inactive":
|
||||
v.Class = "badge-neutral"
|
||||
default: // degraded (or an unknown future status — surface it, never hide it)
|
||||
if c.Critical {
|
||||
v.Class = "badge-error"
|
||||
} else {
|
||||
v.Class = "badge-warn"
|
||||
}
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// capabilitiesNeedDRMigration reports whether any pbsdr-* capability is degraded with the
|
||||
// pre-v1.15.0 signature ("binary not found") — the box predates the uniform DR plumbing. The
|
||||
// host page then surfaces the migration one-liner instead of silently pretending (§8 of the
|
||||
// DR-by-default spec).
|
||||
func capabilitiesNeedDRMigration(caps []capabilityView) bool {
|
||||
for _, c := range caps {
|
||||
if strings.HasPrefix(c.Name, "pbsdr-") && c.Status == "degraded" && c.Reason == "binary not found" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// storageTargetView is the rich per-drive row the host-detail Storage Targets table renders:
|
||||
// fill %, role/state, thin-pool, and SMART health/temp/wear. Parsed from the latest report's
|
||||
// storage_targets[] (the full hostStorageTarget wire shape lives in the api package; this view
|
||||
@@ -296,6 +360,24 @@ func (s *Server) hostDetailData(host *store.Host, r *http.Request) map[string]in
|
||||
vitals := parseHostVitals(reportJSON)
|
||||
storageTargets := parseHostStorageTargets(reportJSON)
|
||||
sort.Slice(storageTargets, func(i, j int) bool { return storageTargets[i].Name < storageTargets[j].Name })
|
||||
// v0.51.0: capability chips — non-ok first (what the operator needs to see), then by name.
|
||||
capabilities := parseHostCapabilities(reportJSON)
|
||||
sort.SliceStable(capabilities, func(i, j int) bool {
|
||||
rank := func(s string) int {
|
||||
switch s {
|
||||
case "degraded":
|
||||
return 0
|
||||
case "inactive":
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
if a, b := rank(capabilities[i].Status), rank(capabilities[j].Status); a != b {
|
||||
return a < b
|
||||
}
|
||||
return capabilities[i].Name < capabilities[j].Name
|
||||
})
|
||||
|
||||
// DR / backup presence — booleans only, never the opaque blobs.
|
||||
drBundle, _ := s.store.GetHostDRBundle(host.HostID)
|
||||
@@ -320,6 +402,8 @@ func (s *Server) hostDetailData(host *store.Host, r *http.Request) map[string]in
|
||||
"GuestRunning": guestRunning,
|
||||
"GuestTotal": len(guests),
|
||||
"StorageTargets": storageTargets,
|
||||
"Capabilities": capabilities,
|
||||
"NeedsDRMigration": capabilitiesNeedDRMigration(capabilities),
|
||||
"DRPresent": drBundle != nil,
|
||||
"EscrowPresent": escrow != nil,
|
||||
// v0.46.0 Diagnostics: pending log pulls + received/blocked bundles (72 h TTL).
|
||||
|
||||
Reference in New Issue
Block a user