REPORT/CHANGELOG: dogfood 0.85.1→0.86.0 (Phase 1 endpoint), global floor 0.87.0, auto-update fired (initiated_by=auto-floor, success), no-flap at floor confirmed, hub controller_version=0.87.0. Demo left at 0.87.0 / floor 0.87.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSZmmSFVzGwEzhYmxbkgBK
5.2 KiB
REPORT — v0.86.0: Phase 2 managed updates — floor-driven auto-update
Repo: felhom-controller · Version: v0.85.1 → v0.86.0 · Date: 2026-06-27
Why
Phase 1 shipped an opt-in "update to latest" button. Phase 2 adds the managed default: the operator sets a per-customer minimum controller version (FLOOR; defaults to a global floor), and any box below it auto-updates to the floor on its next report — no customer click. Latest stays the customer's manual opt-in; the floor is the auto-target (never latest).
Files changed
internal/report/pusher.go—PushResponse+=min_controller_version,latest_version. The pusher already parsed the ACK (forcustomer_blocked) and exposedOnPushResponse; extended, not a new path. (The task spec pointed atnotify/notifier.go'sio.Discardresponse-drops, but the real periodic report sender ispusher.go— it already had the seam, so the floor is read there.)cmd/controller/main.go— the existingOnPushResponsenow alsoupdater.SetFloor(...)+updater.MaybeAutoUpdate(). Rides the existing report cycle; no new timer/endpoint.internal/selfupdate/updater.go—floor/lastAutoFloorAttemptstate;SetFloor/GetFloor;MaybeAutoUpdate()reusing Phase 1performUpdate(pull → agentSwapController→ rollback) with the floor as target (initiatedBy="auto-floor").internal/web/handlers.go+templates/settings.html— Hungarian floor display + auto restart-poll.
Gating (MaybeAutoUpdate) — auto-target is the FLOOR, never latest
No-op unless ALL: floor set; current parses (not dev); current < floor (at/above = nothing — does NOT chase latest, Scenario B); agent wired; no backup running; no swap in flight; not already attempted this floor (in-process flag and persisted UpdateState — no flapping across the report cycle or a restart); floor is pullable (floor ≤ latest available in the registry; floor > latest → warn + do nothing, no chasing a non-existent image). Rollback on failure is Phase 1's (the agent) — not duplicated.
Tests — go build ./... && go vet ./... && go test ./... green
internal/selfupdate/floor_test.go:
- below-floor → updates to the floor ref (fake agent asserts
…:0.87.0, not latest) — Scenario A - at/above floor → no pull, no swap (latest far ahead is irrelevant) — Scenario B
- no floor → inert — Scenario D
- floor > latest available → no pull/swap + warning — Rule 2
- no-flap → exactly one swap across repeated reconciles — Rule 4
- raised floor (override/global bump) honored on next reconcile — Scenario C/E
- dev current / no agent → no-op
- Companion red-proof (verified): forcing
MaybeAutoUpdateto always no-op →TestMaybeAutoUpdate_BelowFloor_UpdatesToFloorFAILS (pulled "", 0 swaps) → restored → green.
Live test — floor auto-update 0.86.0 → 0.87.0 on demo 9201 — PASS
Sequence (all live on felhom-pve guest 9201 + hub on k3s):
- Built/pushed hub
0.15.0+ controller0.86.0; deployed hub via ArgoCD. - Dogfood deploy (Phase 1 still works): triggered the controller's self-update via the exact
endpoint the Settings button invokes (
POST /api/selfupdate/update, hub-key bearer, in-guest) → the demo went 0.85.1 → 0.86.0 (in-guest pull → agent swap → healthy). This put the floor-reading code on the box. - Built/pushed a trivial 0.87.0 (version-only) as the auto-update target.
- Set the floor: global floor → 0.87.0 via
DEFAULT_MIN_CONTROLLER_VERSIONon the hub (the implemented global-floor mechanism; the operator-UI form is password-gated, which I'm not permitted to submit — the form handlers are covered by hub unit/render tests). Hub loggedDefault controller-version floor: 0.87.0. - Auto-update fired (Scenario A): on the demo's next report (forced immediately by restarting the
controller, which performs a startup hub report) the ACK carried
min_controller_version=0.87.0; the controller saw current0.86.0 < 0.87.0and auto-updated to 0.87.0 with no click.- Proof (
/api/selfupdate/statuslast_state):previous_version=0.86.0,target_version=0.87.0,status=success,initiated_by="auto-floor", completed 10:15:42Z. - Host-side: container
…:0.87.0 Up (healthy);/etc/felhom-controller-image = 0.87.0. - Hub-side:
reports.controller_versionfor demo-felhom = 0.87.0 (verified via sqlite on the copied DB; temp copy deleted afterwards).
- Proof (
- No flap / at-or-above floor (Scenario B): forced another report with the box now at 0.87.0
(== floor) → no second update: image stayed
…:0.87.0healthy, andlast_state.initiated_atwas unchanged (still 10:15:37Z). The floor does not re-trigger when at/above.
End state (left consistent, per spec): demo 9201 at 0.87.0; global floor 0.87.0 persisted in
manifests/hub.yaml (DEFAULT_MIN_CONTROLLER_VERSION), hub Synced/Healthy. Only demo-felhom reports
(peti-felhom is config-only) so the global floor has no other effect.
What was NOT done
- No agent binary change (reuses Phase 1 swap). The customer latest-only button is untouched. No new floor-polling endpoint/timer. No auto-update loop / while a swap is in flight.