hub v0.45.0: floor-UI separation + effective-floor source + per-box MinAgent conditional floor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -745,6 +745,48 @@ func (s *Server) handleUnblockCustomer(w http.ResponseWriter, r *http.Request, c
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=unblocked", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// countBoxesBelowFloor counts reporting boxes whose EFFECTIVE floor (per-customer override else the
|
||||
// proposed global) would exceed their reported controller version — i.e. how many boxes a proposed
|
||||
// global-floor save would immediately push into an update. Boxes with a per-customer override are
|
||||
// governed by that override, not the proposed global, so they are excluded from the global-save
|
||||
// blast radius (the confirm dialog is about the GLOBAL knob). Reused by the confirm-count endpoint.
|
||||
func (s *Server) countBoxesBelowFloor(proposedGlobal string) int {
|
||||
customers, err := s.store.GetCustomers()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
configs, _ := s.store.ListCustomerConfigs()
|
||||
override := make(map[string]string, len(configs))
|
||||
for _, c := range configs {
|
||||
if c.MinControllerVersion != "" {
|
||||
override[c.CustomerID] = c.MinControllerVersion
|
||||
}
|
||||
}
|
||||
n := 0
|
||||
for _, c := range customers {
|
||||
floor := proposedGlobal
|
||||
if ov, ok := override[c.CustomerID]; ok {
|
||||
floor = ov // an overridden box is not moved by the global knob
|
||||
}
|
||||
if floor != "" && c.ControllerVersion != "" && compareVersions(floor, c.ControllerVersion) > 0 {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// handleGlobalFloorImpact answers the confirm dialog's "how many boxes are below <v>?" probe
|
||||
// (GET /configuration/global-floor/impact?v=X.Y.Z). Read-only JSON; blank v = 0.
|
||||
func (s *Server) handleGlobalFloorImpact(w http.ResponseWriter, r *http.Request) {
|
||||
v, ok := normalizeFloorInput(r.URL.Query().Get("v"))
|
||||
count := 0
|
||||
if ok && v != "" {
|
||||
count = s.countBoxesBelowFloor(v)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"version": v, "valid": ok, "below": count})
|
||||
}
|
||||
|
||||
// handleSetGlobalFloor sets (or clears) the global controller-version floor (Phase 2 managed
|
||||
// updates). Empty clears the hub_settings override, falling back to the config/env default.
|
||||
func (s *Server) handleSetGlobalFloor(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -796,7 +838,8 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
agentVer, okAV := normalizeFloorInput(r.FormValue("agent_version"))
|
||||
goldenVer, okGV := normalizeFloorInput(r.FormValue("golden_version"))
|
||||
if !okAV || !okGV {
|
||||
minAgent, okMA := normalizeFloorInput(r.FormValue("min_agent")) // Part D: empty = uncoupled release
|
||||
if !okAV || !okGV || !okMA {
|
||||
http.Redirect(w, r, "/configuration?flash=artifact_ver_invalid", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
@@ -811,12 +854,13 @@ func (s *Server) handleSetArtifacts(w http.ResponseWriter, r *http.Request) {
|
||||
AgentSHA256: agentSHA,
|
||||
GoldenVersion: goldenVer,
|
||||
GoldenSHA256: goldenSHA,
|
||||
MinAgent: minAgent,
|
||||
}); err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to set artifact manifest: %v", err)
|
||||
http.Error(w, "Internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] Artifact manifest set: agent=%s golden=%s", agentVer, goldenVer)
|
||||
s.logger.Printf("[INFO] Artifact manifest set: agent=%s golden=%s min_agent=%q", agentVer, goldenVer, minAgent)
|
||||
http.Redirect(w, r, "/configuration?flash=artifacts_set", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user