f665bbed45
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.
146 lines
9.3 KiB
Markdown
146 lines
9.3 KiB
Markdown
# 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.
|