v0.86.0: Phase 2 managed updates — floor-driven auto-update

The controller honors an operator-enforced minimum version (FLOOR) on the hub
report ACK and auto-updates to the floor when below it (managed default, no click),
reusing the Phase 1 in-guest-pull + agent-swap + rollback. Latest stays the opt-in
button; the floor is the auto-target, never latest.

- pusher.go: PushResponse += min_controller_version, latest_version (existing ACK seam)
- main.go: OnPushResponse → updater.SetFloor + MaybeAutoUpdate (rides report cycle)
- updater.go: SetFloor/GetFloor + MaybeAutoUpdate reusing performUpdate (auto-floor);
  no-op at/above floor, floor>latest, dev/no-agent/backup; no flap (in-mem+persisted)
- settings UI (HU): floor display + auto restart-poll during an auto-update
- tests: below/at/floor>latest/no-flap/raised-floor; below-floor red-proof verified
- no agent change (reuses Phase 1 POST /controller/swap)

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:47 +02:00
parent 3aa9777f1c
commit 1310a0ebd7
10 changed files with 445 additions and 60 deletions
+8
View File
@@ -329,6 +329,14 @@ func main() {
} else {
sett.SetHubVerified(true, time.Now())
}
// Phase 2 managed updates: the ACK carries the operator-enforced minimum version (FLOOR).
// Hand it to the updater and reconcile — if the box is below the floor it auto-updates to
// the floor (reusing the Phase 1 swap). This rides the existing report cycle; no new timer.
// latest_version stays informational (the customer's opt-in button), NOT the auto-target.
if updater != nil {
updater.SetFloor(resp.MinControllerVersion)
updater.MaybeAutoUpdate()
}
}
// Wire hub push status into alert manager for dashboard alerts
alertMgr.SetHubPushStatus(func() web.HubPushStatusData {