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

146 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.