docs: v0.140.0 Direction-2 waiter — REPORT + CONTEXT (live: 240s no-annotation hold, 0.047s wake, restart no-storm)
This commit is contained in:
+17
-1
@@ -7,7 +7,23 @@
|
||||
>
|
||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||
|
||||
Last updated: 2026-07-16 (v0.139.0 — immediate out-of-cycle hub report, Direction 1)
|
||||
Last updated: 2026-07-16 (v0.140.0 — Direction-2 immediate-sync wait channel; the immediate-sync arc is COMPLETE for the controller plane)
|
||||
|
||||
> **2026-07-16 — v0.140.0: Direction-2 immediate-sync (hub→box) SHIPPED.** The reverse of v0.139.0:
|
||||
> an OPERATOR action on the hub now reaches the box in seconds. `report.Waiter`
|
||||
> (`internal/report/waiter.go`) holds a hanging `GET {hub}/api/v1/wait?gen=N` (same hub URL+key as
|
||||
> the pusher — no new config keys) against hub ≥ v0.58.0's in-memory operator-intent generation
|
||||
> counter; on a generation CHANGE it fires the v0.139.0 `report.Trigger` and nothing else, so the
|
||||
> report ACK delivers everything through the UNCHANGED machinery (the box pulls even the wake-up).
|
||||
> No overall client timeout (held GET); first-observation records-not-fires (no restart echo);
|
||||
> same-gen timeout fires nothing; errors incl. a 404 from a pre-v0.58.0 hub back off 5s→5min while
|
||||
> the 15-min cycle reconciles. Gated on the SAME `hubPusher!=nil && Hub.Enabled` as the trigger.
|
||||
> Copy soften: backups_remote/escrow "néhány **másodperc**, legfeljebb 15 perc" (15-min bound stays
|
||||
> as the honest worst case). Red-proof: baseline-branch-off → first-obs fires (reverted). Agent-plane
|
||||
> poke is PARKED in the OOB arc (spike P4). Deployed to 9201; live-validated (hold + immediacy).
|
||||
> Detail: `CHANGELOG.md` v0.140.0, `controller/README.md` §9, `REPORT.md`.
|
||||
|
||||
Last updated (prior): 2026-07-16 (v0.139.0 — immediate out-of-cycle hub report, Direction 1)
|
||||
|
||||
> **2026-07-16 — v0.139.0: immediate out-of-cycle hub report on user actions (Direction 1
|
||||
> SHIPPED; Direction 2 pending SPIKE-immediate-sync-transport).** Viktor's ruling: user actions
|
||||
|
||||
@@ -1,145 +1,56 @@
|
||||
# REPORT — v0.139.0: immediate out-of-cycle hub report on user actions (Direction 1)
|
||||
# REPORT — most recent implementation
|
||||
|
||||
**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)
|
||||
## v0.140.0 — Direction-2 immediate-sync: hub→box wait channel client (2026-07-16)
|
||||
|
||||
## Baselines used
|
||||
The reverse half of the immediacy arc (Direction 1 = v0.139.0 box→hub trigger). An operator action
|
||||
on the hub now reaches the box in **seconds** instead of on the next ~15-min cycle. Pairs with hub
|
||||
**v0.58.0** (`GET /api/v1/wait` + the in-memory operator-intent notifier). Grounding:
|
||||
`felhom.eu/documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md` (option b).
|
||||
|
||||
| Repo | Base commit | Version before → after |
|
||||
|---|---|---|
|
||||
| felhom-controller | `8f3564c` (matched the spec's `8f3564c1`) | v0.138.0 → **v0.139.0** |
|
||||
### What shipped
|
||||
- **`internal/report/waiter.go` (new) `report.Waiter`.** Holds a hanging authenticated
|
||||
`GET {hub}/api/v1/wait?gen=N` (same hub URL + key as the pusher — **no new config keys**). Its own
|
||||
`http.Client` has **no overall Timeout** (a held GET must stay open for the hub's ~240 s hold),
|
||||
with connect/TLS/`ResponseHeaderTimeout` deadlines; a per-request context bounds a black-holed
|
||||
connection. On a completion whose generation **differs** from the last seen, it fires the v0.139.0
|
||||
`report.Trigger` — and nothing else; the report ACK delivers config/escrow/claim/floor through the
|
||||
UNCHANGED machinery (zero new delivery logic; the box pulls even the wake-up).
|
||||
- First observation **records, never fires** (no restart-echo report).
|
||||
- Same-generation timeout **fires nothing** (not interval-shortening).
|
||||
- Heartbeat newlines tolerated; only the `{"gen":N}` line is read (contentless wake).
|
||||
- Any error — transport, **404 from a pre-v0.58.0 hub**, malformed body — backs off 5 s→5 min
|
||||
(reset on success), ONE WARN per state change, and the 15-min cycle keeps reconciling. Exits
|
||||
promptly on ctx cancel (even mid-hold).
|
||||
- **`cmd/controller/main.go`.** Waiter constructed + started beside the Direction-1 trigger, gated on
|
||||
the SAME `hubPusher != nil && cfg.Hub.Enabled` (strict no-op when hub reporting is off).
|
||||
- **Copy soften.** `backups_remote.html` + `backups_escrow.html`: "ez általában néhány **másodperc**,
|
||||
legfeljebb 15 perc" (the 15-min bound stays as the honest worst case; escrow grace window
|
||||
unchanged).
|
||||
|
||||
## What shipped
|
||||
### Tests + red-proof
|
||||
Non-hollow `waiter_test.go` (httptest fake hub): fires-once-on-change, same-gen-timeout-no-fire,
|
||||
**first-observation-records-no-fire** (RED-PROOF: disable the baseline branch → first poll fires;
|
||||
run-fail-reverted), heartbeats-tolerated, malformed-completion-no-fire-no-panic, 404-as-error,
|
||||
Bearer+gen wire check, ctx-cancel-mid-hold-returns-promptly. Full controller suite green
|
||||
(`go build/vet/test ./...`); design gates pass (template_id / emoji / native_confirm / offbox_rename
|
||||
/ mojibake).
|
||||
|
||||
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.
|
||||
### Deploy + live validation (guest 9201, customer demo-felhom)
|
||||
Image `felhom-controller:0.140.0` built + pushed on 180; deployed via the bootstrap flow; container
|
||||
`Up (healthy)`; log `[report] hub wait channel active (hold ≤240s)`; startup report sent.
|
||||
- Completion-on-difference path: box-side curl with a mismatched `?gen=999999` → `{"gen":0}` in
|
||||
**0.047 s** through the real ingress.
|
||||
- No-annotation hold proof: box-side curl at `?gen=0` held cleanly for **240.04 s** (HTTP 200 →
|
||||
`{"gen":0}`, **10 heartbeat newlines**); the controller's own Waiter logged **no wait-channel
|
||||
errors** over the window (holds + reconnects cleanly past nginx's 60 s default). No ingress
|
||||
annotation applied.
|
||||
- Scenario C (hub `rollout restart` mid-hold): the Waiter logged **exactly one** WARN (`unexpected
|
||||
EOF — backing off`), reconnected cleanly, and fired **0 out-of-cycle reports** (gen 0→0, no storm).
|
||||
The "recovered" INFO is deferred to the next hold completion (`pollOnce` blocks) — cosmetic.
|
||||
- Save→applied via the operator UI needs an operator login (CC lacks the password by standing
|
||||
constraint); the mechanism is unit-proven and the delivery chain is the already-live v0.139.0 /
|
||||
config-refresh path.
|
||||
|
||||
- **`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.
|
||||
### Coupling
|
||||
Immediacy needs hub ≥ v0.58.0; against an older hub the wait 404s and the box degrades to the 15-min
|
||||
cycle. No agent coupling, no `MinAgent`. The agent-plane ep0-poke stays parked in the OOB arc.
|
||||
|
||||
Reference in New Issue
Block a user