Files
felhom-controller/REPORT.md
T
admin 7cddb885e0 docs: v0.86.0 live floor-test results — auto-update 0.86.0→0.87.0 (auto-floor)
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
2026-06-27 12:22:03 +02:00

74 lines
5.2 KiB
Markdown

# 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 — PASS
**Sequence (all live on `felhom-pve` guest 9201 + hub on k3s):**
1. Built/pushed hub `0.15.0` + controller `0.86.0`; deployed hub via ArgoCD.
2. **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.
3. Built/pushed a trivial **0.87.0** (version-only) as the auto-update target.
4. **Set the floor:** global floor → **0.87.0** via `DEFAULT_MIN_CONTROLLER_VERSION` on 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 logged
`Default controller-version floor: 0.87.0`.
5. **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 current `0.86.0 < 0.87.0` and **auto-updated to 0.87.0 with no click**.
- Proof (`/api/selfupdate/status` `last_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_version` for demo-felhom = **0.87.0** (verified via sqlite on the
copied DB; temp copy deleted afterwards).
6. **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.0` healthy, and `last_state.initiated_at`
was **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.