hub v0.15.0: Phase 2 managed updates — per-customer controller-version floor

Operator sets a minimum controller version (FLOOR), per-customer defaulting to a
global floor; the report ACK returns the effective floor + latest_version so the
controller auto-updates to the floor when below it (latest stays the opt-in button).

- store: min_controller_version column + hub_settings global floor + Effective/
  Get/SetGlobal/SetMin resolution + config/env DEFAULT_MIN_CONTROLLER_VERSION
- handler: report ACK {min_controller_version, latest_version}; LatestVersionProvider
- web: global floor editor + per-customer override form + Floor column (English)
- tests: floor resolution + ACK + render; override-precedence red-proof verified

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSZmmSFVzGwEzhYmxbkgBK
This commit is contained in:
2026-06-27 11:59:30 +02:00
parent ea09ead806
commit 30380a59f4
12 changed files with 614 additions and 111 deletions
+29
View File
@@ -24,6 +24,13 @@ type ConfigTemplateProvider interface {
Template() string
}
// LatestVersionProvider returns the latest controller image version known to the hub's registry
// checker (e.g. "0.86.0"), or "" if unknown. Satisfied by *web.VersionChecker; nil when the
// registry checker is disabled. Used to advertise latest on the controller report ACK.
type LatestVersionProvider interface {
LatestVersion() string
}
// Handler handles API endpoints for report ingest and customer queries.
type Handler struct {
store *store.Store
@@ -35,6 +42,13 @@ type Handler struct {
templateProvider ConfigTemplateProvider
dispatcher *notify.Dispatcher
assetsMgr *assets.Manager
latestVersion LatestVersionProvider
}
// SetLatestVersionProvider wires the registry version checker so the controller report ACK can
// advertise the latest available version (Phase 2). nil-safe (no latest_version field emitted).
func (h *Handler) SetLatestVersionProvider(p LatestVersionProvider) {
h.latestVersion = p
}
// New creates a new API handler.
@@ -267,6 +281,21 @@ func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) {
resp["customer_blocked"] = true
}
}
// Phase 2 managed updates: advertise the effective controller-version FLOOR (per-customer override
// else global default) and the latest available version. The controller compares its current
// version against the floor and auto-updates when below it (latest stays the customer's opt-in
// "update to latest" button — NOT the auto-target). Both fields are omitted when empty, so an old
// controller that ignores them, or a hub with no floor configured, behaves exactly as before.
if floor := h.store.EffectiveMinControllerVersion(payload.CustomerID); floor != "" {
resp["min_controller_version"] = floor
}
if h.latestVersion != nil {
if latest := h.latestVersion.LatestVersion(); latest != "" {
resp["latest_version"] = latest
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(resp)