# 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 (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.go` — `floor`/`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.