Files
felhom-controller/REPORT.md
T
admin f665bbed45 docs(report): v0.139.0 live verification — 2s save-to-hub round-trip proven on 9201
Out-of-cycle pushes at 17:42:19/17:42:34Z (quiet-window + min-interval
pacing exactly as designed, trailing edge carried the final state);
scheduled 15-min cycle unaffected (17:49:00Z tick on both ends).
Red-proof outcomes and the NOT-yet-live-validated list recorded.
2026-07-16 19:49:46 +02:00

9.3 KiB
Raw Blame History

REPORT — v0.139.0: immediate out-of-cycle hub report on user actions (Direction 1)

Date: 2026-07-16 · Session: Claude Code (spec: "Immediate out-of-cycle hub report on user actions (Direction 1)") · Repo: felhom-controller only (hub/agent/catalog untouched, as specced)

Baselines used

Repo Base commit Version before → after
felhom-controller 8f3564c (matched the spec's 8f3564c1) v0.138.0 → v0.139.0

What shipped

One generic, debounced out-of-cycle report trigger (report.Trigger) on the proven outbound push channel; the 15-min hub-report cycle untouched as the reconciliation backbone.

  • controller/internal/report/trigger.go (new): buffered-1 signal chan + single worker (shape: hub wgsync/reconciler.go). Fire() non-blocking; worker = quiet 2 s → drain → min-interval 15 s → ONE fire of an opaque func() error. Coalesce-and-eventually-fire (trailing edge) — deliberately NOT the internal/sync refuse-debounce. No own retries (Pusher owns 3×5 s); fire error → one WARN, loop continues; exits on ctx cancel.
  • controller/cmd/controller/main.go: ONE canonical fire closure (BuildReport + Claimed + Push), built only when hubPusher != nil && cfg.Hub.Enabled; trigger.Run under the process ctx; replaced the raw per-fire goroutine behind apiRouter.SetReportPushTrigger (v0.70.0 geo seam kept, now debounced); new webServer.SetReportTrigger(trigger.Fire).
  • controller/internal/api/router.go: reportPushNow() added after success in deploy, remove, delete (geo save/sync already had it).
  • controller/internal/web: SetReportTrigger/reportTriggerNow seam (server.go, nil-safe, mirrors api.Router.reportPushNow); fired AFTER the successful local commit in: escrow recovery-code claim (escrow_handlers.go — the headline), notification-prefs save + app-email toggle (handlers.go; password handler untouched — it is a separate handler), offsite target config (offbox_handlers.go offboxConfigHandler) and per-app offsite toggle (offboxToggleHandler), customer claim completion (claim.go).
  • Docs: CHANGELOG (v0.139.0 top entry), CONTEXT.md decision ("Direction 1 shipped; Direction 2 pending SPIKE-immediate-sync-transport"), README report section rewritten, REUSE.md (§1 helper row + §2 coalescing-vs-refuse pattern pair).

Handler-name mapping vs the spec (per §Part 3 "wire the real one")

Spec name Real handler wired
"settings POST handler(s) in web/handlers.go" settingsNotificationsHandler + settingsAppEmailHandler (both preference/notification-type; settingsPasswordHandler is a separate handler → cleanly excluded)
"offbox_handlers.go toggle handler" BOTH offboxConfigHandler (target enable/disable) and offboxToggleHandler (per-app Távoli-mentés flag) — the spec's "enable/disable" is the config handler; the literal "toggle handler" is per-app; both are hub-visible state
"api-router deploy/remove endpoints" deployStack, removeStack, deleteStack (both removal variants)

Commits

Hash Content
fe9266f feat(report): v0.139.0 — trigger engine + all wiring + tests + docs (15 files, +501/14)
(this commit) docs(report): REPORT.md live-validation evidence

Tests & red-proofs

Green gate go build ./... && go vet ./... && go test ./... — all packages pass except 6 pre-existing Windows-environment failures (appexport disk-free df=0 ×4, stacks TestMigrate_PaperlessShape_ScopeApp, web TestFab_SelectionsRideBothStartPipelines) — verified failing identically on base 8f3564c via git stash + rerun. Changed packages: internal/report ok, internal/api ok, internal/web ok (except the listed pre-existing fab test). Design gates (template_id, emoji, native_confirm, offbox_rename): all OK. Test functions: 629 total after (+8 new).

New tests (internal/report/trigger_test.go, web/report_trigger_seam_test.go, api/report_trigger_nilsafe_test.go):

Group Test Result
A TestTrigger_SingleFireExactlyOnePush (1 fire → exactly 1 push) PASS
A TestTrigger_FireNonBlocking (100× Fire < 1 ms each, buffer full, no worker) PASS
B TestTrigger_BurstCoalescesTrailingEdge (10 fires → 1..2 pushes, last push after last fire) PASS
C TestTrigger_FireErrorWorkerContinues (error isolated, next fire pushes) PASS
§8 TestTrigger_CancelDuringWaitExitsPromptly (<1 s exit mid-wait, 0 fires) PASS
D TestReportTriggerNow_NilSeamIsNoOp (web) + TestReportPushNow_NilSeamIsNoOp (api) PASS
TestReportTriggerNow_FiresAfterSuccessfulCommitOnly (real offbox-toggle handler: 0 on refused save, 1 on committed save) PASS

Red-proof B (run→fail→revert): replaced Run with the naive for { <-signal; fire() } loop (every signal straight to fire) → TestTrigger_BurstCoalescesTrailingEdge FAILED with "10-fire burst produced 10 pushes, want 1..2" → reverted → green.

Red-proof C (run→fail→revert): added return after the fire-error WARN (worker dies on first error) → TestTrigger_FireErrorWorkerContinues FAILED with "fire count = 1, want >= 2" (the subsequent fire never pushed) → reverted → green.

Deploy (guest 9201, bootstrap flow)

  • Built + pushed gitea.dooplex.hu/admin/felhom-controller:0.139.0 on 180 (digest 73fb3f57…, 145 MB).
  • Deployed: pull + /etc/felhom-controller-image + systemctl restart felhom-controller-bootstrap.service.
  • Verified: docker ps…felhom-controller:0.139.0 Up … (healthy); startup clean; startup report 17:34:04Z "Hub report pushed successfully (2953 bytes)" ↔ hub pod 19:34:04 Received report from demo-felhom (2953 bytes) (hub logs are CEST).

Live validation (real-UI-flow; method stated per doctrine)

Method: browser login was not possible (dashboard password is customer-owned; the deploy also invalidated sessions, and entering credentials via browser automation is out of bounds for CC), so per the repo doctrine the exact endpoints the UI invokes were driven server-side from 180 with curl — real login POST → session cookie → CSRF token from the real page → real form POSTs. The password was read into a shell var from Viktor's out-of-band store (never echoed/logged; length 13 only). Residual gap vs a browser: client-side rendering only — no server logic skipped.

Action: bookstack per-app Távoli-mentés toggle OFF → (6 s later) → ON — value-identical end state (offbox: true confirmed in settings.json afterwards); both POSTs 302 (success flash).

Event Controller (UTC) Hub pod (CEST) Delta
Toggle OFF POST 17:42:17
Out-of-cycle push #1 17:42:19 "Hub report pushed successfully (2968 bytes)" 19:42:19 "Received report from demo-felhom (2968 bytes)" 2 s after the save (= quiet window)
Toggle ON POST 17:42:23
Out-of-cycle push #2 (trailing) 17:42:34 (2961 bytes) 19:42:34 (2961 bytes) 15 s after push #1 (= min-interval pacing; last state carried)

Round-trip user action → hub: 2 seconds (previously up to ~15 min). The second push also live-demonstrates Scenario B's coalescing/pacing (two fires 6 s apart → exactly 2 pushes, 15 s spaced, trailing edge).

Scheduled cycle unaffected: the next 15-min hub-report tick fired on schedule at 17:49:00Z (≈15 min after the 17:34:04Z startup report; the two out-of-cycle pushes at 17:42 did NOT perturb it) — controller "Hub report pushed successfully (2982 bytes)" ↔ hub 19:49:00 Received report from demo-felhom (2982 bytes).

NOT yet live-validated (unit-covered; shared seam proven live)

  • Escrow recovery-code claim call site — deliberately NOT exercised (the demo box is escrowed; un/re-escrowing is Viktor's supervised flow). Covered by unit tests + the identical reportTriggerNow seam proven live via the offbox toggle. Expected effect at Viktor's next wizard pass: the "megerősítésre vár" card flips in seconds.
  • App deploy/remove call sites — not exercised live (a deploy/remove on the demo box is not harmless); same reportPushNow seam as geo, now trigger-backed.
  • Customer claim + notification-prefs + app-email call sites — not exercised live (claim would reset Viktor's password; the notifications form on this box has an empty stored email — an "identical re-save" is impossible without picking an email, and the v0.137.0 wipe guard refuses empty-email-with-events anyway).

Observations (out of scope — documented, not acted on)

  • BuildReport call-site duplication: the BuildReport(...) + r.Claimed = sett.GetClaimed() pair still exists at the scheduled job, the startup report, and the debug callback (per spec §Part 2.1 only the trigger closure was centralized).
  • Copy soften candidate (post-live-proof): backups_remote.html "legfeljebb 15 perc" → "általában néhány másodperc" is now a truthful later one-liner (spec §Part 3 note).
  • offboxConfirmEscrowHandler (legacy manual escrow confirm) also flips hub-relevant state but was not in the Viktor-approved wiring list — a candidate for a later one-liner.
  • Windows green gate: 6 pre-existing environment failures (df=0/rsync-shape); the Linux build on 180 is the authoritative gate for those packages.