Files
felhom-controller/REPORT.md
T
admin 1310a0ebd7 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
2026-06-27 11:59:47 +02:00

3.2 KiB

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

Repo: felhom-controller · Version: v0.85.1v0.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.goPushResponse += min_controller_version, latest_version. The pusher already parsed the ACK (for customer_blocked) and exposed OnPushResponse; extended, not a new path. (The task spec pointed at notify/notifier.go's io.Discard response-drops, but the real periodic report sender is pusher.go — it already had the seam, so the floor is read there.)
  • cmd/controller/main.go — the existing OnPushResponse now also updater.SetFloor(...) + updater.MaybeAutoUpdate(). Rides the existing report cycle; no new timer/endpoint.
  • internal/selfupdate/updater.gofloor/lastAutoFloorAttempt state; SetFloor/GetFloor; MaybeAutoUpdate() reusing Phase 1 performUpdate (pull → agent SwapController → 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 MaybeAutoUpdate to always no-op → TestMaybeAutoUpdate_BelowFloor_UpdatesToFloor FAILS (pulled "", 0 swaps) → restored → green.

Live test — floor auto-update 0.86.0 → 0.87.0 on demo 9201

(Filled after the live run — see the "Build / deploy / LIVE test" section below.)

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.