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
+28
View File
@@ -1,5 +1,33 @@
## Changelog
### v0.86.0 — Phase 2 managed updates: floor-driven auto-update (2026-06-27)
- **What:** the controller now honors an operator-enforced **minimum version** (FLOOR) delivered on the
hub report ACK and **auto-updates to the floor** when below it — the managed default (no customer
click). The customer "update to latest" button is unchanged (latest, opt-in); the floor is the
**auto-target**, never latest.
- **`internal/report/pusher.go`:** `PushResponse` gains `min_controller_version` + `latest_version`
(the pusher already parsed the ACK for `customer_blocked` — extended, not a new path). *(The task
pointed at `notify/notifier.go`'s response-discards, but the actual report sender is `pusher.go`,
which already had an `OnPushResponse` seam — used here.)*
- **`cmd/controller/main.go`:** the existing `OnPushResponse` callback now also calls
`updater.SetFloor(resp.MinControllerVersion)` + `updater.MaybeAutoUpdate()` — riding the existing
report cycle; **no new timer/endpoint**.
- **`internal/selfupdate/updater.go`:** `SetFloor`/`GetFloor` + `MaybeAutoUpdate()` which **reuses the
Phase 1 `performUpdate`** (in-guest pull → agent `SwapController` → rollback on failure) with the
**floor** as target (`initiatedBy="auto-floor"`). Strict no-op unless: floor set, current parses,
current < floor (at/above = nothing — does NOT chase latest), agent wired, no backup running, no swap
in flight, not already attempted this floor (in-memory + persisted-state guard = no flapping/storm),
and the floor is **pullable** (floor ≤ latest available; floor > latest → warn + do nothing).
- **UI (settings, Hungarian):** shows "Minimális verzió (üzemeltető): X" when a floor is set, and during
an auto-update surfaces the same restart-poll panel as the button (auto-polls `/api/health` on load).
- **Tests (`internal/selfupdate/floor_test.go`):** below-floor→floor (not latest); at/above→no-op;
no-floor inert; floor>latest→no chase + warn; no-flap (one swap across repeated reconciles); raised-floor
honored (Scenario C/E); dev/no-agent→no-op. **Companion red-proof (verified):** making `MaybeAutoUpdate`
always no-op fails the below-floor test → restored → green.
- **No agent change** (reuses Phase 1 swap). Live: dogfood-deployed 0.86.0 to demo 9201 via the Phase 1
button, then floor set to 0.87.0 → box auto-updated 0.86.0 → 0.87.0 with no click; at/above-floor no-op
confirmed (see REPORT.md).
### v0.85.1 — version-only build (live self-update validation target) (2026-06-26)
- No code change vs v0.85.0. Pushed as the registry "latest" so the live e2e self-update path could be
validated via the real Settings button (demo `0.85.0 → 0.85.1`: in-guest pull → agent swap → reload).