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

5.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 — 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.