Files
felhom.eu/REPORT.md
T

11 KiB
Raw Blame History

felhom.eu — task reports

Overwrite this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in hub/CHANGELOG.md; the scripts history lives in scripts/CHANGELOG.md.

Hub v0.63.0 — system-initiated immediacy (poke + intent bump at every mutation site) — 2026-07-17

1. Baseline used

  • felhom.eu @ 4c9b0e87063278cddbe51e3966a8f8592c7c18f2 ("docs: R-21 slice C …", 2026-07-17), clean tree on main, local == origin/main re-confirmed before starting.
  • Hub v0.62.0 → v0.63.0. Deploy manifest manifests/hub.yaml image line felhom-hub:0.62.0:0.63.0.
  • Both immediate-sync transports were already PROVEN-LIVE (agent poke: agent v0.89.0 + hub v0.59.0, 2026-07-17; intent wait channel: hub v0.58.0 / controller v0.140.0). This task is call-site wiring only — no new mechanism, no spike.

2. Files created / modified

Implementation (commit 30972d8):

  • hub/internal/web/pbsdr.go — +3 pokes: PBSDRAutoProvision, ReissuePBSDR, handlePBSDRReissue (each after the successful SetHostDesired).
  • hub/internal/api/handler.go — new nil-safe Poker interface + poker field + SetPoker; poke in handleAdminSetDesiredState; intent Bump in reissueOnReenroll.
  • hub/internal/api/wg.goPokeAllHosts in handleAdminSetOperatorPeer (only on a successful BumpAllHostGenerations).
  • hub/cmd/hub/main.go — capture the one poke.Notifier and wire it to BOTH planes (webServer.SetPoke(n) + apiHandler.SetPoker(n)); startup log "web + api admin seams armed".
  • hub/internal/web/pbsdr_poke_test.go (new) — 5 web tests (Groups A, B).
  • hub/internal/api/poke_seam_test.go (new) — 5 api tests (Groups C, D).
  • hub/CHANGELOG.md — v0.63.0 entry (newest on top).
  • REUSE.md — new canonical-pattern row: "Immediate-sync notify (per plane)".

Docs (commit 50c090e):

  • REPORT.md (this file, overwritten), CONTEXT.md (decision recorded), documentation/architecture/00-capability-map.md (immediacy row note + version), documentation/backlog/ROADMAP.md (new R-28: agent fast-tick SECONDARY).

Manifest (commit 587fcc2): manifests/hub.yaml image tag → felhom-hub:0.63.0.

3. Per-commit hashes pushed to main

# Hash Contents
1 30972d8 code + tests + CHANGELOG + REUSE
2 50c090e REPORT + CONTEXT + capability-map + ROADMAP
3 587fcc2 manifests/hub.yaml image bump → 0.63.0

4. Test results + red-proof outcomes

New tests (10), all green:

Test Group Asserts
TestPBSDR_AutoProvisionPokes A auto-provision success → exactly one poke to the resolved WG /32 (10.77.0.2) within 2 s
TestPBSDR_AutoProvisionBlockedDoesNotPoke A (neg) blocked precondition (no WG peer) → zero pokes
TestPBSDR_ReissueCorePokes B1 ReissuePBSDR success → one poke to the host's /32
TestPBSDR_ReissueCoreErrorDoesNotPoke B (neg) tenantsync-error reissue → zero pokes (fire-after-commit)
TestPBSDR_HandleReissuePokes B2 operator button success → one poke
TestAdminSetDesiredState_PokesTargetHostOnly C1 admin-set success → PokeHost==1 (target only), PokeAllHosts==0; invalid-JSON path → 0
TestAdminSetDesiredState_NilPokerNoPanic C1 (nil) nil poker → mutation still 200, no panic
TestAdminSetOperatorPeer_PokesFleet C2 operator-peer success → PokeAllHosts==1, PokeHost==0
TestReenroll_BumpsIntent D re-enroll → intent generation advances (synchronous)
TestReenroll_NilIntentHubNoPanic D (nil) nil intent hub → enroll still 201, no panic

Async web assertions synchronize on a buffered channel in the fake sender with a bounded select (≤2 s), never sleep-poll. The web fake sender structurally satisfies poke's unexported pokeSender seam (value passed, type never named); the notifier is a REAL poke.Notifier over the test store — exactly as main.go builds it. The api fake Poker satisfies the exported api.Poker interface and counts calls synchronously.

Red-proofs (run-fail-restore), one representative per scenario group:

Group Removed Test Result
A PBSDRAutoProvision poke (Part 1.1) TestPBSDR_AutoProvisionPokes FAILED red ("no poke within 2s… 10.77.0.2") → restored
B ReissuePBSDR poke (Part 1.2) TestPBSDR_ReissueCorePokes FAILED red ("no poke within 2s… 10.77.0.2") → restored
C handleAdminSetDesiredState poke (Part 2.2) TestAdminSetDesiredState_PokesTargetHostOnly FAILED red ("PokeHost targets = [], want [h1]") → restored
D reissueOnReenroll bump (Part 3) TestReenroll_BumpsIntent FAILED red ("intent generation = 0, want > 0") → restored

These tests fail on the pre-fix tree by construction; a representative removal per group was demonstrated (B2 and C2 share their group's wiring shape with the demonstrated B1/C1 removals).

5. Test count before/after

  • Package internal/web: +5 tests (new pbsdr_poke_test.go).
  • Package internal/api: +5 tests (new poke_seam_test.go).
  • Full suite after: go build ./... && go vet ./... && go test ./...all packages green (api + web are the only two exercising the new code; every other package unchanged).

6. Deployed version + pod/log verification

Built on 180 from ~/git/felhom.eu @ 50c090e (build.sh git pull --ff-only confirmed the wiring is present in the built source) → gitea.dooplex.hu/admin/felhom-hub:0.63.0 pushed. Manifest bump (587fcc2) → ArgoCD app felhom hard-refresh (revision 587fcc27ca03, OutOfSync) → deliberate sync:

sync=Synced health=Progressing
deployment "hub" successfully rolled out
image: gitea.dooplex.hu/admin/felhom-hub:0.63.0

# startup log (poke ENABLED — POKE_SSH_KEY_FILE Secret present in this environment):
[INFO] felhom-hub 0.63.0 starting
[INFO] agent-plane poke enabled (endpoint 167.233.158.164:22, user felhom-peersync; web + api admin seams armed)

The startup line is the new one — "web + api admin seams armed" confirms apiHandler.SetPoker(n) was wired alongside webServer.SetPoke(n) over the one notifier. Poke is enabled (not the disabled fallback), so all wired sites are armed live.

7. NOT yet live-validated (real system-initiated events only)

The new pokes/bumps fire only on real system events, none manufacturable non-destructively from this task. Acceptance bar here = unit tests + deploy + startup-log verification. Unfired live legs, to be observed at the rehearsal's bind sequence (RUNBOOK-rehearsal-v3 S5):

  • Auto-provision poke — fires when a DR-ON customer's host registers its WG peer for the first time and the atom provisions (the exact observed slice-C lag).
  • Reconciler-escalation poke — fires when the pbsdrheal reconciler escalates to ReissuePBSDR (a box stuck in waiting_secret/consumed_failed after losing its converged marker).
  • Clean-slate re-enroll bump — fires when a wiped, re-minted box re-enrolls for a claimed/offsite customer (on that path the controller usually does not exist yet; its startup fetch covers that shape).
  • Operator-button / admin-api pokes (handlePBSDRReissue, admin desired-state / operator-peer) — the operator-triggered legs; exercisable in a supervised session but not fired here.

The first-hold-baseline race stays OPEN by design: a bump landing during a fresh controller's FIRST wait is recorded as baseline without firing (the ruled fix is carrying intent_gen in the report ACK — its own future task, out of scope).

8. Part-4 audit table — EVERY desired-state mutation site

Re-grepped at implementation time for all non-test callers of SetHostDesired, BumpHostDesired, BumpAllHostGenerations, plus the system-initiated customer-deliverable writers. No forgotten fourth caller — every site below is accounted for.

Site (file:landmark) Initiator Plane Notification after this task Reason if none
web/pbsdr.go applyPBSDR disable (~L143→148) operator (form) agent poke (pre-existing)
web/pbsdr.go applyPBSDR re-enable/storage-id (~L168→173) operator (form) agent poke (pre-existing)
web/pbsdr.go applyPBSDR fresh-provision (~L179→186) operator (form) agent poke (pre-existing)
web/pbsdr.go pbsdrProvisionAtom SetHostDesired (~L273) — (shared atom) agent poke at each caller callers poke: applyPBSDR (L186) + PBSDRAutoProvision (NEW)
web/pbsdr.go PBSDRAutoProvision (~L312) system (WG-reg hook) agent NEW poke — (the observed lag)
web/pbsdr.go ReissuePBSDR SetHostDesired (~L357) system (reconciler escalation) agent NEW poke — (also covers pbsdrheal, zero reconciler change)
web/pbsdr.go handlePBSDRReissue SetHostDesired (~L409) operator (button) agent NEW poke
api/handler.go handleAdminSetDesiredState SetHostDesired (~L1419) operator/system (admin api) agent NEW poke (PokeHost)
api/wg.go handleAdminSetOperatorPeer BumpAllHostGenerations (~L504) operator (admin api) agent NEW poke (PokeAllHosts, fire-after-commit)
api/wg.go handleRegisterWGPeer BumpHostDesired (~L291) system (agent registration) agent none (deliberate) box's tunnel does not exist until it fetches the WG block — a poke is undeliverable by construction; the agent fast-tick SECONDARY (R-28) owns this leg
api/wg.go WG peer delete BumpHostDesired (~L422) operator agent none (deliberate) the mutation removes the transport
pbsdrheal Restage path system (reconciler) agent none (deliberate) no generation bump → the agent's 60 s pbsdr ticker is the pickup path (a poke is a verified no-op); internal/pbsdrheal/ byte-unchanged
api/handler.go reissueOnReenroll (F2 claim + F3 offsite, ~L1071) system (clean-slate re-enroll) controller NEW intent bump
web/configs.go bumpIntent ×11 (L564/633/657/702/742/779/796/842/859/876/1055) operator controller intent bump (pre-existing)
web/customer_reset.go bumpIntent (~L226) operator controller intent bump (pre-existing)
web/logtail.go bumpIntent (~L35) / web/logbundle.go bumpIntent (~L68) operator controller intent bump (pre-existing)

9. Observations (not acted on)

  • handlePBSDRReissueReissuePBSDR duplication. The operator button re-implements the reissue atom's tail (reissue → store secret → descriptor bump) instead of calling ReissuePBSDR, deliberately, to keep granular per-step HTTP error strings (the "keep in lockstep" comment marks the known duplication). Both now poke after their SetHostDesired. Not deduped (per spec forbidden-dedup); if ever unified, the handler's error-string granularity is the constraint to preserve.
  • pbsdrProvisionAtom is poke-free by design — its two callers poke on success (form path at L186, hook path at the new L312). This keeps the poke at the decision point (blocked vs minted) rather than inside the shared atom, matching the existing L186 structure.