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
+22
View File
@@ -58,6 +58,13 @@ type Config struct {
Listen string `yaml:"listen"`
DataDir string `yaml:"data_dir"`
} `yaml:"server"`
ControllerUpdates struct {
// DefaultMinVersion is the global controller-version FLOOR fallback (Phase 2 managed updates):
// the minimum controller version any customer's box auto-updates to, unless overridden
// per-customer or via the operator UI (hub_settings). Empty = no global floor. Env-overridable
// with DEFAULT_MIN_CONTROLLER_VERSION.
DefaultMinVersion string `yaml:"default_min_version"`
} `yaml:"controller_updates"`
}
func main() {
@@ -83,6 +90,9 @@ func main() {
if v := os.Getenv("REGISTRY_TOKEN"); v != "" {
cfg.Registry.Token = v
}
if v := os.Getenv("DEFAULT_MIN_CONTROLLER_VERSION"); v != "" {
cfg.ControllerUpdates.DefaultMinVersion = v
}
// Ensure data dir exists
os.MkdirAll(cfg.Server.DataDir, 0755)
@@ -96,6 +106,13 @@ func main() {
defer dataStore.Close()
logger.Printf("[INFO] Database opened at %s", dbPath)
// Phase 2 managed updates: seed the global controller-version floor fallback (config/env). A
// hub_settings row set via the operator UI overrides this at runtime.
dataStore.SetDefaultMinControllerVersion(cfg.ControllerUpdates.DefaultMinVersion)
if cfg.ControllerUpdates.DefaultMinVersion != "" {
logger.Printf("[INFO] Default controller-version floor: %s", cfg.ControllerUpdates.DefaultMinVersion)
}
// Parse stale threshold
staleThreshold, err := time.ParseDuration(cfg.Alerting.StaleThreshold)
if err != nil {
@@ -192,6 +209,11 @@ func main() {
logger.Printf("[INFO] Registry version checker disabled (no credentials configured)")
}
webServer.SetVersionChecker(versionChecker)
// Advertise latest on the controller report ACK (Phase 2). Guard the nil case so we don't store a
// non-nil interface wrapping a nil *VersionChecker.
if versionChecker != nil {
apiHandler.SetLatestVersionProvider(versionChecker)
}
// Session cleanup — removes expired sessions every hour
go webServer.CleanupSessions(ctx)