STATUS.md: a plain-language operator page, and today's four decisions recorded

Documentation only — no code, no box, no build.

STATUS.md (repo root, 652 words / 67 lines): what works · what's broken ·
what we're working on · waiting on you · changed since. A VIEW of
OPEN-ITEMS.md, holding nothing of its own; not CONTEXT.md, and both files
now say why they stay separate. No R-n is the subject of a sentence —
identifiers are bracketed pointers only.

CONTEXT.md S-5 records the four operator decisions taken 2026-08-02
(D-a … D-d), none of them implemented:
  D-a merge mp1 into mp0 rather than resize it — before any external
      install, and D-c ships in the same step        → R-165
  D-b desired/observed app state in its own store, with the state-store
      safety rule verbatim                           → R-166 (BLOCKED)
  D-c customer fill warning + operator backup-failure alert → R-167
  D-d only DooPlex and Peti's box are protected      → target-selection.md

R-163 RE-FRAMED, not closed: the sizing question is withdrawn rather than
answered; the row survives as the record of the constraint until R-165
lands. R-156's papra referral RESOLVED — deployed nowhere, so the template
fix strands nothing; the docker ps evidence is recorded with its
provenance and its scope limit.

target-selection.md: two protected machines, everything else disposable.
ep0 is no longer Tier 2 but is not scratch (it holds the only off-premises
copy of real customer data) — flagged for explicit operator confirmation.
The demo-box backup-target fence drops from prohibition to stated cost,
because D-d spends that reference anyway.

CLAUDE.md gains an End-of-session checklist carrying the STATUS.md
maintenance rule and "a finding goes in OPEN-ITEMS.md first".
This commit is contained in:
2026-08-02 14:20:29 +02:00
parent 260a8f6e58
commit e994bf35d2
6 changed files with 314 additions and 146 deletions
+135 -129
View File
@@ -1,162 +1,168 @@
# REPORT — Hub v0.85.0: the Network card (2026-07-31)
# REPORT — `STATUS.md` created, and the 2026-08-02 operator decisions recorded (2026-08-02)
**Overwritten** per the standing rule. Pairs with **agent v0.119.0** (see `felhom-agent/REPORT.md`);
neither half is useful alone. Live evidence:
`documentation/audits/host-addresses-visible-2026-07-31.md`.
**Overwritten** per the standing rule. The prior contents (hub v0.85.0 Network card + v0.86.0 Copy
without reveal, 2026-07-31) have their durable record in `hub/CHANGELOG.md` and
`documentation/audits/host-addresses-visible-2026-07-31.md`; nothing was lost by this overwrite.
> The earlier v0.84.0 work this file previously described (break-glass Console access card) shipped,
> deployed and validated in the same session; its durable record is `hub/CHANGELOG.md` v0.84.0 plus
> `CONTEXT.md` ruling **S-4** and `documentation/backlog/` **R-133**.
**Class: documentation only.** No code, no template, no box, no build. Repo `felhom.eu` only —
`app-catalog-felhom.eu` was read for context and **not** modified. **No `CHANGELOG.md` entry exists
for this change and none is missing:** this repo has no root changelog, only per-area `hub/`,
`scripts/`, `website/` (`CLAUDE.md`), and this session touched none of those areas.
## 0. Follow-up shipped the same session — hub v0.86.0 (Copy without reveal)
Baselines: `felhom.eu` @ `260a8f6`, `app-catalog-felhom.eu` @ `fd7747d`. Part 1 was derived by reading
the register's rows and both ranking sections, not from memory.
**The operator hit a real defect in the v0.84.0 Console access card and it cost a login.** Copy was
`disabled` until a Reveal, so clicking it did **nothing, silently**; the clipboard kept its previous
contents — **another host's console password** — which was pasted into demo-hp's PVE login. It failed
with `password check failed for user (root)`, which reads exactly like a stale credential and sends
you diagnosing the wrong thing.
---
Diagnosis, read-only first: the vaulted password **matched the box's `/etc/shadow` hash**, the account
was unlocked and non-expiring, no TFA, no `pam_faillock` — and then it **minted a real PVE ticket**
(`POST /api2/json/access/ticket` → HTTP 200, `root@pam`, 367-char ticket). So the credential was
always good; only the clipboard was wrong. **That also closes the leg §7 of this report listed as
operator-only** — the capability-map row now records it.
## 1. `STATUS.md` — the file
**v0.86.0** makes Copy work without revealing (the *safer* default — the secret never renders, so it
cannot be shoulder-surfed or screenshotted) and closes three silent-failure branches in one eight-line
function: the disabled no-op, a missing `navigator.clipboard`, and an **ignored `writeText()`
rejection** that let the operator believe a refused write had succeeded. The success message now names
the host, because the clipboard is fleet-wide and "copied" alone cannot say for which box.
Root of `felhom.eu`, so it is the first thing visible. Sections in the specified order: what works ·
what's broken · what we're working on · waiting on you · changed since last update.
Tests 566 → 568, both pinning the regression; red-proof: re-adding `disabled` reproduces the shipped
bug. Deployed and live-verified (`Synced/Healthy`, image `:0.86.0`, button served without `disabled`,
retrieval URL defined exactly once, all four outcome messages present). Commits `670ec35` + `80f4739`.
**Word count: 652 total, 581 excluding the header block** (`wc -w`; the header carries the
view-not-source, not-`CONTEXT.md` and maintenance rules, which the spec requires). **That is over the
~500 target and it is a deliberate miss, stated rather than hidden.** Five passes took it from 819 to
652. Getting under 500 needed either dropping a mandated item or dropping the off-site-credential line
— the register's **top-ranked** open item and the largest customer-data exposure on it. Cutting the
biggest data risk to save forty words is the wrong trade on a page whose job is to show the operator
what is at stake. It is 67 lines and fits a screen. **If the operator disagrees, the line to cut is
the R-95/R-87 one** and the page drops to ~545.
## 1. Baselines
Content, in the operator's ranking: an app can stay off after a power cut, silently (R-157) · the
off-site copy can be erased by the box that wrote it, and has never been restored from (R-95, R-87) ·
three of fifty-three apps saved data where backups never looked (R-156) · 20 GB of backup space
against 50 GB of apps (R-163) · when that trips, one page says so and nothing alerts (R-158) · the
checker exists but a person has to remember it (R-161).
| Repo | `main` @ start of this half | Version before → after |
**Constraints honoured:** no `R-n` is the subject of any sentence — every identifier is a bracketed
pointer at the end of a line; no file paths, function names or version numbers appear; every broken
item is stated as what a customer or the operator would notice. Shipped, watching and
blocked-on-a-predicate rows (R-159, R-160, R-162, R-164) are absent by design.
**One deviation, flagged per standing rule 4.** "Waiting on you" is specified as *decisions only*, and
it carries one non-decision: **the hub password needs rotating** (R-132, owner Viktor). It is the only
thing on the register waiting on the operator with a live credential consequence, and omitting it from
the operator's own page to honour a section rule would be the letter over the point. It is labelled
*"a job, not a decision"* so the section's shape is not quietly eroded.
## 2. The decisions — where each one went
All four are in **`CONTEXT.md` as standing ruling S-5**, labelled **D-a … D-d** as in the discussion
and deliberately kept distinct from S-3's `D1…D6`. Open work is carried as backlog rows, per the
existing convention — no new home was created for either.
| Decision | Recorded | Work |
|---|---|---|
| felhom.eu (hub) | `b4edc087fa9a` | hub **v0.84.0 → v0.85.0** |
| felhom-agent | `6b5dade4dcc5` | agent **0.118.1 → 0.119.0** |
| **D-a** — merge the backup partition away (not resize) | `CONTEXT.md` S-5 | **R-165** (new) |
| **D-b** — desired/observed app state, own store | `CONTEXT.md` S-5 | **R-166** (new, `BLOCKED`) |
| **D-c** — storage monitoring + backup alerts | `CONTEXT.md` S-5 | **R-167** (new) |
| **D-d** — only DooPlex and Peti's box are protected | `CONTEXT.md` S-5 | `runbooks/target-selection.md`, this session — no row; the decision *is* the change |
| **Maintenance rule** (Part 3) | `STATUS.md` header **and** `CLAUDE.md` § End-of-session checklist | — |
## 2. The ask, and what it turned out to be
Recorded verbatim inside D-b, because it is the decision's binding constraint: *losing the state store
must never cause an app to be deleted, restarted wrongly, or reported healthy when it is not — the
worst acceptable outcome is re-running a backup that already ran.* Its two "establish before speccing"
items are carried on R-166 as the reason that row is `BLOCKED` rather than `READY`.
*"Show the LAN and WireGuard IPs on the hosts page — it looks like it is not visible anywhere."*
Nearly exact, and the two halves were very different jobs:
**Deliverable 5 asks for "the five decisions".** Part 2 defines four (D-a … D-d); the fifth deliverable
line is the Part-3 maintenance rule, and it is in the table above. Nothing else in the task reads as a
fifth decision — flagged rather than invented.
- **WireGuard** — the hub already had it (`wg_peers.assigned_ip`, `GetWGPeerForHost`, **no UI
consumer**). It was visible in exactly one place, `/offsite`'s peer table keyed by pubkey, so you
could go peer→host and never host→peer.
- **LAN** — **not renderable at all.** `HostReport`/`HostMetrics` carried no address of any kind, so
this needed a cross-repo agent release, not an edit. Confirmed against the struct and against a
live box before proposing anything.
**D-a's two conditions are recorded as conditions, not commentary:** it changes the disk layout so it
must land **before any external install**, and it removes a wall that currently fails safely so
**R-167 ships in the same step, never after**. R-165 restates both; R-167 names R-165 as the thing it
gates.
Operator chose: both together as one arc, with the `addresses[]` (iface + cidr) shape.
**None of D-a, D-b or D-c is implemented.** No controller, agent, installer or hub file was opened for
editing.
## 3. Files (hub half)
## 3. R-163 re-framed, and R-156's papra referral resolved
- `hub/internal/web/hosts.go``parseHostAddresses`, `hostAddressView`, `hostNetworkView`,
`hostNetwork`, `minAgentForAddresses`, `hostDetailData` +1 key
- `hub/internal/web/templates/host_detail_body.html` — the Network card
- `hub/internal/web/hosts_network_test.go` (**new**)
- `hub/internal/api/testdata/host-report.golden.json` — the cross-repo contract, moved in lockstep
- `hub/CHANGELOG.md`, `REUSE.md`, `manifests/hub.yaml`
- `documentation/architecture/00-capability-map.md` (new row),
`documentation/audits/host-addresses-visible-2026-07-31.md` (new)
**R-163 is re-framed, not closed** — as instructed. State went `WAITING-ON-OPERATOR — the ratio is a
tier-sizing ruling``RE-FRAMED 2026-08-02 — open, no longer waiting on a ratio`; "Blocked on" went
from `the operator's sizing decision` to a pointer at R-165; owner `operator``CC`. The cell now says
the sizing **question is withdrawn rather than answered**, that the row survives as the record of the
constraint until the merge lands, and that the original finding follows unchanged. The intake ranking
(item 4) was updated with it, and records that **R-165 inherits R-163's rank and is the highest-ranked
item that must land before any external install**.
## 4. Commits pushed to `main`
**R-156's papra referral is resolved.** The referral existed because moving a mount relocates live data
out from under a running app; with papra deployed nowhere there is nothing to strand, so the cheaper
leg — the template mounts `/app/app-data` — is takeable without waiting on upstream.
| Repo | Commit | What |
|---|---|---|
| felhom-agent | `14642e3` | v0.119.0 — the host report carries the box's addresses |
| felhom-agent | `4663df7` | REPORT |
| felhom.eu | `e07d90f` | hub v0.85.0 — Network card |
| felhom.eu | `37f7ff6` | manifests: hub 0.84.0 → 0.85.0 |
**The provenance is recorded with the claim, because it decides the row.** The evidence is
`docker ps -a` on **demo-hp's guest 9201** returning empty, **supplied with the task**; this session
**did not re-measure** — it is documentation-only and every box was fenced. The recorded scope is
honest about its edge: it covers the one guest papra was convicted on in Campaign 10, and **no other
customer's guest was enumerated**, so the row instructs the task that edits the template to re-check
first. Next action on the row is the catalog edit plus `catalog_gates.py`, explicitly not done here.
## 5. Tests + red-proofs (hub half)
## 4. `target-selection.md` per D-d
Suite **559 → 566** (+7). `go build ./... && go vet ./... && go test ./...` **rc=0**, run separately
from the commit per standing rule 1.
The rule at the top is now D-d: **two protected machines, everything else disposable**, with the
correction stated as a correction — the earlier caution was costing sessions and pushing drills onto
DooPlex. The tier table's Tier 2 row is DooPlex + Peti's cluster "and, by D-d, nothing else".
| Test | Asserts | Result |
|---|---|---|
| `TestNetwork_A_LANAddressRendered` | the LAN address + its interface reach the page | PASS |
| `TestNetwork_B_WireGuardAllocatedAndConfirmed` | WG shown once, marked confirmed, not duplicated as a plain row | PASS |
| `TestNetwork_C_AllocatedButBoxDoesNotHoldIt` | the drift case renders `not confirmed by the box` | PASS |
| `TestNetwork_D_OldAgentSaysUnknownNotNone` | an old agent renders UNKNOWN, never "no addresses" | PASS |
| `TestNetwork_E_NoPeerAllocated` | no peer → says so; the box's wg address is not silently dropped | PASS |
| `TestNetwork_F_NoReportYet` | never-reported host says "waiting", not "no addresses" | PASS |
| `TestParseHostAddresses_RealWire` | parses the real wire; empty/malformed degrade without panic | PASS |
**Two consequences the decision did not name, both handled visibly rather than silently:**
**The report fixture is the REAL wire** — the `addresses` block copied out of
`felhom-agent --selftest=hub` on demo-felhom running 0.119.0. A hand-written shape would have proved
only that the parser matches my own idea of the format.
- **`ep0` + the Hetzner Storage Boxes.** D-d's protected list has two machines and ep0 is not one, so
the page no longer calls it Tier 2. It is **not** thereby scratch: it holds the PBS-DR datastore and
the restic copy of a real customer's data — the only off-premises copy that exists. Read the narrow
way (not protected, but not wipeable), using the page's own *fences-name-acts* rule, and **flagged
in the page for the operator to confirm explicitly.**
- **The shared "do not re-point either backup target" fence** on the two demo boxes was **downgraded
from a prohibition to a stated cost**, because D-d makes both boxes freely reinstallable, which
spends that reference configuration just as thoroughly — keeping the fence would have left the page
self-contradicting. The reason survives: know you are spending the regression reference, and put the
box back.
| # | Mutation | Observed | Reverted |
|---|---|---|---|
| A | `hostDetailData` ships an empty `hostNetworkView{}` (the inert-seam shape) | `FAIL … the LAN address is not on the host page — the feature shows nothing` | yes |
| B | `WGConfirmed` set unconditionally from the allocation | `FAIL … an allocated-but-unheld peer renders as healthy — the drift is invisible` | yes |
| D | delete the `AgentTooOld` branch | `FAIL … an old agent's silence is not explained` | yes |
Also corrected while in the file: the *fences-name-acts* example cited the fence this edit removed, and
demo-hp's access line asserted "no baked SSH key" — which **R-129** records as measured false on
2026-07-31. It now points at R-129 instead of sending the next session to the hub vault for a
credential it may not need.
**Red-proof A took two attempts, and the first one proved nothing.** The initial mutation
(`_ = network` inside a map literal) did not compile, so the "failure" was a build error, not a test
result. Recorded because a red-proof that fails to build is indistinguishable from one that lands if
you only read the exit code.
## 5. The maintenance rule
## 6. Deployed
In two places, as specified: the `STATUS.md` header block, and a new **`## End-of-session checklist`**
in `CLAUDE.md` — which also gathers the couplings that were previously scattered (CHANGELOG + REPORT,
REUSE, the capability map's own end-of-session line, S-1's architecture coupling) and closes with *a
finding goes in `OPEN-ITEMS.md` first, never only in a report, an audit or `STATUS.md`*.
Image `gitea.dooplex.hu/admin/felhom-hub:0.85.0` built on DooPlex and pushed; `manifests/hub.yaml`
bumped in git; ArgoCD hard-refreshed and **deliberately synced** (never `kubectl set image`).
`CONTEXT.md` gained a header block stating why it and `STATUS.md` are separate — same subjects,
different readers, and `STATUS.md` holds nothing of its own. `STATUS.md` says the same from its side.
- ArgoCD app `felhom`: **Synced / Healthy**
- `deploy/hub` image: **`gitea.dooplex.hu/admin/felhom-hub:0.85.0`**
- Rollout: `deployment "hub" successfully rolled out` (rc=0)
- Startup log: `2026/07/31 08:50:57 [INFO] felhom-hub 0.85.0 starting`
## 6. What could not be translated into plain language
**One sequencing note worth keeping:** the first sync fired before ArgoCD had re-read git, so it
reported `Synced/Healthy` while still running 0.84.0 — a green pair that meant nothing. The fix is to
wait until `.status.sync.revision` equals the pushed commit **before** trusting the sync, which the
final run did.
Asked for explicitly, because an untranslatable row usually means the row itself is unclear.
## 7. Live validation
- **R-29** — *"gates are enforced nowhere"*. The class is stateable ("we have checks nobody runs"), but
its instances are four differently-broken scripts across two repos with no shared consequence, so
every plain sentence either says nothing or misstates one instance. **R-161 is its readable
fragment**, which is why R-161 is on the page and R-29 is not.
- **R-123 / R-125** — process findings about how the register and how tests are written. Real, and they
belong on the register; there is no customer-visible symptom to lead with, so they have no honest
first sentence for this page. They are not "broken" in the operator's sense.
- **R-133 (the plaintext break-glass credential)** — translatable, and left off only for space. It is
the strongest candidate for the next update if something else closes.
- **R-115 vs R-110** — separate rows, one plain-language paragraph. Merged into a single "Waiting on
you" bullet carrying both pointers, because two adjacent bullets about publishing read as one item
the operator has already half-decided.
**Method: endpoint-level** (no browser on DooPlex), against the deployed hub, cross-checked
against `ip addr` read directly over SSH on both boxes.
**A register defect found while reading, filed here because the fix is not mine to guess: `R-133` is
used TWICE** — `OPEN-ITEMS.md:80` (duplicate `domain` values accepted by the hub) and `:86` (the
plaintext break-glass credential). Two different findings, one ID, both `READY`. One needs renumbering,
and which one is the operator's call since both are cited from elsewhere (`CONTEXT.md` S-4 cites the
credential one).
| Host | Agent | Network card, live | Matches ground truth? |
|---|---|---|---|
| `demo-felhom-8363b5` | 0.119.0 | WG **10.77.0.2 confirmed**; `vmbr0 192.168.0.162/24`, `tailscale0 100.70.170.35/32` + ULA | **yes, exactly** |
| `demo-hp-bb76ea` | 0.119.0 | WG **10.77.0.3 confirmed**; `vmbr0 192.168.0.87/24`, `tailscale0 100.76.96.79/32` + ULA | **yes, exactly** |
| `drill-r50-0a4f9a` | 0.113.0 | WG 10.77.0.4 **allocated**; *"agent (0.113.0) does not report its addresses — they are unknown, not absent. Needs 0.119.0 or newer."* | correct UNKNOWN rendering |
| `sess-f-2670b5` | old | same UNKNOWN rendering | correct |
## 7. Files changed
Correctly absent from every card: `lo`, `vmbr9`'s `169.254.253.1` island literal, every `fe80::`.
| File | Change |
|---|---|
| `STATUS.md` | **new** — the operator page |
| `CONTEXT.md` | S-5 (D-a … D-d); header note on the `STATUS.md` separation |
| `documentation/backlog/OPEN-ITEMS.md` | R-165/166/167 filed; R-163 re-framed (state, blocked-on, owner, ranking); R-156's referral resolved with its provenance |
| `documentation/runbooks/target-selection.md` | D-d rule; tier table; ep0; the demo-box fence; two stale lines |
| `CLAUDE.md` | new `## End-of-session checklist`, carrying the `STATUS.md` maintenance rule |
**A fifth host 404'd and it was not a bug.** `sess-g-e687c3` appeared in a host list I read 30
minutes earlier and returned 404 when I fetched its page; the concurrent session had deleted it as
part of its scratch-customer cleanup. Chased to ground before reporting rather than filed as a defect.
## 8. Not yet validated / not covered
- **A box with two LAN bridges** — handled by design (one row per interface) and unit-tested; no such
box exists to observe.
- **A real WireGuard drift** — the `not confirmed` branch is unit-tested and red-proofed but never
seen in production, because no live peer is currently unapplied.
- **IPv4-only / IPv6-only hosts** — every box here is dual-stacked via tailscale's ULA.
## 9. Teardown
This run provisioned nothing — no guest, no VM, no scratch customer, no external resource. The only
mutations are two in-place agent upgrades (previous binaries retained as `.bak-0.118.1` on both
boxes), the Day-0 manifest agent vouch, and the hub image bump.
## 10. Backlog
- **Opened: none.** **Closed: none.**
- **R-115 / R-121 touched, not closed.** This release walked all three legs deliberately —
deployed, **published** (round-trip verified) and **vouched** (read back) — so no new drift
instance was created. The finding those rows record is that nothing *notices* when the legs
diverge, which is unchanged. One small contribution: the Network card now surfaces
"this host's agent is older than 0.119.0" per host, which is a per-field hint, not the
installed-vs-vouched checker R-121 asks for.
- **Concurrent session:** every path was staged explicitly; no foreign WIP was swept into any commit.
Nothing was built, deployed, published or touched on any host. Every claim about the register above is
a claim about pushed source in this repo at the commit below.