R-167/R-158 shipped and proven live (controller v0.191.x, hub v0.89.0): two new capability-map rows PROVEN-LIVE with live citations, and 07-backup-architecture.md §7.5's closing claim "nothing warns when an app crosses the line" is now false and rewritten (S-1: an architectural contract changed in the same session). §7.5 also gains the caveat that its size bound is ONE BOX'S, not the fleet's. Part 3 SPIKE (audits/SPIKE-r165-mp1-merge-2026-08-02.md): M1-M5 measured, NO layout touched. Three findings the merge session must not re-derive: "the layout" is not one thing (200G/50G vs 50G/20G vs 16G/8G); mp1 is a BULKHEAD and not only a ceiling, so after the merge an overflow reaches /var/lib/docker; the golden fails closed on the split in four places. D-a's condition (1) is currently SATISFIED — no external box is in the hub's register, and both demo boxes are Tier 0 and reinstallable. Recommendation given, choice NOT made — it ends at the operator's ruling. CONTEXT.md S-11 (D-c's routing, and why R-158's own backup_failed proposal was overruled) and S-12 (the monitoring landed BEFORE the merge). STATUS.md gains the plain-language section and the merge decision, with two older entries trimmed so the page did not grow. New rows R-174 (closed same session), R-175, R-176, R-177; each ID grepped free before minting.
9.9 KiB
REPORT — hub v0.89.0: the two halves of decision D-c, plus the R-165 merge spike (2026-08-02)
Overwritten per the standing rule. The prior contents (R-168, the CI runner, same day) have their
durable record in scripts/CHANGELOG.md and CONTEXT.md S-8/S-9/S-10.
Companion report: felhom-controller/REPORT.md holds the controller side (v0.191.0/.1/.2), the
full red-proof table, the Hungarian copy, and the live evidence for all three flows. This file covers
the hub change, the documentation coupling, and Part 3's spike.
1. Baseline drift — recorded, because the task's §1 was wrong
The task targeted hub v0.87.0 → v0.88.0. On arrival main was at 8ef92a3f with hub v0.88.0
already shipped (R-172, the WAL fix), not d5774d318941/v0.87.0. Target corrected to v0.89.0.
Highest register ID in use was R-173, not R-171.
2. Hub change (v0.89.0)
One new event type, not two. The task called for a new customer-facing type and a new
operator-only one. Reconnaissance found disk_warning/disk_critical already allowlisted here, with
Hungarian customerMessages, in the controller's DefaultEnabledEvents and behind a UI checkbox —
and with no producer in any repo. The operator chose to wire that inert pair rather than mint a
near-duplicate, so only the operator type is new.
| Change | File | Why |
|---|---|---|
+ "recovery_unit_capture_failed" |
internal/api/handler.go (allowedEventTypes) |
without it the controller's POST 400s and the event vanishes |
+ "recovery_unit_capture_failed" |
internal/notify/dispatcher.go (operatorOnlyEvents) |
this is what makes it operator-only; the allowlist does not, and v0.78.0 claimed otherwise and shipped the defect |
- customerMessages["disk_warning"], - ["disk_critical"] |
internal/notify/templates.go |
FormatCustomerEmail PREFERS the entry over the message, so a static template would discard the drive label and the free-space figures the controller now sends. Same reason offbox_enlarge_blocked and disk_health_degraded have none |
+ func IsOperatorOnly |
internal/notify/dispatcher.go |
lets the api package pin BOTH registers in ONE test; checked separately, allowlisted-but-not-operator-only is invisible. Read-only — the register stays unexported so nothing can widen it at runtime |
REUSE.md §5 "new event type" rewritten |
REUSE.md |
it told readers to always add a customerMessages entry, which is wrong for operator-only types and harmful for dynamic-message ones |
Tests 574 → 579, full suite green (go build ./... && go vet ./... && go test ./...), all five
repo_gates.py gates OK.
Red-proof (Scenario G), demonstrated not argued: removing recovery_unit_capture_failed from
operatorOnlyEvents fails two tests, one reading "a customer was emailed the OPERATOR-ONLY
recovery_unit_capture_failed (customer@example.com)". The dispatch test runs under the breaking
configuration — the customer has the event enabled and an email set — because that is the only
configuration in which the missing entry is visible.
Live (guest 9201 → hub): both event types accepted and stored; operator | sent; and the positive
observable customer | recovery_unit_capture_failed | skipped | operator_only read from
notification_log. The customer half: customer | disk_warning | sent and customer | disk_critical | sent with the dynamic Hungarian intact.
Deploy: GitOps only — manifests/hub.yaml bumped 0.88.0 → 0.89.0 (6d359a5), pushed, then a
deliberate ArgoCD hard-refresh + sync. Never kubectl set image. App felhom Synced / Healthy,
deploy/hub rolled out, running gitea.dooplex.hu/admin/felhom-hub:0.89.0, startup log clean.
3. Part 3 — the R-165 spike. M1-M5 each answered; nothing was changed.
Full document: documentation/audits/SPIKE-r165-mp1-merge-2026-08-02.md. No partition was created,
resized, moved or deleted; no golden rebuilt; no guest config edited. ep0 and Peti's box were not
contacted (D-d, runbooks/target-selection.md).
M1 — what is actually there. ANSWERED, and it contradicts the architecture doc.
| demo-felhom | demo-hp | golden default | |
|---|---|---|---|
mp0 /var/lib/docker |
200 G (13 G used) | 50 G (5.4 G used) | 16 G |
mp1 /mnt/sys_drive |
50 G (2.0 G used, 5%) | 20 G (92 M used, 1%) | 8 G |
§7.5 documents the appliance as mp0 50G / mp1 20G — that is demo-hp exactly and not demo-felhom.
Any merge plan expressed as a fixed pair is already wrong for one of the two boxes that exist. §7.5's
headline bound ("≈ 19 GB … ≈ 10 GB") is derived from mp1 = 20 G and is therefore one box's, not
the fleet's → R-175, filed and §7.5 annotated in this session.
M2 — what lives on mp1. ANSWERED, and it is not only backups. Four things would move:
Tier-1 units of driveless apps (269 M, ~30 apps on demo-felhom), Tier-2 mirrors (1.7 G — i.e. the
MAJORITY is Tier 2, not Tier 1), the userdata/import drop zone which lives on the system drive by
contract (R-75), and the system-data userdata namespace. Observed fill is 5% / 1%: the constraint
is a ceiling problem, not a current-fill one.
M3 — which merge shapes exist. ANSWERED for three shapes, with ONE item explicitly unmeasured.
The golden fails closed on the split in four places, not one (build-golden.sh:126,130
separate-mount asserts + :315,319 vzdump-exclusion guards). The archive scope rootfs+mp0+mp1 stays
complete after a merge (the data moves onto mp0). mountParity holds for new archives. Unmeasured
and reported as such: whether a pre-merge archive restore-tests into a merged guest — reading
mountParity says it should, but that is reasoning from source about an unvalidated mechanism, which
this project has got wrong four times → R-176.
M4 — the bulkhead. ANSWERED, and it is the important one. mp1 is not only a ceiling: today an
overflow is refused per app with the last good unit byte-identical and cannot reach
/var/lib/docker. After the merge it can, and a full Docker data-root is a stopped box, not a slow
one. Four replacements costed — a reserved block percentage, a refusal threshold in the capture
path, a project quota, or deeming R-167's warnings sufficient — with the trade-off of each.
Deliberately not chosen: this is the operator's ruling.
M5 — existing boxes. ANSWERED for the measurable population; one part honestly UNMEASURED. The
hub's /hosts register holds four hosts, two ONLINE, both demo boxes — and both are Tier 0,
therefore reinstallable rather than migratable (D-d), so migration cost for the measurable population
is zero. D-a's condition (1) — "before any external install" — is currently SATISFIED, which
makes this the cheapest this decision will ever be. peti-felhom exists as a customer with NO host
in the register, so its layout is not knowable from the hub and the box was not contacted; whether it
needs converting or reinstalling is the operator's information. The in-place migration procedure has
never been rehearsed, so "is the box restorable at every point of it?" is currently unknown → also
R-176.
Ranked options and recommendation: (1) S1 — one volume with the two paths as directories — plus
B2, a refusal threshold in the capture path, shipped as a fresh-install shape with the demo boxes
reinstalled; (2) S1 + warnings only; (3) S3, grow mp1 and keep the split (D-a's rejected baseline,
measured for comparison); (4) S2, two mounts on one pool — not recommended at all, it satisfies
every assertion while delivering none of the benefit and converts a clean per-app refusal into a
shared-pool exhaustion neither df can see coming.
STOPPED at the operator's question, per the task. The merge is next session's supervised work.
4. Documentation coupling
| File | Change |
|---|---|
documentation/backlog/OPEN-ITEMS.md |
R-158 closed (by R-167 — no second row for the same wire); R-167 closed; R-165 updated with M1-M5 + the operator question, stays open; 4 new rows R-174/175/176/177 |
documentation/backlog/ROADMAP.md |
R-158 collapsed to a shipped one-liner; R-167 added as shipped; R-165 added as spiked/waiting-on-operator |
documentation/architecture/00-capability-map.md |
two new rows, both PROVEN-LIVE with live citations |
documentation/architecture/07-backup-architecture.md §7.5 |
S-1: the contract changed in the same session. The section's closing claim "nothing warns when an app crosses the line" is now false; the alerting is written in, and the one-box-vs-fleet caveat added |
CONTEXT.md |
S-11 (D-c's routing, and why R-158's own backup_failed proposal was overruled) and S-12 (the monitoring landed before the merge, not with it) |
STATUS.md |
new plain-language section; the merge decision added to Waiting on you; two older entries trimmed so the page did not grow — one screen, per its own rule |
REUSE.md |
the "new event type" extension point rewritten (see §2) |
5. Register IDs
Opened: R-174, R-175, R-176, R-177. Each established free by
grep -ro "R-17n\b" documentation/ *.md → 0 hits, run before minting.
Closed: R-158, R-167, R-174. Updated, still open: R-165, R-163 (unchanged — it stays the
record of the constraint until the merge lands).
6. CI — run ids and conclusions
| Repo | Final commit | Run | Conclusion |
|---|---|---|---|
felhom-controller |
9a3c485 |
see below | see below |
felhom.eu |
(docs commit) | see below | see below |
(Filled in from …/actions/tasks?limit=3 after the final push — see the closing section of the
session output; a green CI that was not looked at is an assumption, not an observation.)
7. --no-verify
Not used anywhere. Every push in this session ran .githooks/pre-push (repo_gates.py --fast /
controller_gates.py --fast) and passed.