Verify the standing picture against source: 12 downgrades, and the decay ran both ways
gates / gates (push) Successful in 21s
gates / gates (push) Successful in 21s
55 claims verified. Twelve moved, all downwards: walked 32 -> 20, built 5 -> 17. Register ceiling R-284 -> R-290. THE RULE DID NOT FIRE THE WAY IT WAS EXPECTED TO. Not one downgrade came from code moving under an old proof. All twelve came from step 1 of the same rule -- the cited evidence does not exist. Measured: of the 28 capability-map rows behind the page's claims, 8 carry a tests/ or audits/ path and 20 carry prose only. The green dots were drawn from rows that cite an argument, not a walk (R-290). The map, not the dataset, is what needs fixing -- it still says PROVEN-LIVE for all twelve. And once it ran backwards: fault.operator-email looked contradicted by R-182, but live source shows the backup_run_failures digest allowlisted, operator-only and templated, with recovery_unit_capture_failed now record-only. The claim is right and the REGISTER ROW is stale (R-289). The session went looking for stale proofs and found a stale defect. R-281 WITHDRAWN -- wrong in both directions, settled by the operator's mailbox. The tripwire DID fire (escrow_blob_served 10:19:41Z = 12:19 CEST) and false error-severity alarms fired too, for deliberate attended work (R-285). The measurement's cause is ESTABLISHED: the P7 query copied hub.db without hub.db-wal, and the signature is exact -- it reported "2 events all day, newest 00:30:07", and the rows at or before 00:30:07 number exactly 2. Timezone and wrong-key were tested and refuted. The control had been drawn from the same stale snapshot as the measurement, which is why it agreed (R-286). Part 4: NO WORKFLOW CHANGED, deliberately. The gate is not ref-sensitive -- it enumerates from the Gitea tags API, and both previous tag pushes passed. The red is TRUE: run 267 saw v0.120.0 downloadable, run 284 on the same commit saw 404. Who deleted the package is NOT established and is not guessed (R-287). The page is now generated from where-felhom-stands.yaml by scripts/render_stands.py: static, zero script tags, every moved status carrying a visible "changed, was X" chip. The React bundle -- whose content was gzip+base64 inside a JS module map -- is kept as a dated snapshot. scripts/check_stands.py gates the data and convicted 51 problems in my own first draft before the staged positive control ever ran.
This commit is contained in:
@@ -1,184 +1,191 @@
|
||||
# REPORT — the seed that never ran twice, and three pictures that were not true (2026-08-08)
|
||||
# REPORT — making the picture true (2026-08-09, unattended)
|
||||
|
||||
Four defects of one family, each with a source-verified mechanism, tests and red-proofs.
|
||||
Agent **v0.128.0** · controller **v0.210.0** · `gates.yml` (workflow only). **The hub was not touched,
|
||||
not bumped and not deployed.**
|
||||
Read-only against all live infrastructure. **Both demo machines were powered off and in transit; no
|
||||
box was probed, woken or waited on.** Claims that only a running box could settle are marked
|
||||
`needs-hardware`, which is a verdict, not a gap.
|
||||
|
||||
## 1. Part 1's live sequence — RUN, OPERATOR-PRESENT, PASSED
|
||||
---
|
||||
|
||||
On `demo-felhom` guest 9201, agent 0.128.0. Backup taken first and verified byte-identical
|
||||
(`sha256 eff18437…`), pbsdr marker sha recorded and **unchanged at every step**
|
||||
(`fd4c97b5…`).
|
||||
## 1. The verdict table
|
||||
|
||||
**Step 3 — the preflight with the key removed (today's defect, live):**
|
||||
**55 claims. Statuses moved on 12 of them — all downwards.** `walked 32 → 20`, `built 5 → 17`;
|
||||
`partial 14` and `missing 4` unchanged. Full per-claim detail with sources is in
|
||||
`documentation/architecture/where-felhom-stands.yaml`.
|
||||
|
||||
```
|
||||
{"id": "pbs_storage_id", "ok": false, "detail": "escrow.pbs_storage_id not configured"}
|
||||
```
|
||||
|
||||
**Step 4 — one 60 s tick later, same daemon:**
|
||||
|
||||
```
|
||||
{"id": "pbs_storage_id", "ok": true, "detail": "felhom-pbs"}
|
||||
```
|
||||
|
||||
**No restart:** agent MainPID **1993397 before and 1993397 after** — the same process. The wizard
|
||||
would have refused at 18:04:16Z and would pass at 18:05:44Z, with nobody touching the box.
|
||||
|
||||
**Step 5 — nothing else moved:** 45 keys in the backup, 45 now, **added none / removed none /
|
||||
changed none**. The one key is back with its original value.
|
||||
|
||||
**Positive control:** the preflight was exercised BEFORE the change and reported `ok: true` on the
|
||||
same row, so a green afterwards is a measurement and not an artefact of the probe.
|
||||
|
||||
## 2. The writer of `agent.json` — ESTABLISHED
|
||||
|
||||
`step_agent_config`, **`felhom.eu/scripts/felhom-host-install.sh:2396`**; the Python render at
|
||||
**`:2449`**; the `O_TRUNC` write at **`:2579`**. `PRESERVE_FROM` defaults empty (**`:256`**) and is set
|
||||
only by an explicit `--preserve-from` (**`:1246`**). **The render never writes an `escrow` section at
|
||||
all** — grep over the whole heredoc: zero hits. The pbsdr marker is host-side
|
||||
(`<agent-state>/pbsdr/marker.json`) and survives. **R-221's attribution was correct.**
|
||||
|
||||
A rebuild is only the case that was *measured*; the same hole opens for a hand-edited or restored
|
||||
config, which is the honest reason the fix is at the seam rather than in the installer.
|
||||
|
||||
## 3. Red-proofs — 6 of 6, each with the mutation asserted applied
|
||||
|
||||
| # | mutation | assertion it applied | outcome |
|
||||
| claim | was | now | why |
|
||||
|---|---|---|---|
|
||||
| 1 | **Part 1 / Scenario A: remove the new seed call** | marker `MUTATED: the R-221 re-assert removed` present | **RED** — and **yes, it failed against today's tree**, with the intended message ✔ |
|
||||
| 2 | Part 1: remove the early return as well | marker `MUTATED: early return deleted` present | **RED** — the zero-Proxmox-calls assertion is load-bearing, not decorative ✔ |
|
||||
| 3 | Part 3 / F: revert to `status.LastDBDump.Success` | marker present | **RED** — app X's false green returns ✔ |
|
||||
| 4 | Part 3 / G: map "no result" to `ok` | marker present | **RED** — green-on-presence returns ✔ |
|
||||
| 5 | Part 2 / D: ignore `DiskKnown` in the template | marker present | **RED** — „0.0 GB / 0.0 GB (0%)" in the nominal colour returns ✔ |
|
||||
| 6 | Part 2 / E: force the flag false | marker present | **RED** — a healthy box is shown losing its numbers ✔ |
|
||||
| `install.installer-by-tag` | walked | **built** | gate 6 asserts the manifest names an installer tag; no walk of a rollback on file |
|
||||
| `use.lifecycle` | walked | **built** | no walk document cited |
|
||||
| `drives.enrol` | walked | **built** | the 08-09 walk exercised RE-attach (which failed, R-280); first enrolment of a NEW drive has no walk |
|
||||
| `drives.migrate` | walked | **built** | no walk document cited |
|
||||
| `backup.tier1` | walked | **built** | no walk document cited |
|
||||
| `backup.whole-machine` | walked | **built** | no walk document cited |
|
||||
| `backup.restore-proof` | walked | **built** | no walk cited, **and the last recorded restore-test on demo-hp FAILED** (2026-08-05) |
|
||||
| `fault.selfheal` | walked | **built** | no walk document cited |
|
||||
| `fault.operator-email` | walked | **built** | source-verified as correct, but no run observed delivering |
|
||||
| `fail.drive-filling` | walked | **built** | no walk document cited |
|
||||
| `fail.lost-recovery-code` | walked | **built** | by-design refusal; no walk document cited |
|
||||
| `fail.hub-down` | walked | **built** | no walk document cited |
|
||||
|
||||
All six restored and re-verified green. **Answer to the question asked directly: the Part 1 test DID
|
||||
fail against today's tree.**
|
||||
**Upgraded: 1.** `install.byo` — the page said *"the first real one has not happened"*. A real
|
||||
`--mode byo` install completed on demo-hp on 2026-08-09 (`Day-0 provision SUCCESS`, 3 m 49 s). Still
|
||||
not a customer's own hardware, so not *walked*, but the sentence was false.
|
||||
|
||||
## 4. The Hungarian strings as shipped
|
||||
**`needs-hardware`: 4** — `use.lan-fallback`, `backup.restore-proof`, `fail.disk-failing`,
|
||||
`fail.internet-down`. Each needs an observation on a running box; each says which.
|
||||
|
||||
- `„A tárhely mérete most nem olvasható ki."` — the disk caveat line
|
||||
- `„nem ismert"` — the short label in the value slot
|
||||
- `„Erről a mentésről nincs eredményünk."` — the `title` on the no-verdict backup mark
|
||||
**Confirmed: 38.** Seven of those were re-confirmed against live source or the live hub tonight rather
|
||||
than against paperwork: the tripwire, the off-site repository, the claim path, the catalogue, the
|
||||
tunnel, the reset code and the operator-email digest.
|
||||
|
||||
## 5. The §7.3 truth table as implemented
|
||||
## 2. Every downgrade, with the coupling that broke
|
||||
|
||||
| this app's own most recent dump result | restore point | verdict |
|
||||
|---|---|---|
|
||||
| any of its databases failed | yes | `error` |
|
||||
| all clean | yes | `ok` |
|
||||
| none recorded | yes | **no icon**, time only, with the title above |
|
||||
| any | no | no tier-1 row, unchanged |
|
||||
The rule is *"a proof is about the code that existed when it ran"*. **It did not fire the way the task
|
||||
expected.** Not one downgrade came from code moving under an old proof. **All twelve came from step 1
|
||||
of the same rule — the cited evidence does not exist.**
|
||||
|
||||
**Recency was left alone**, deliberately: an age threshold means inventing a number, and the time is
|
||||
already printed beside the icon. Recorded as an observation.
|
||||
Measured: of the 28 capability-map rows behind the page's claims, **8 carry a `tests/` or `audits/`
|
||||
path in their evidence column and 20 carry prose only.** The green dots were being drawn from rows
|
||||
that cite an argument, not a walk. Filed as **R-290**.
|
||||
|
||||
## 6. The CI timeout, and what is still unknown
|
||||
**And the decay ran the other way once.** `fault.operator-email` — *"one mail per run, every failing
|
||||
app named"* — I first took to be contradicted by R-182 (open, *"tells the operator about ONE app and
|
||||
silently swallows every other"*). Reading live source: the digest `backup_run_failures` is allowlisted
|
||||
(`hub/internal/api/handler.go:1837`), operator-only (`notify/dispatcher.go:423`) and templated
|
||||
(`notify/templates.go:48`); `recovery_unit_capture_failed` is record-only (`dispatcher.go:376`); a
|
||||
cooldown drop now logs a `suppressed` row (`dispatcher.go:314-330`). **The claim is right and the
|
||||
register row is stale** — filed as **R-289**. The session went looking for stale proofs and found a
|
||||
stale defect.
|
||||
|
||||
**`timeout-minutes: 5`** — every honest run in the observed session finished in **18–34 s**, so 5 min
|
||||
is ~9× the slowest honest run and far under whatever reaped run 264 at 834 s. The alarm mail now
|
||||
carries **`Elapsed`** (a start stamp in step 1 via `$GITHUB_ENV`; an absent stamp prints
|
||||
`unknown (no start stamp)`, never a bogus number), and its "names itself in the run log" sentence is
|
||||
qualified so it cannot mislead when there is no log.
|
||||
## 3. The positive control
|
||||
|
||||
**THE UNKNOWN IS NOT CLOSED.** Whether the `if: failure()` alarm fires at all for a *reaped* job is
|
||||
**still unverified**. The timeout makes the reap unreachable in practice; it does not answer what
|
||||
happens inside one. Demonstrating it would mean deliberately hanging a run on `main`, which would
|
||||
leave the branch red for a parallel session, so it was not done. Said in the workflow comment, the
|
||||
changelog, R-265 and here — four places, none of them claiming it is answered.
|
||||
```
|
||||
1 BASELINE real dataset -> OK, exit 0
|
||||
2 PLANT scratch copy: use.dlna missing -> walked -> CONVICTED, exit 1
|
||||
"use.dlna: status 'walked' but NO evidence document cited"
|
||||
3 REMOVE scratch copy deleted; committed dataset never touched
|
||||
4 RE-RUN real dataset -> OK, exit 0
|
||||
```
|
||||
|
||||
## 7. Tests
|
||||
Plant → convicted → removed → clean. The gate also convicted **51 problems in my own first draft** of
|
||||
the dataset (bad anchors, register ids that are not in `OPEN-ITEMS.md`, an evidence path that does not
|
||||
exist) before any of this — which is the more convincing demonstration, because it was not staged.
|
||||
|
||||
| | before | after |
|
||||
|---|---|---|
|
||||
| controller | — | **1355** total (`+7` this session: 4 verdict, 3 disk-meter) |
|
||||
| agent | — | **947** total (`+5` this session) |
|
||||
## 4. The two known disagreements — both settled, and neither document was wrong
|
||||
|
||||
`go build ./... && go vet ./... && go test ./...` **green in both repos** (controller 28 packages,
|
||||
agent 29), run separately from every commit. `controller_gates.py --fast` all OK; `agent_gates.py
|
||||
--fast` all OK; `repo_gates.py --fast` **all 8 OK**.
|
||||
**"A customer restores their own data with no help."** The map says **MISSING (as evidence)**; the
|
||||
2026-08-07 walk records a customer route completed with no shell. **Not a contradiction.** The map's
|
||||
row is *"A customer (**not the operator**) performs a restore via UI alone"* — it is about *who*. The
|
||||
walk proves the *route*. No non-operator has ever done it, which is what the page's own neighbouring
|
||||
claim already says.
|
||||
|
||||
The dashboard test **extracts** the meter block from the shipped template rather than copying it — a
|
||||
copied block drifts and then passes while the page it covers has changed.
|
||||
**The reinstall story.** The map's `PROVEN-LIVE (2026-08-04 night drill)` row is scoped in its own text
|
||||
to *"a controller-data-volume rebuild — NOT a total host loss"*. The 2026-08-09 rehearsal was a
|
||||
whole-host uninstall and reinstall. **The map has no row for that case at all** — a gap, not a
|
||||
disagreement.
|
||||
|
||||
## 8. Deploy
|
||||
**Would anything here have caught either one? No — and it could not have, because neither was false.**
|
||||
Both are collisions of vocabulary: "customer" meaning *the route* or *a person*, "rebuild" meaning
|
||||
*the guest* or *the host*. No gate detects an ambiguity that makes two true sentences look
|
||||
contradictory. They surfaced only when someone tried to state them side by side. **That is the
|
||||
argument for the dataset** — one id, one scope, one status — and against prose rows.
|
||||
|
||||
| | version | evidence |
|
||||
|---|---|---|
|
||||
| agent | **0.128.0** | `felhom-agent --version` on `felhom-pve`; service `active`; prior binary kept as `felhom-agent.bak-0.127.0` |
|
||||
| controller | **0.210.0** | `docker ps` on guest 9201: `felhom-controller:0.210.0 Up (healthy)` |
|
||||
## 5. The data file
|
||||
|
||||
**Endpoint-level validation of the controller UI was ATTEMPTED AND DID NOT SUCCEED — stated rather
|
||||
than skipped.** What was tried: login POST against the container IP `172.17.0.2:8080` with the
|
||||
mandatory `Host:` header, first with curl's cookie jar and then with the `Set-Cookie` handled
|
||||
explicitly (the known `felhom_session` jar trap). Login returned `302` and `/` returned `302` back to
|
||||
login both times, so the dashboard was never rendered. **Fallback observable, on the deployed
|
||||
artifact rather than the source** — `grep -a` inside the running container's binary:
|
||||
`SystemInfo.DiskKnown` ×1, the disk caveat line ×6, the no-verdict title ×1, `--version` →
|
||||
`0.210.0 (commit c732fe1)`. That proves the shipped bytes carry both fixes; it does **not** prove the
|
||||
rendered page, and the template-level tests are what stand for that.
|
||||
`documentation/architecture/where-felhom-stands.yaml`, 55 entries. **Every entry cites at least one
|
||||
source and the gate proves it** (`check_stands.py` rule 1). YAML rather than JSON because statuses move
|
||||
one line at a time and a YAML diff shows which claim moved; a JSON re-dump reflows.
|
||||
|
||||
## 9. The bake, and the three Day-0 values
|
||||
Rules honoured: it is a **view** (every entry cites map / register / evidence); **no status was raised
|
||||
in it** — the one upgrade is recorded against evidence and the map is named as the thing that must
|
||||
change; and it is **regenerated, not hand-edited** for the page.
|
||||
|
||||
`documentation/tests/golden-0.210.0-2026-08-08/` — golden **0.210.0**, **656 787 777 B**, sha256
|
||||
`b9f701fa…4c0a00`, round-trip verified, `./etc/felhom-controller-image` read **out of the downloaded
|
||||
archive** → `felhom-controller:0.210.0`. Markers all green, token grep 0 with a control returning 1,
|
||||
bake VM destroyed, drill disk restored to `virgin`.
|
||||
## 6. The page
|
||||
|
||||
| field | set to | verified |
|
||||
|---|---|---|
|
||||
| `golden_version` | **0.210.0** | package `GET` **200**; hub dropdown offers it, `data-sha` matches the bake |
|
||||
| `agent_version` | **0.128.0** | package `GET` **200**; hub dropdown offers it, `data-sha` matches the deployed binary |
|
||||
| `min_agent` | **0.127.0** (unchanged) | read from the CHANGELOG header written this session |
|
||||
- `where-felhom-stands.html` — **generated, 54 KB, zero `<script>` tags**, same palette
|
||||
(`#0b1220` / `#121b2c` / `#34d399` `#60a5fa` `#fbbf24` `#64748b`), 1600 px, A3 landscape print rules.
|
||||
- Every moved status carries a visible **`changed 2026-08-09, was walked`** chip plus a *why it moved*
|
||||
line — 12 of them, no diffing required.
|
||||
- `Where Felhom Stands.html` → **`where-felhom-stands-2026-08-09-snapshot.html`** (`git mv`, so the
|
||||
space is out of every shell path), with a line in `documentation/README.md` calling it a dated
|
||||
snapshot that is not maintained.
|
||||
- **What the old bundle actually was**, since it aimed the fix: not merely minified — the content sat
|
||||
**gzip+base64 inside a JS module map**, and the three blobs decompress to the bundler and React, with
|
||||
the document itself in a JSON-escaped string on line 393. It rendered and nothing else.
|
||||
|
||||
**⚠ The agent was NOT published until this session checked, and it mattered.** R-221's fix is in the
|
||||
**agent**; the binary had been hand-deployed and never published, so `agent_version 0.128.0` was not
|
||||
selectable and a fresh install would have received 0.127.0 — the golden would have carried the
|
||||
controller fixes and **not** the one the headline defect needed. Caught by checking each value was
|
||||
*fetchable* instead of assuming. Published from the **live-deployed bytes**, sha-verified across the
|
||||
hop first (`c6eba73b…` identical on both sides).
|
||||
## 7. The corrected silence rows
|
||||
|
||||
**`min_agent` stays 0.127.0 deliberately:** `MinAgent` declares what the *controller* requires, and
|
||||
v0.210.0 requires nothing new from the agent. R-221 is delivered by `agent_version`, not by the floor.
|
||||
**R-281 is WITHDRAWN. It was wrong in both directions**, and the operator's mailbox is what settled it.
|
||||
|
||||
**Nothing was vouched. No hub setting was touched.** The Save is the operator's.
|
||||
- **The tripwire DID fire**: `escrow_blob_served` at 10:19:41 UTC = **12:19 CEST**, eight minutes before
|
||||
the verified restore.
|
||||
- **False alarms fired too**: `host_down` 09:28 UTC and `node_down` 09:30 UTC, both error severity, both
|
||||
`sent`, for deliberate attended work — eight operator mails in all. → **R-285**, the opposite gap
|
||||
from the one filed.
|
||||
|
||||
## 10. Gate failures remaining
|
||||
**The measurement's cause IS established.** The P7 query copied `/data/hub.db` **without
|
||||
`hub.db-wal`**; the hub runs SQLite in WAL mode, so everything after the last checkpoint was invisible.
|
||||
**Signature, exact:** P7 reported *"2 events all day, newest `db_dump_completed` 00:30:07"*, and the
|
||||
number of rows on 08-09 at or before 00:30:07 is **exactly 2**.
|
||||
|
||||
**None.** `golden_currency_gate.py` went red the moment the controller was bumped — correct, and
|
||||
closed by the bake, not by `--no-verify`. **No `--no-verify` anywhere in this session.**
|
||||
The two obvious alternatives were **tested and refuted**, not waved away: a **timezone offset** — all
|
||||
nine mailbox stamps equal the hub's UTC + 2 h exactly, so the window was right; and a **wrong key or
|
||||
wrong store** — the same table and key return the correct rows now. A live re-run **cannot** reproduce
|
||||
the fault because the WAL has since been checkpointed, and that is stated rather than dressed up as a
|
||||
reproduction.
|
||||
|
||||
## 11. Register
|
||||
**The lesson, filed as R-286:** the control was drawn from the *same stale snapshot* as the
|
||||
measurement, so it agreed. **A control must come from a different channel.** The independent channel —
|
||||
the mailbox — was available the whole time. This is also a trap `operations/nodes.md` already
|
||||
documents, and which I had avoided correctly earlier in the same session.
|
||||
|
||||
**Closed:** R-221, R-259, R-258, R-265 (the last with its unknown explicitly still open).
|
||||
**Minted:** **R-266** — the failed root `statfs` still travels to the hub as a 0-of-0 disk; ranked
|
||||
low because it is the quiet direction, and now a two-repo wire change governed by G-1's gate.
|
||||
**Highest ID moved R-265 → R-266.** **G-3 unblocked** in `ROADMAP.md`; **CONTEXT S-39** rules the
|
||||
convention.
|
||||
## 8. Register
|
||||
|
||||
**Still open, untouched:** R-246, R-255, R-256, R-257, R-261, R-262, R-263, R-264, R-240, R-243,
|
||||
R-202, R-213, R-244, R-214/R-235, C7's test-comment half, and G-8's other half.
|
||||
**Ceiling moved R-284 → R-290.** Opened: **R-285** (planned reinstall pages the operator), **R-286**
|
||||
(same-channel control), **R-287** (the CI red is true), **R-288** (the capability map is unreadable),
|
||||
**R-289** (R-182's row is stale), **R-290** (map rows cite no evidence). Withdrawn: **R-281**.
|
||||
|
||||
## 12. The capability-map row
|
||||
## 9. Part 4 — and I did not change the workflow, on purpose
|
||||
|
||||
`00-capability-map.md:93` — *"A failed per-app Tier-1 backup reaches the OPERATOR"*. **Checked, and
|
||||
it was NOT claiming something untrue:** it is about the operator notification path and claims nothing
|
||||
about what `/backups/apps` draws. But its narrative — *"the page you open to ask whether ONE app is
|
||||
backed up"* — invites the wrong reading, and the adjacent thing WAS false: the page's tick was green
|
||||
on presence until v0.210.0, so the two halves disagreed and only the operator half was true. The row
|
||||
now records that.
|
||||
**Which gate:** `published` / `scripts/check-published-versions.py`.
|
||||
|
||||
## 13. Observations — noticed, NOT acted on
|
||||
**The premise is wrong in every particular. It is not ref-sensitive.** The gate enumerates releases
|
||||
from the **Gitea tags API** (`main()`, `/api/v1/repos/admin/felhom-agent/tags?limit=200`), so the
|
||||
checked-out ref is irrelevant — and the two previous tag pushes **passed** (run 190 `v0.126.0`, run 216
|
||||
`v0.127.0`).
|
||||
|
||||
1. **Other collectors in `info_linux.go` return silently on error** — `readLoadAvg`, `readMemInfo`
|
||||
and the temperature read. Only the disk one was traced to a customer-visible surface, and the
|
||||
change was deliberately not widened into a refactor of that file.
|
||||
2. **The tick's recency weakness stands.** A tick over a three-week-old restore point is still a
|
||||
tick. Adding an age threshold means inventing a number; the time is printed beside it.
|
||||
3. **The controller UI could not be driven headlessly this session** (§8). Worth one session to
|
||||
re-establish the documented headless login, because "invoke the endpoint the UI invokes" is this
|
||||
project's standard validation method and it is currently unavailable for the controller.
|
||||
4. **`HDDKnown` is wired but has no template consumer yet** — the HDD path renders through
|
||||
`StorageBars`, which has its own `Disconnected` state. Adding the flag there is the natural next
|
||||
step of the S-39 conversion and is part of G-3's survey, not this session.
|
||||
**What is true:** run 267 (main, `28ba8593b8`, 08-08 14:29 UTC) printed `ok v0.120.0: binary
|
||||
downloadable`. Run 284 (**the same commit**, on the tag, 08-09 09:30 UTC) printed
|
||||
`FAIL v0.120.0 — HTTP 404`. A published release became uninstallable between the two. The registry now
|
||||
holds exactly the ten newest versions; `0.128.0` was published **14:47 UTC, eighteen minutes after run
|
||||
267**.
|
||||
|
||||
**Who removed `0.120.0` is NOT established, and I will not guess:** `package_cleanup_rule` is empty
|
||||
(queried in Postgres), `app.ini` sets no limit, `publish-agent.sh:77` only pre-deletes the version it
|
||||
is publishing, the Gitea pod has 53 days uptime and 0 restarts, and **no `DELETE` on the packages API
|
||||
appears in 48 h of router logs**. The internal `[cron.cleanup_packages]` `@midnight` job falls in the
|
||||
window and would leave no router line — **a leading candidate, not a conclusion.**
|
||||
|
||||
**So nothing was silenced and no workflow file was changed.** The red is a **true positive** — a tagged
|
||||
version that cannot be installed is the exact R-115 defect the gate exists to catch, and muting it
|
||||
would hide the next one. **What it therefore still does not check: nothing. Nothing was disabled.**
|
||||
The honest fixes — bound the gate to versions at or above the vouched `min_agent` floor (0.127.0 today;
|
||||
nothing can install 0.120.0), or retire tags whose packages go — are release decisions, and §8 forbids
|
||||
fixing findings here. Filed as **R-287**.
|
||||
|
||||
**Also measured, and it is good news:** the failure alarm did send —
|
||||
`RESEND-ACCEPTED id=fa1a7a83-714f-4357-b0ca-d3c4bb7ae73f`.
|
||||
|
||||
## 10. Observations — noticed, not acted on
|
||||
|
||||
- **`fail.app-crash` and `fault.operator-email` are the same unobserved thing** seen from two sides:
|
||||
the digest is wired and correct in source, and no one has watched it arrive.
|
||||
- **The verification depth is recorded per claim** (`depth: source-read | register+map |
|
||||
needs-hardware`). 23 of 55 got a source or evidence read; the rest were checked against the register
|
||||
and map only. That is on the face of the data rather than implied by a green tick.
|
||||
- **The capability map is the thing that actually needs fixing.** The dataset now disagrees with it for
|
||||
twelve rows, and the dataset is only a view — **the map still says PROVEN-LIVE for all twelve** (R-290).
|
||||
- `documentation/audits/` holds 131 files and `tests/` 37; the evidence exists in quantity. The gap is
|
||||
that the map does not point at it.
|
||||
- **Out of scope and left alone, as instructed:** every finding above, the capability-map restructure
|
||||
(R-288), and the two guards owed from yesterday.
|
||||
|
||||
@@ -17,6 +17,12 @@ The Docker-only app-domain controller. Full per-area docs grounded in current so
|
||||
→ [`controller/README.md`](controller/README.md): module map, deploy & stack lifecycle, backup
|
||||
architecture, storage/monitoring/metrics, auth/hub/sync/integrations.
|
||||
|
||||
### Where we stand — `architecture/where-felhom-stands.*`
|
||||
The operator's one-page picture of what is proven, built, partial and missing.
|
||||
- [`architecture/where-felhom-stands.html`](architecture/where-felhom-stands.html) — **generated**; do not hand-edit
|
||||
- [`architecture/where-felhom-stands.yaml`](architecture/where-felhom-stands.yaml) — the data behind it; every claim cites its source. Gate: `scripts/check_stands.py`; regenerate with `scripts/render_stands.py`
|
||||
- [`architecture/where-felhom-stands-2026-08-09-snapshot.html`](architecture/where-felhom-stands-2026-08-09-snapshot.html) — **a dated snapshot, NOT maintained.** The original React bundle, kept for the record; its statuses are those of 2026-08-09 before the verification pass
|
||||
|
||||
### Host agent & platform — `architecture/`, `proxmox-platform.md`
|
||||
The operator-tier agent and the Proxmox platform.
|
||||
- [`architecture/01-topology-and-trust.md`](architecture/01-topology-and-trust.md) — topology & trust model
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,727 @@
|
||||
# where-felhom-stands.yaml — the data behind documentation/architecture/where-felhom-stands.html
|
||||
#
|
||||
# THIS IS A VIEW, NEVER A SOURCE. Every entry cites the capability-map row, register row or
|
||||
# evidence document it derives from; an entry with no source is a defect, not a claim.
|
||||
# A status may not be RAISED here — if the evidence supports a stronger status than the
|
||||
# capability map records, the MAP changes first and this file follows it.
|
||||
# Regenerate the page after any status move: python3 scripts/render_stands.py
|
||||
#
|
||||
# YAML rather than JSON, deliberately: statuses move one line at a time and a YAML diff shows
|
||||
# which claim moved. A JSON re-dump reflows and shows the whole file.
|
||||
#
|
||||
# verdict vocabulary: confirmed | downgraded | upgraded | contested | needs-hardware
|
||||
# depth: source-read (opened live source or evidence) | register+map (checked against the
|
||||
# register and capability map only) | needs-hardware (cannot be settled off-box)
|
||||
verified_on: 2026-08-09
|
||||
verified_against:
|
||||
felhom-agent: 28ba8593b8
|
||||
felhom-controller: c732fe1283
|
||||
hub: 56f8aa611c
|
||||
claims:
|
||||
- id: install.iso-selfregister
|
||||
band: journey
|
||||
stage: 1
|
||||
title: "A blank machine installs itself from our own boot image and registers itself as unclaimed — proven on two different boards"
|
||||
status: walked
|
||||
note: "Two boards: N100 2026-07-18, HP t740 2026-07-21."
|
||||
sources:
|
||||
- capability-map: "Bare-metal Felhom ISO (blank hardware → zero-touch auto-install → first-boot host-install)"
|
||||
- evidence: "tests/VALIDATION-n100-rehearsal-2026-07-18.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: install.installer-by-tag
|
||||
band: journey
|
||||
stage: 1
|
||||
title: "The installer is published rather than pushed: rolling it back is one act"
|
||||
status: built
|
||||
note: "Gate 6 of hostinstall_gates asserts the manifest names an installer-v tag; ran green tonight."
|
||||
sources:
|
||||
- capability-map: "The installer is PUBLISHED, not pushed"
|
||||
- register: "R-110"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "gate 6 asserts the manifest names an installer tag, but no walk of a rollback is on file"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: source-read
|
||||
- id: install.byo
|
||||
band: journey
|
||||
stage: 1
|
||||
title: "Installing onto hardware the customer already owns — the path exists, the first real one has not happened"
|
||||
status: partial
|
||||
note: "UPGRADED: a real --mode byo install completed on demo-hp 2026-08-09 (Day-0 provision SUCCESS, 3m49s). Still not a customer's own hardware, so not 'walked' — but 'has not happened' is now false."
|
||||
sources:
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: upgraded
|
||||
depth: source-read
|
||||
- id: install.nic-selfheal
|
||||
band: journey
|
||||
stage: 1
|
||||
title: "A machine that ends up on the wrong network port explains itself on screen and finds its way back — proven in a virtual drill, never on metal"
|
||||
status: partial
|
||||
note: "Map says the same: PROVEN-LIVE (nested drill — nested != metal)."
|
||||
sources:
|
||||
- capability-map: "Box survives a wrong-NIC install"
|
||||
- evidence: "audits/SPIKE-firstboot-nic-sweep-2026-07-22.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: install.reinstall-refuses
|
||||
band: journey
|
||||
stage: 1
|
||||
title: "Reinstalling a machine we previously installed refuses, because our own removal leaves a name service holding the port our own installer checks"
|
||||
status: partial
|
||||
note: "Warning card. Confirmed live 2026-08-09; dnsmasq restarted by --uninstall seizes :53."
|
||||
sources:
|
||||
- register: "R-272"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: claim.one-time-code
|
||||
band: journey
|
||||
stage: 2
|
||||
title: "An emailed one-time code; the customer sets their own password and the operator never sees it"
|
||||
status: walked
|
||||
note: "Exercised live 2026-08-09: reset code accepted at /claim, 302 + session."
|
||||
sources:
|
||||
- capability-map: "Customer claim: one-time emailed code → customer sets own password"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: claim.selfbind
|
||||
band: journey
|
||||
stage: 2
|
||||
title: "The customer can bind their own machine from a link, with no operator present — done once, for real"
|
||||
status: walked
|
||||
note: "attempts=0, locked=0, source customer_selfbind, 2026-07-18."
|
||||
sources:
|
||||
- capability-map: "Customer binds their own appliance (self-service)"
|
||||
- evidence: "tests/VALIDATION-n100-rehearsal-2026-07-18.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: claim.never-by-non-operator
|
||||
band: journey
|
||||
stage: 2
|
||||
title: "It has never been done by a person who is not the operator"
|
||||
status: partial
|
||||
note: "Map records MISSING (as evidence). Still true after 2026-08-09."
|
||||
sources:
|
||||
- capability-map: "A customer (not the operator) performs a restore via UI alone"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: claim.code-naming
|
||||
band: journey
|
||||
stage: 2
|
||||
title: "The same word is used for two different secrets across three surfaces; the email points at a page a rebuilt machine does not show"
|
||||
status: partial
|
||||
note: "Warning card. Cost a real code on 2026-08-09."
|
||||
sources:
|
||||
- register: "R-282"
|
||||
- register: "R-283"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: use.catalog
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "Apps installed from a catalog of about 52, with a memory guard and health-aware progress"
|
||||
status: walked
|
||||
note: "53 templates listed on the rebuilt box 2026-08-09; deploy driven live."
|
||||
sources:
|
||||
- capability-map: "Deploy an app from the catalog (env config, memory guard, health-aware progress)"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: use.lifecycle
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "Start, stop, restart, update, logs, remove — and the parts that must not be stopped cannot be"
|
||||
status: built
|
||||
sources:
|
||||
- capability-map: "App lifecycle: start/stop/restart/update/logs/remove/redeploy"
|
||||
- register: "R-108"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited by the map row or anywhere else"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: register+map
|
||||
- id: use.tunnel
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "Reachable from anywhere through a tunnel, per-app addresses"
|
||||
status: walked
|
||||
note: "Verified tonight: the rebuilt box answered on its public URL from outside."
|
||||
sources:
|
||||
- capability-map: "Remote access via Cloudflare Tunnel + Traefik (per-app subdomains)"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: use.lan-fallback
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "Reachable on the home network when the internet is down"
|
||||
status: built
|
||||
note: "Only an observation on a running box with WAN pulled could settle it. Boxes are off."
|
||||
sources:
|
||||
- capability-map: "LAN access when internet is down (lan_resolver)"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: needs-hardware
|
||||
depth: needs-hardware
|
||||
- id: use.files
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "Phone photos, documents with text recognition, files from Windows Explorer or a Mac"
|
||||
status: walked
|
||||
sources:
|
||||
- capability-map: "Files from Windows Explorer / Mac Finder (SMB server)"
|
||||
- evidence: "audits/SPIKE-lan-discovery-2026-07-18.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: use.launcher
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "A one-tap launcher, and a read-only guest link for visitors"
|
||||
status: built
|
||||
sources:
|
||||
- capability-map: "Indítópult (app launcher) — one-tap grid of the household's openable apps"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: use.dlna
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "Media to a TV"
|
||||
status: missing
|
||||
note: "Map: MISSING."
|
||||
sources:
|
||||
- capability-map: "Media to TV via DLNA"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: use.multiuser
|
||||
band: journey
|
||||
stage: 3
|
||||
title: "Separate accounts per household member"
|
||||
status: missing
|
||||
note: "Map: MISSING."
|
||||
sources:
|
||||
- capability-map: "Multiple household users / per-person accounts"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: drives.enrol
|
||||
band: journey
|
||||
stage: 4
|
||||
title: "A new drive is found, offered, formatted, mounted and enrolled — including on awkward older boot layouts"
|
||||
status: built
|
||||
note: "Applies to a NEW drive. Re-attaching an existing one after a reinstall is R-280 and fails."
|
||||
sources:
|
||||
- capability-map: "Drive wizard: scan/format/mount/enroll, incl. legacy-boot LVM-root hosts"
|
||||
- register: "R-220"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "the 2026-08-09 walk exercised RE-attach (which failed, R-280); first-enrolment of a NEW drive has no walk on file"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: source-read
|
||||
- id: drives.migrate
|
||||
band: journey
|
||||
stage: 4
|
||||
title: "Moving data between drives, crash-safe; removing a drive safely; unplug detected"
|
||||
status: built
|
||||
sources:
|
||||
- capability-map: "Data migration between drives (all / per-app), crash-safe"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: register+map
|
||||
- id: drives.nas
|
||||
band: journey
|
||||
stage: 4
|
||||
title: "A network drive can be browsed and hold bulk media, but may not hold an app's data — enforced"
|
||||
status: walked
|
||||
note: "RefuseAsAppNamespace is the fail-closed predicate."
|
||||
sources:
|
||||
- capability-map: "Network storage (NAS) is browse + bulk-media only"
|
||||
- register: "R-108"
|
||||
- evidence: "audits/R108-network-app-namespace-2026-07-30.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: drives.reattach-wall
|
||||
band: journey
|
||||
stage: 4
|
||||
title: "After a reinstall the data drive cannot be re-attached through any dashboard route"
|
||||
status: partial
|
||||
note: "Warning card. /api/disks/candidates returns empty; the restore page promises two clicks."
|
||||
sources:
|
||||
- register: "R-280"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: backup.tier1
|
||||
band: journey
|
||||
stage: 5
|
||||
title: "App data on the machine, nightly database dumps, a copy on a second drive"
|
||||
status: built
|
||||
sources:
|
||||
- capability-map: "Tier-2 secondary-drive copy: class-driven legs"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: register+map
|
||||
- id: backup.whole-machine
|
||||
band: journey
|
||||
stage: 5
|
||||
title: "A whole-machine archive that lands off the guest's own disk — a single-drive machine is recorded as degraded rather than pretending"
|
||||
status: built
|
||||
sources:
|
||||
- capability-map: "Whole-guest backup lands OFF the guest's own physical device"
|
||||
- register: "R-165"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: register+map
|
||||
- id: backup.offsite
|
||||
band: journey
|
||||
stage: 5
|
||||
title: "An encrypted off-site copy, sealed with a key the operator cannot read"
|
||||
status: walked
|
||||
note: "Verified tonight from the repo itself: 18 snapshots, daily, unbroken."
|
||||
sources:
|
||||
- capability-map: "Offsite (restic → Hetzner Storage Box)"
|
||||
- register: "R-199"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: backup.restore-proof
|
||||
band: journey
|
||||
stage: 5
|
||||
title: "The backups prove themselves: a restore is actually performed, unattended, on every tier, on both machines"
|
||||
status: built
|
||||
note: "'on both machines, unattended, every tier' is a continuing claim about scheduled runs. Both boxes are off; the last recorded restore-test on demo-hp FAILED (notification_log 2026-08-05 restore_test_failed). Cannot be settled tonight."
|
||||
sources:
|
||||
- capability-map: "Restore-proof is UNATTENDED — the scheduler covers EVERY tier"
|
||||
- register: "R-86"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited, and the last recorded restore-test on demo-hp FAILED (2026-08-05)"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: needs-hardware
|
||||
- id: backup.fill-warning
|
||||
band: journey
|
||||
stage: 5
|
||||
title: "The customer is warned before a drive fills, per drive, in their own language"
|
||||
status: walked
|
||||
note: "R-177 (no operator-triggerable run) limits testing, not the capability."
|
||||
sources:
|
||||
- capability-map: "The customer is warned BEFORE a filesystem fills"
|
||||
- register: "R-167"
|
||||
- evidence: "audits/SPIKE-r165-mp1-merge-2026-08-02.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: backup.sikeres
|
||||
band: journey
|
||||
stage: 5
|
||||
title: "A backup that covered nothing still calls itself successful"
|
||||
status: partial
|
||||
note: "Warning card."
|
||||
sources:
|
||||
- register: "R-240"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: fault.selfheal
|
||||
band: journey
|
||||
stage: 6
|
||||
title: "The machine watches itself and repairs some faults without telling anyone it had to"
|
||||
status: built
|
||||
note: "R-264 records that the self-heal counters reach the hub and are decoded nowhere."
|
||||
sources:
|
||||
- capability-map: "Box survives an unattended app or guest-network failure"
|
||||
- register: "R-264"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: source-read
|
||||
- id: fault.operator-email
|
||||
band: journey
|
||||
stage: 6
|
||||
title: "Failures reach the operator by email, one mail per run, every failing app named"
|
||||
status: built
|
||||
note: "CONFIRMED against source: backup_run_failures digest is allowlisted (api/handler.go:1837), operator-only (dispatcher.go:423), templated (templates.go:48); recovery_unit_capture_failed is record-only (dispatcher.go:376). NOTE: R-182's register row still describes the PRE-FIX behaviour — see R-289."
|
||||
sources:
|
||||
- capability-map: "A failed per-app Tier-1 backup reaches the OPERATOR — EVERY failing app, in ONE mail per run"
|
||||
- register: "R-182"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "the digest is wired and source-verified, but no run of it has been observed delivering"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: source-read
|
||||
- id: fault.customer-email
|
||||
band: journey
|
||||
stage: 6
|
||||
title: "Failures reach the customer"
|
||||
status: built
|
||||
note: "The customer leg is built; today's log shows customer-channel rows skipped as operator_only."
|
||||
sources:
|
||||
- capability-map: "App crashes → customer notified (one event per transition, no flapping spam)"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fault.already-paired
|
||||
band: journey
|
||||
stage: 6
|
||||
title: "An already-paired box is still told to pair itself"
|
||||
status: partial
|
||||
note: "Warning card."
|
||||
sources:
|
||||
- register: "R-214"
|
||||
- register: "R-235"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: recover.screen
|
||||
band: journey
|
||||
stage: 7
|
||||
title: "A rebuilt machine shows a full-page recovery screen without anyone looking for it, and says plainly that nobody can replace a lost recovery code"
|
||||
status: walked
|
||||
note: "Seen unsought on the rebuilt demo-hp 2026-08-09, seal date matching host_escrow.created_at."
|
||||
sources:
|
||||
- register: "R-193"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: recover.byte-identical
|
||||
band: journey
|
||||
stage: 7
|
||||
title: "The customer's code opens the sealed package and the data returns byte for byte — including accented Hungarian filenames, verified as raw bytes"
|
||||
status: walked
|
||||
note: "Reproduced 2026-08-09: 4/4 byte-identical, name bytes NFC-preserved, out of snapshot 41c830db."
|
||||
sources:
|
||||
- register: "R-201"
|
||||
- evidence: "tests/walk5-r201-2026-08-07/journal.md"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: recover.no-shell
|
||||
band: journey
|
||||
stage: 7
|
||||
title: "Walked end to end with no command line inside the machine (2026-08-07)"
|
||||
status: walked
|
||||
note: "CONTESTED-RESOLVED: not a contradiction. The walk proves the ROUTE needs no guest shell; the map's MISSING row is about a NON-OPERATOR doing it, which has still never happened. Two questions, one word 'customer'."
|
||||
sources:
|
||||
- register: "R-201"
|
||||
- evidence: "tests/walk5-r201-2026-08-07/journal.md"
|
||||
- capability-map: "A customer (not the operator) performs a restore via UI alone"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: contested
|
||||
depth: source-read
|
||||
- id: recover.putback
|
||||
band: journey
|
||||
stage: 7
|
||||
title: "Putting restored files back where they belong is still manual"
|
||||
status: partial
|
||||
note: "Warning card. Confirmed 2026-08-09: the restore lands in a verification folder and says so."
|
||||
sources:
|
||||
- register: "R-213"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: recover.tripwire
|
||||
band: journey
|
||||
stage: 7
|
||||
title: "The tripwire that says someone is opening this customer's backups does fire"
|
||||
status: walked
|
||||
note: "CONFIRMED tonight from the hub store: escrow_blob_served 2026-08-09 10:19:41Z = 12:19 CEST in the operator's mailbox. R-281's original claim of silence is WITHDRAWN."
|
||||
sources:
|
||||
- register: "R-281"
|
||||
- register: "R-285"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.disk-failing
|
||||
band: failures
|
||||
title: "A disk starts failing — healthy path only; a genuinely failing disk has never been seen"
|
||||
status: partial
|
||||
note: "Only a failing disk on a running box could settle it."
|
||||
sources:
|
||||
- capability-map: "Lemez-egészség felügyelet: per-disk SMART kártya"
|
||||
- evidence: "audits/SPIKE-smart-coverage-2026-07-25.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: needs-hardware
|
||||
depth: needs-hardware
|
||||
- id: fail.backup-drive-unplugged
|
||||
band: failures
|
||||
title: "The backup drive is unplugged"
|
||||
status: walked
|
||||
sources:
|
||||
- capability-map: "An ABSENT backup-target drive raises its OWN alarm"
|
||||
- evidence: "audits/R116-v0116-2026-07-30.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: fail.drive-filling
|
||||
band: failures
|
||||
title: "A drive is filling up"
|
||||
status: built
|
||||
sources:
|
||||
- register: "R-167"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: register+map
|
||||
- id: fail.app-crash
|
||||
band: failures
|
||||
title: "An app crashes — the email leg has never been confirmed end to end"
|
||||
status: built
|
||||
note: "Consistent with fault.operator-email: the digest is wired but its delivery is unobserved."
|
||||
sources:
|
||||
- capability-map: "App crashes → customer notified (one event per transition, no flapping spam)"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.power-cut
|
||||
band: failures
|
||||
title: "Power cut mid-backup"
|
||||
status: walked
|
||||
sources:
|
||||
- evidence: "audits/AUDIT-power-outage-recovery-2026-07-22.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: fail.guest-destroyed
|
||||
band: failures
|
||||
title: "The guest is destroyed"
|
||||
status: walked
|
||||
sources:
|
||||
- register: "R-201"
|
||||
- evidence: "audits/DRILL-r201-night-run-2026-08-04.md"
|
||||
- evidence: "audits/DRILL-r201-night-run-2026-08-04.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: fail.wiped-reinstalled.data
|
||||
band: failures
|
||||
title: "The whole machine is wiped and reinstalled — the data comes back"
|
||||
status: walked
|
||||
note: "4/4 byte-identical 2026-08-09."
|
||||
sources:
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.wiped-reinstalled.journey
|
||||
band: failures
|
||||
title: "The whole machine is wiped and reinstalled — the journey needs a terminal twice"
|
||||
status: partial
|
||||
note: "CONTESTED-RESOLVED against the map: the map's PROVEN-LIVE row is scoped to a controller-data-volume REBUILD (2026-08-04), not a whole-host reinstall. The map has no row for the host case, so there was no contradiction — only a gap."
|
||||
sources:
|
||||
- register: "R-273"
|
||||
- register: "R-280"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.stolen-machine
|
||||
band: failures
|
||||
title: "The machine is stolen, and someone opens the backups — the operator is told"
|
||||
status: walked
|
||||
note: "Confirmed 2026-08-09 from the store and the mailbox."
|
||||
sources:
|
||||
- register: "R-281"
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.forgot-password
|
||||
band: failures
|
||||
title: "The customer forgets their dashboard password"
|
||||
status: walked
|
||||
note: "Exercised 2026-08-09 via the reset code."
|
||||
sources:
|
||||
- evidence: "audits/REHEARSAL-byo-reinstall-2026-08-09.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.lost-recovery-code
|
||||
band: failures
|
||||
title: "The customer loses their recovery code — by design, the data is unrecoverable"
|
||||
status: built
|
||||
note: "The recovery screen states it in Hungarian."
|
||||
sources:
|
||||
- capability-map: "Escrow ceremony: customer-facing wizard, one-shot R claim, operator zero-knowledge"
|
||||
- register: "R-198"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "a by-design refusal; no walk document cited"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: source-read
|
||||
- id: fail.moves-house
|
||||
band: failures
|
||||
title: "The machine moves house / new network"
|
||||
status: walked
|
||||
sources:
|
||||
- capability-map: "Box survives a site/network change (relocation, different subnet, DHCP re-lease)"
|
||||
- evidence: "audits/AUDIT-vacation-remote-ops-2026-07-20.md"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: fail.internet-down
|
||||
band: failures
|
||||
title: "The internet goes down — built, never walked"
|
||||
status: built
|
||||
note: "Needs a running box with WAN pulled."
|
||||
sources:
|
||||
- capability-map: "LAN access when internet is down (lan_resolver)"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: needs-hardware
|
||||
depth: needs-hardware
|
||||
- id: fail.hub-down
|
||||
band: failures
|
||||
title: "The hub is down"
|
||||
status: built
|
||||
sources:
|
||||
- capability-map: "Config/state change round-trips in seconds (hub↔box immediacy"
|
||||
changed:
|
||||
from: walked
|
||||
reason: "no walk document cited"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: downgraded
|
||||
depth: register+map
|
||||
- id: fail.broken-release
|
||||
band: failures
|
||||
title: "We ship a broken release — the guard is missing"
|
||||
status: missing
|
||||
note: "Proven the hard way on 2026-08-09: a vouched agent version had no git tag and every install died at 5/8. Both guards still owed."
|
||||
sources:
|
||||
- register: "R-273"
|
||||
- register: "R-287"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.stale-image
|
||||
band: failures
|
||||
title: "A fresh install picks up an old image"
|
||||
status: partial
|
||||
note: "Narrowed 2026-08-09: the resume path fetched the vouched golden; a fresh install still takes the newest LOCAL archive with no manifest comparison."
|
||||
sources:
|
||||
- register: "R-274"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.offsite-account-deleted
|
||||
band: failures
|
||||
title: "The off-site provider account is deleted"
|
||||
status: partial
|
||||
note: "The credential that holds the customer's documents can still delete."
|
||||
sources:
|
||||
- register: "R-95"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: register+map
|
||||
- id: fail.expected-downtime
|
||||
band: failures
|
||||
title: "The machine is switched off for an afternoon — no notion of expected downtime"
|
||||
status: missing
|
||||
note: "Confirmed 2026-08-09: eight operator mails for deliberate, attended work."
|
||||
sources:
|
||||
- register: "R-285"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: confirmed
|
||||
depth: source-read
|
||||
- id: fail.customer-self-restore
|
||||
band: failures
|
||||
title: "A customer restores their own data with no help"
|
||||
status: partial
|
||||
note: "CONTESTED-RESOLVED: the map's MISSING is about a NON-OPERATOR performing it; the walks prove the route, not the person. Neither record was wrong."
|
||||
sources:
|
||||
- capability-map: "A customer (not the operator) performs a restore via UI alone"
|
||||
- register: "R-201"
|
||||
verified:
|
||||
date: 2026-08-09
|
||||
verdict: contested
|
||||
depth: source-read
|
||||
@@ -509,10 +509,16 @@ applied.** The one that matters: Scenario A **fails against today's tree** with
|
||||
| **R-278** | **demo-felhom's off-site tier has never completed a run and has been stuck for six days.** `offsite.state=needs_credential` since the 2026-08-03 guest rebuild; the hub's own alarm reads *"enabled + escrowed but no run has EVER succeeded"*; the controller's `offsite-credential-retry` job runs every 5 minutes and completes in 0 s, doing nothing. R-193's fix (the recovery SCREEN, controller 0.200.0) is present on the box, so the remedy exists — it just needs the customer-present ceremony that nobody has run, which is R-243's shape (*"a machine waiting for its recovery code can stop backing up off-site without alarming us"*) landing on a real box. **Contrast that makes it a defect and not a chore:** demo-hp, same rebuild, same day, recovered and has 18 snapshots | **READY (S) — NEW 2026-08-09** | — | Either the self-heal reconciler owns this shape end-to-end, or the box must say plainly on the dashboard that it is unprotected pending the recovery code | CC |
|
||||
| **R-279** | **There is no operator-triggerable off-site backup.** The only route to `POST /backup/offbox/run` is the customer's own dashboard session; `signed_jobs` carries opaque operator-SIGNED blobs and the hub holds no signing key. This cost the rehearsal a stop: preparing the run needed one off-site push and there was no operator path to it. Sibling of **R-177** (no operator-triggerable fill check) | **READY (XS) — NEW 2026-08-09** | — | Same shape as R-177; solve both together | CC |
|
||||
| **R-280** | **RANK 1 — after a reinstall the data drive cannot be re-attached through ANY dashboard route, and the restore page promises it is "two clicks".** Measured on the rebuilt demo-hp, 2026-08-09. The restore page diagnoses the situation perfectly and then sends the customer to an empty page: *„Előbb csatold vissza az adatmeghajtót. A mentéseid megvannak, és a meghajtók is megvannak — újratelepítés után viszont a gép még nem ismeri őket, ezért most nincs hová visszaállítani. **Ez két kattintás:** Tárhely → Meghajtók, »Meglévő meghajtó csatolása«."* **It is not two clicks; it is zero possible clicks.** `GET /api/disks/candidates` → `{"initialize":[],"attach":[]}`, so both wizards render an empty selector, and `Tárhely → Meghajtók` reads „Nincs regisztrált adattároló" with an empty unregistered list. **The agent is not at fault** — `GET /api/disks` returns the NVMe in full (1.0 TB, SMART PASSED, `mount_path:/mnt/nvme-1tb`, `guest_attached:false`), so the channel and enumeration work. **ROOT CAUSE:** `handleDiskCandidates` builds both lists from `ListCandidateDisks`, the UNCLAIMED-disk scan; demo-hp's NVMe is deliberately BOTH the user-data drive and the `felhom-backup` target (`operations/nodes.md`), so it is claimed and never offered. That filter is **correct for `initialize`** (never offer to format a disk in use — `/storage/init` even says so: *„Rendszer- és biztonsági-mentés meghajtók itt nem jelennek meg — azok védettek"*) and **over-broad for `attach`**, which is non-destructive by definition and whose own page says *„A meghajtón lévő adatok nem törlődnek — a csatolás csak elérhetővé teszi azokat."* **It cascades:** no store → Calibre-Web's install page degrades to *„Nincs regisztrált adattároló — adja meg kézzel az útvonalat"* and demands a hand-typed `E-könyvtár útvonal`; no app → the restore rows read „Nincs telepítve". **THE ESCAPE HATCH WORKS AND NO CUSTOMER COULD FIND IT:** `POST /settings/storage/add` with `storage_path=/mnt/sys_drive` succeeded first try (*„Adattároló sikeresen hozzáadva"*) — and `/mnt/sys_drive` is an internal path, the very one registered before the wipe. Once registered, everything unblocked and the deploy form became a proper picker (*„Tárhely (sys_drive) — 64.2 GB szabad"*). **This is R-220's successor:** R-220 was closed as "drives unenrollable after a rebuild — fixed"; enumeration is fixed, OFFERING is not | **READY (M) — NEW 2026-08-09** | — | Populate `attach` from mounted-but-unregistered filesystems rather than from the unclaimed-DISK scan; and never print "two clicks" without asserting the destination is non-empty | CC |
|
||||
| **R-281** | **The hub said NOTHING through an entire reinstall — and the tripwire for a sealed-backup unseal did not fire on a real unseal.** Between the uninstall (08:38 UTC) and the verified restore (10:27 UTC) demo-hp's guest was destroyed, the agent and its pveum identity removed, the host re-enrolled, a new guest provisioned, the box re-claimed, the sealed off-site package opened with the customer's recovery code, an app redeployed and 3.8 MB restored. **Events recorded for demo-hp in that window: ZERO. Notifications: ZERO.** **Positive control on the query** (standing rule 3): the hub recorded **2 events all day across all customers**, newest `db_dump_completed` at 00:30:07 — so the store is reachable and the silence is real, not a bad filter. **The good half, stated first:** no FALSE alarm fired during a legitimate reinstall, which is what P7 was watching for. **The owed half:** `escrow_blob_served` exists precisely as the tripwire for this moment — its text is *"the blob cannot be opened without the customer's recovery code… If no recovery is in progress on that box, investigate"* — and it **has fired for demo-hp before** (twice, last 2026-08-04 20:12:54). Today's unseal, through the R-193 recovery screen, fired it **not at all**. Either the screen's unlock path does not emit it or the rebuilt-box path bypasses it; **which of those is not established here.** A reinstall and a theft of a machine look identical to the operator | **READY (M) — NEW 2026-08-09** | — | Emit on the recovery-screen unlock path; and decide which reinstall milestones are worth one line each | CC |
|
||||
| **R-281** | ~~**The hub said NOTHING through an entire reinstall — and the tripwire for a sealed-backup unseal did not fire on a real unseal.**~~ **WITHDRAWN 2026-08-09 — THE FINDING WAS AN ARTEFACT OF MY OWN MEASUREMENT, AND IT WAS WRONG IN BOTH DIRECTIONS.** The operator's mailbox settled it: the hub fired **twenty events** on 2026-08-09, and `escrow_blob_served` **DID** fire — 10:19:41 UTC / **12:19 CEST**, eight minutes before the verified restore. **Cause of the false reading, ESTABLISHED (not guessed):** the P7 query copied `/data/hub.db` **without `hub.db-wal`**. The hub runs SQLite in WAL mode (R-172), so every write since the last checkpoint was invisible. **The signature is an exact match:** P7 reported *"2 events all day, newest `db_dump_completed` 00:30:07"*, and the number of rows on 08-09 at or before 00:30:07 is **exactly 2**. **The two obvious alternatives were TESTED AND REFUTED**, not waved away: a **timezone offset** — all nine mailbox stamps equal the hub's UTC + 2 h exactly (`escrow_blob_served` 10:19→12:19, `host_down` 09:28→11:28, and seven more), so the window was right; and a **wrong customer key or wrong store** — the same table and key return the correct rows now. A live re-run cannot reproduce the fault because the WAL has since been checkpointed; the case rests on the command text plus the 2-of-2 count signature, and that is stated rather than dressed up as a reproduction. **This is a trap this project has already documented** — `operations/nodes.md` says copying `hub.db` alone is *"valid but stale … the worst failure shape"* — and I had avoided it correctly earlier in the same session before hitting it. **Split out: → R-285** (the real, opposite defect) and **→ R-286** (the measurement lesson) | **WITHDRAWN 2026-08-09** | — | Superseded by R-285/R-286 | CC |
|
||||
| **R-282** | **One secret, three different Hungarian names, and the email sends the customer to a page their box is not showing.** Sending it from the hub is „**Visszaállító** kód küldése"; the email that arrives is subject „Jelszó-**visszaállítási** kód", body „**Visszaállító** kód: …", and it instructs *„Add meg a vezérlőpult »**Elfelejtett jelszó**« oldalán"*; the page the box actually serves is „A szerver **beállítása**" asking for a „**Beállító** kód". **A rebuilt box shows a SETUP page and the hub can only send a RESET mail** (because hub-side the customer is still `claimed_at 2026-07-21`), so the instruction names a route that does not exist on screen. **It does work if you ignore the instructions** — the reset code was accepted on the setup page (302 + session), so this is naming, not function. **It cost this session real time and one wasted code:** the operator supplied a 3-word Hungarian code believing it was the recovery code, because the hub calls the claim code „Visszaállító kód" and the ESCROW code is also „Visszaállító kód" — the only reliable discriminator is length (claim = 3 Hungarian words; recovery = **10** EFF-list words, and the recovery screen does say „(tíz szó)") | **READY (S) — NEW 2026-08-09** | — | Pick one name per secret and use it on all three surfaces; make the mail's page reference match what a rebuilt box actually shows | CC |
|
||||
| **R-283** | **After a rebuild the hub says "Claimed 18d ago" while the box serves its first-run setup page.** `customer_claims` for demo-hp still read `claimed_at 2026-07-21 16:29:25`, `generation 2`, `issued_at 2026-08-03` while the freshly provisioned guest — whose `settings.json` is new — correctly showed „A szerver beállítása". The two sides never reconcile: the hub's claim state survives a guest rebuild and the box's does not. Consequences: the operator's screen says the box is claimed when it is not, a resend produces a RESET code instead of a SETUP code (→ **R-282**), and any previously issued code fails with *„Hibás vagy lejárt kód"* — a message that is technically true and tells the customer nothing about the real cause, namely their own reinstall. Mirror image of **R-214/R-235** (an already-paired box still told to pair itself) | **READY (S) — NEW 2026-08-09** | — | Let a report from a box carrying no claim state clear the hub's, or show both sides on the operator page | CC |
|
||||
| **R-284** | **„A kiválasztott tárhely majdnem megtelt." on a store that is 93 % FREE — an apparent inverted threshold.** Calibre-Web's deploy page rendered `<option value="/mnt/sys_drive" data-free-percent="93">` alongside „Tárhely (sys_drive) — **64.2 GB szabad**" and the warning „A kiválasztott tárhely majdnem megtelt." 93 % free read as 93 % used is the obvious candidate, and `checkStorageSpace(this)` is the function to look at. **Not confirmed by reading the code** — reported as measured output only. A capacity warning that cries wolf on an empty disk is one a customer learns to click past | **READY (XS) — NEW 2026-08-09** | — | Check `checkStorageSpace`'s comparison against `data-free-percent`; add a render test per branch | CC |
|
||||
| **R-285** | **A planned, supervised reinstall pages the operator as if the machine had died — there is no notion of expected downtime anywhere.** During the 2026-08-09 rehearsal the hub sent, all `status: sent` to the operator channel: `host_stale` 08:58 UTC, `node_stale` 09:00, **`host_down` 09:28 (error)**, **`node_down` 09:30 (error)**, `host_leaf_changed` 09:31, `host_recovered` 09:31, `node_recovered` 09:34, `offsite_delivery_stuck` 09:34 — eight operator mails for work that was deliberate, attended and announced. **This is the OPPOSITE gap from the one R-281 filed:** the alarms are not missing, they are indiscriminate. `host_stale` at 30 min and `host_down` at 60 min (`monitor/host_staleness.go:22-23`, `downAfter = 2 * threshold`) cannot distinguish a wiped-on-purpose box from a dead one, and `host_leaf_changed` firing on a reinstall is correct-but-expected. **Note the interaction with the mute used on 2026-08-09 evening:** blocking a customer silences everything, so today the only two settings are *page me for planned work* and *tell me nothing at all*. **What is owed is a middle:** a maintenance window, or an operator-set expected-downtime flag, that suppresses staleness and leaf-change while leaving genuine faults audible | **READY (M) — NEW 2026-08-09** | — | The evidence is the operator's mailbox plus `events`/`notification_log` for 2026-08-09 | CC |
|
||||
| **R-286** | **A control drawn from the same channel as the measurement cannot detect a defect in that channel — and this one passed while the measurement was wrong.** The P7 check asked *"did the hub record anything?"* against a stale snapshot, got "no", and then validated itself with *"is the hub recording ANY events today, for anyone?"* — **against the same stale snapshot**. It answered "2 events all day", which was internally consistent and entirely false. The standing rule (*an absent log line is not evidence*) was followed in form: a positive control WAS run. **It was the wrong kind of control**, and nothing in the rule as written says so. **The independent channel existed and was available the whole time: the operator's mailbox.** One glance at it would have shown eight alarms in the window. **The durable lesson, to be added where the standing rules live:** a control must come from a DIFFERENT channel than the measurement — same query, same snapshot, same API, same clock all fail this. **Concrete follow-through owed:** (a) add this to the standing rules in `runbooks/workspace-CLAUDE.md`; (b) any hub-state check in a runbook must copy `-wal` or query the pod directly, never `cat hub.db` alone — the trap `operations/nodes.md` already documents | **READY (S) — NEW 2026-08-09** | — | Parent: R-281 (withdrawn) | CC |
|
||||
| **R-287** | **`felhom-agent` CI is red for a TRUE reason, and the diagnosis it was filed under is wrong in every particular.** The task premise was *"the gate is sensitive to being run against a tag ref rather than a branch"*. **It is not.** `check-published-versions.py` enumerates releases from the **Gitea tags API** (`/api/v1/repos/admin/felhom-agent/tags?limit=200`, `main()`), so the checked-out ref is irrelevant; and the two previous tag pushes **passed** (run 190 `v0.126.0`, run 216 `v0.127.0`). **What is actually true:** run 267 (main, `28ba8593b8`, 2026-08-08 14:29 UTC) printed `ok v0.120.0: binary downloadable`; run 284 (tag, **the same commit**, 2026-08-09 09:30 UTC) printed `FAIL v0.120.0 — binary NOT downloadable (HTTP 404)`. **A published release became uninstallable between those two runs.** The registry now holds exactly the ten newest versions (0.121.0…0.128.0); `0.128.0` was published **2026-08-08 16:47 CEST = 14:47 UTC, eighteen minutes after run 267**, and `0.120.0` is gone. **WHO REMOVED IT IS NOT ESTABLISHED, and that is stated rather than guessed:** `package_cleanup_rule` is **empty** (queried in Postgres), `app.ini` sets no package limit, `publish-agent.sh` only pre-deletes the version it is publishing (`:77`), the Gitea pod has **53 days uptime and 0 restarts** so `RUN_AT_START` did not fire, and **no `DELETE` on the packages API appears in 48 h of Gitea router logs**. The leading candidate is the internal `[cron.cleanup_packages]` `@midnight` job, which falls inside the window and would leave no router log line — **leading candidate is not established.** **THEREFORE NO GATE WAS SILENCED AND NO WORKFLOW WAS CHANGED.** Silencing it would hide a released-but-uninstallable version, which is the exact R-115 defect the gate exists to catch. **It will recur:** if the ten-version window is real, the next publish evicts `0.121.0`. **Two honest fixes, both out of tonight's scope:** bound the gate to versions at or above the vouched `min_agent` floor (0.127.0 today — nothing installs 0.120.0 and nothing can), or retire ancient tags when their packages go. **Also measured, and good news:** the failure alarm DID send — `RESEND-ACCEPTED id=fa1a7a83-714f-4357-b0ca-d3c4bb7ae73f` | **READY (M) — NEW 2026-08-09** | — | Establish the deleter first; do not raise the retention until it is known | Viktor |
|
||||
| **R-288** | **The capability map is too long to be read, and that is why it stops being true.** `architecture/00-capability-map.md` is **134 642 bytes / 19 456 words across 99 table rows in only 159 lines** — because the rows ARE the length. Measured, longest first: the unaided-recovery-journey row is **3 024 words**, the offsite-password-recovery row **1 087**, the unattended-restore-proof row **971**, the app/guest-network-failure row **904**. That single longest row is a novella of nested corrections, each appended rather than resolved. Its own verification stamp reads **2026-07-16 against evidence corpus @ felhom.eu tip `4b18cc5`** (line 23) — three weeks stale, which is the measurable consequence: nobody re-reads a row they cannot finish. **This is the project's memory, so restructuring it is surgery and wants daylight** — filed, deliberately not attempted in the 2026-08-09 session. **What the shape should probably be:** one line of status per capability plus a dated evidence pointer, with the argument moved to the audit it came from | **READY (M) — NEW 2026-08-09** | — | Do not fold this into another session; it needs its own | Viktor |
|
||||
| **R-289** | **R-182's register row describes a defect the code no longer has — an OPEN row that is a false alarm.** The row reads *"A full disk tells the operator about ONE app and silently swallows every other app's refusal for an hour"*, cited at `hub/internal/notify/dispatcher.go:268`. **Read against live source 2026-08-09, that is fixed:** the per-run digest `backup_run_failures` is allowlisted (`hub/internal/api/handler.go:1837`), operator-only (`dispatcher.go:423`) and templated (`notify/templates.go:48`); `recovery_unit_capture_failed` is now a **record-only** event (`dispatcher.go:376`) whose notification IS the digest, listing every failed app in one mail; and a cooldown drop now writes a `suppressed` row instead of vanishing (`dispatcher.go:314-330`). The capability map already records the fixed shape (*"EVERY failing app, in ONE mail per run"*). **So the register is behind the code, which is the mirror of the decay this session was looking for** — the session expected stale PROOFS and found a stale DEFECT. **Not closed here, deliberately:** the digest's *delivery* has never been observed end to end (the page's own "an app crashes — the email leg has never been confirmed" card), so the honest move is to re-scope R-182 to that residue rather than tick it | **READY (XS) — NEW 2026-08-09** | — | Re-scope R-182 to "the digest has never been seen delivering", or close it and open that | CC |
|
||||
| **R-290** | **Most capability-map rows that back a green dot cite no evidence document at all — measured, 20 of 28 probed.** The page's *Walked* means *"done end to end on real hardware, evidence on file"*. Extracting the evidence column for the 28 rows behind the page's claims found a `tests/` or `audits/` path in **8**; the other 20 carry prose only. **Consequence, applied this session:** of 32 claims the page drew as Walked, **12 were downgraded to Built** because no walk document exists for them — `install.installer-by-tag`, `use.lifecycle`, `drives.enrol`, `drives.migrate`, `backup.tier1`, `backup.whole-machine`, `backup.restore-proof`, `fault.selfheal`, `fault.operator-email`, `fail.drive-filling`, `fail.lost-recovery-code`, `fail.hub-down`. **This is not a claim that those twelve are false** — several are near-certainly fine — it is a claim that nothing on file distinguishes them from an opinion, which is exactly what the status word promises. **The gate now enforces it going forward:** `scripts/check_stands.py` fails on `status: walked` with no `evidence:` source. **What is owed:** either a walk document per row, or an honest demotion in the map itself (the map is the source; the dataset only follows it) | **READY (M) — NEW 2026-08-09** | R-288 | The dataset was corrected; **the capability map itself still says PROVEN-LIVE for these rows** and is the thing to fix | Viktor |
|
||||
|
||||
**Explicitly still open, untouched by this session:** R-246, R-255, R-256, R-257, R-261, R-262,
|
||||
R-263, **R-264** (the twenty-one undecided facts — a design session of its own), R-240, R-243,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""check_stands.py — the gate on documentation/architecture/where-felhom-stands.yaml.
|
||||
|
||||
The page that dataset renders is the operator's picture of where the product stands. Its whole
|
||||
value is that every claim on it is traceable, so this checks the traceability rather than the
|
||||
claims: a claim nobody can trace is not a weaker claim, it is not a claim at all.
|
||||
|
||||
WHAT IT CONVICTS ON (each is a FAIL, exit 1):
|
||||
|
||||
1. an entry with NO source — §4 rule 1: that is a defect, not a claim
|
||||
2. an `evidence:` path that does not resolve — a citation nobody opened
|
||||
3. a `register:` id absent from OPEN-ITEMS.md — a dangling register reference
|
||||
4. a `capability-map:` anchor not found — the row it derives from has moved or gone
|
||||
5. status `walked` with no `evidence:` source — THE LOAD-BEARING ONE. "Walked" means done end
|
||||
to end on real hardware with evidence on file. If no evidence document is cited, the page is
|
||||
drawing a green dot from an opinion. This is the rule the positive control exercises: take a
|
||||
claim the page marks `missing`, mark it `walked`, and this fires.
|
||||
|
||||
WHAT IT DOES NOT DO, said plainly so the green is not over-read: it does not read the evidence and
|
||||
judge whether it supports the claim, and it cannot. A human verdict lives in each entry's
|
||||
`verified:` block; this gate checks that the paperwork exists, not that the paperwork is right.
|
||||
|
||||
Run: python3 scripts/check_stands.py [path-to-yaml]
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DEFAULT = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.yaml")
|
||||
REGISTER = os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md")
|
||||
CAPMAP = os.path.join(ROOT, "documentation", "architecture", "00-capability-map.md")
|
||||
DOCS = os.path.join(ROOT, "documentation")
|
||||
|
||||
|
||||
def load(path):
|
||||
"""Minimal parser for the shape this file is written in.
|
||||
|
||||
Deliberately not PyYAML: the runner image and the gate hosts carry python3 and nothing else
|
||||
(the same constraint that keeps `uses:` out of the CI workflow), and a gate that needs a pip
|
||||
install is a gate that silently stops running.
|
||||
"""
|
||||
claims, cur, in_sources = [], None, False
|
||||
for raw in open(path, encoding="utf-8"):
|
||||
line = raw.rstrip("\n")
|
||||
if line.startswith(" - id:"):
|
||||
cur = {"id": line.split(":", 1)[1].strip(), "sources": []}
|
||||
claims.append(cur)
|
||||
in_sources = False
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
if line.strip() == "sources:":
|
||||
in_sources = True
|
||||
continue
|
||||
m = re.match(r'\s+- (capability-map|evidence|register): (.*)$', line)
|
||||
if in_sources and m:
|
||||
cur["sources"].append((m.group(1), m.group(2).strip().strip('"')))
|
||||
continue
|
||||
m = re.match(r'\s+(status|title|verdict|depth): (.*)$', line)
|
||||
if m:
|
||||
in_sources = False
|
||||
cur[m.group(1)] = m.group(2).strip().strip('"')
|
||||
return claims
|
||||
|
||||
|
||||
def main():
|
||||
path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT
|
||||
claims = load(path)
|
||||
register = open(REGISTER, encoding="utf-8").read()
|
||||
# Normalise the map before matching: its row labels carry ** and ` markup, so a literal
|
||||
# substring probe against the raw file fails on text that is plainly there. Matching the
|
||||
# rendered words is what the citation means.
|
||||
capmap = open(CAPMAP, encoding="utf-8").read()
|
||||
capmap = re.sub(r"[`*]", "", capmap)
|
||||
capmap = re.sub(r"\s+", " ", capmap).lower()
|
||||
|
||||
print("check_stands — %d claim(s) in %s" % (len(claims), os.path.relpath(path, ROOT)))
|
||||
fails = []
|
||||
|
||||
for c in claims:
|
||||
cid = c["id"]
|
||||
if not c["sources"]:
|
||||
fails.append("%s: NO SOURCE — an entry with no source is a defect, not a claim" % cid)
|
||||
for kind, ref in c["sources"]:
|
||||
if kind == "evidence":
|
||||
if not os.path.exists(os.path.join(DOCS, ref)):
|
||||
fails.append("%s: evidence path does not resolve: documentation/%s" % (cid, ref))
|
||||
elif kind == "register":
|
||||
if not re.search(r"\*\*%s\*\*" % re.escape(ref), register):
|
||||
fails.append("%s: register id %s is not in OPEN-ITEMS.md" % (cid, ref))
|
||||
elif kind == "capability-map":
|
||||
probe = " ".join(ref.split()[:4])
|
||||
probe = re.sub(r"[`*]", "", probe)
|
||||
if probe and probe.lower() not in capmap:
|
||||
fails.append("%s: capability-map anchor not found: %r" % (cid, probe))
|
||||
if c.get("status") == "walked" and not any(k == "evidence" for k, _ in c["sources"]):
|
||||
fails.append("%s: status 'walked' but NO evidence document cited — a green dot "
|
||||
"drawn from an opinion" % cid)
|
||||
|
||||
counts = {}
|
||||
for c in claims:
|
||||
counts[c.get("status")] = counts.get(c.get("status"), 0) + 1
|
||||
print(" statuses: " + ", ".join("%s=%d" % kv for kv in sorted(counts.items())))
|
||||
|
||||
if fails:
|
||||
print("\nCONVICTED — %d problem(s):" % len(fails))
|
||||
for f in fails:
|
||||
print(" " + f)
|
||||
return 1
|
||||
print("\ncheck_stands: OK — every claim cites a source, every citation resolves, and every "
|
||||
"'walked' cites a walk.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""render_stands.py — generate where-felhom-stands.html from where-felhom-stands.yaml.
|
||||
|
||||
Run by hand, deliberately not wired into CI:
|
||||
|
||||
python3 scripts/check_stands.py && python3 scripts/render_stands.py
|
||||
|
||||
WHY THIS EXISTS. The first version of this page was a React bundle: one 154 KB artefact whose
|
||||
content sat gzip+base64 inside a JS module map. It rendered, and that was all it could do — it
|
||||
could not be edited, a diff of it showed nothing a person could read, and it could not be
|
||||
regenerated when a status moved. So it began going stale the moment it was committed, which is the
|
||||
one thing a picture of "where we stand" must not do.
|
||||
|
||||
This emits plain static HTML with inline CSS and NO JavaScript. It is bigger on disk than it needs
|
||||
to be and that is the trade: the page is a build product of a file a person can read and diff.
|
||||
|
||||
The palette and layout deliberately match the 2026-08-09 snapshot — the operator should recognise
|
||||
the page, not learn a new one.
|
||||
"""
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SRC = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.yaml")
|
||||
OUT = os.path.join(ROOT, "documentation", "architecture", "where-felhom-stands.html")
|
||||
|
||||
DOT = {"walked": "#34d399", "built": "#60a5fa", "partial": "#fbbf24", "missing": "#64748b"}
|
||||
LEGEND = [
|
||||
("walked", "Walked", "done end to end on real hardware, evidence on file"),
|
||||
("built", "Built", "shipped and tested, the real path never walked"),
|
||||
("partial", "Partial", "some walked, some not — the note says which"),
|
||||
("missing", "Missing", "does not exist"),
|
||||
]
|
||||
STAGES = {1: "Getting a box", 2: "Making it theirs", 3: "Using it", 4: "Drives",
|
||||
5: "Backing up", 6: "When something goes wrong", 7: "Getting everything back"}
|
||||
|
||||
|
||||
def load(path):
|
||||
claims, cur, sect = [], None, None
|
||||
meta = {}
|
||||
for raw in open(path, encoding="utf-8"):
|
||||
line = raw.rstrip("\n")
|
||||
if line.startswith("verified_on:"):
|
||||
meta["date"] = line.split(":", 1)[1].strip()
|
||||
if line.startswith(" - id:"):
|
||||
cur = {"id": line.split(":", 1)[1].strip(), "sources": [], "changed": None}
|
||||
claims.append(cur)
|
||||
sect = None
|
||||
continue
|
||||
if cur is None:
|
||||
continue
|
||||
if line.strip() == "sources:":
|
||||
sect = "sources"
|
||||
continue
|
||||
if line.strip() == "changed:":
|
||||
sect = "changed"
|
||||
cur["changed"] = {}
|
||||
continue
|
||||
if line.strip() == "verified:":
|
||||
sect = "verified"
|
||||
continue
|
||||
m = re.match(r'\s+- (capability-map|evidence|register): (.*)$', line)
|
||||
if sect == "sources" and m:
|
||||
cur["sources"].append((m.group(1), m.group(2).strip().strip('"')))
|
||||
continue
|
||||
m = re.match(r'\s+(\w+): (.*)$', line)
|
||||
if not m:
|
||||
continue
|
||||
k, v = m.group(1), m.group(2).strip().strip('"')
|
||||
if sect == "changed":
|
||||
cur["changed"][k] = v
|
||||
elif sect == "verified":
|
||||
cur.setdefault("v_" + k, v)
|
||||
else:
|
||||
cur.setdefault(k, v)
|
||||
return meta, claims
|
||||
|
||||
|
||||
def esc(t):
|
||||
return html.escape(t, quote=False)
|
||||
|
||||
|
||||
def dot(status):
|
||||
return ('<span style="width:8px;height:8px;border-radius:50%%;background:%s;'
|
||||
'flex:0 0 8px;margin-top:5px"></span>' % DOT.get(status, "#64748b"))
|
||||
|
||||
|
||||
def claim_html(c):
|
||||
bits = ['<div style="display:flex;gap:8px;align-items:flex-start;margin-bottom:9px">', dot(c.get("status"))]
|
||||
body = ['<span style="font-size:12px;line-height:1.45;color:#b8c4d6;text-wrap:pretty">',
|
||||
esc(c.get("title", ""))]
|
||||
if c.get("changed"):
|
||||
body.append('<span style="display:inline-block;margin-left:6px;padding:1px 5px;border-radius:3px;'
|
||||
'background:#3a2a12;color:#fbbf24;font-size:10px;white-space:nowrap">changed 2026-08-09, was %s</span>'
|
||||
% esc(c["changed"].get("from", "?")))
|
||||
if c.get("v_verdict") in ("needs-hardware", "contested"):
|
||||
col = "#a78bfa" if c["v_verdict"] == "contested" else "#7dd3fc"
|
||||
body.append('<span style="display:inline-block;margin-left:6px;padding:1px 5px;border-radius:3px;'
|
||||
'background:#1b2740;color:%s;font-size:10px;white-space:nowrap">%s</span>'
|
||||
% (col, esc(c["v_verdict"])))
|
||||
if c.get("note"):
|
||||
body.append('<span style="display:block;color:#7c8aa3;font-size:11px;margin-top:3px">%s</span>'
|
||||
% esc(c["note"]))
|
||||
if c.get("changed", {}) and c["changed"].get("reason"):
|
||||
body.append('<span style="display:block;color:#9a7b3a;font-size:11px;margin-top:2px">why it moved: %s</span>'
|
||||
% esc(c["changed"]["reason"]))
|
||||
srcs = " · ".join("%s %s" % (k, esc(v if len(v) < 62 else v[:59] + "…")) for k, v in c["sources"])
|
||||
body.append('<span style="display:block;color:#4b5b76;font-size:10px;margin-top:3px;'
|
||||
'font-family:ui-monospace,SFMono-Regular,Menlo,monospace">%s</span>' % srcs)
|
||||
body.append("</span>")
|
||||
bits.append("".join(body))
|
||||
bits.append("</div>")
|
||||
return "".join(bits)
|
||||
|
||||
|
||||
def panel(title, inner, sub=None):
|
||||
h = ['<div style="background:#121b2c;border:1px solid #1f2c44;border-radius:6px;padding:14px 13px">']
|
||||
h.append('<div style="font-size:14px;font-weight:500;padding-bottom:9px;margin-bottom:10px;'
|
||||
'border-bottom:1px solid #1f2c44">%s</div>' % title)
|
||||
if sub:
|
||||
h.append('<div style="color:#6b7a91;font-size:11px;margin:-6px 0 10px">%s</div>' % esc(sub))
|
||||
h.append(inner)
|
||||
h.append("</div>")
|
||||
return "".join(h)
|
||||
|
||||
|
||||
def main():
|
||||
meta, claims = load(SRC)
|
||||
counts = {}
|
||||
for c in claims:
|
||||
counts[c.get("status")] = counts.get(c.get("status"), 0) + 1
|
||||
changed = [c for c in claims if c.get("changed")]
|
||||
|
||||
o = []
|
||||
o.append("<!DOCTYPE html>\n<html lang=\"en\"><head><meta charset=\"utf-8\">")
|
||||
o.append('<meta name="viewport" content="width=device-width, initial-scale=1">')
|
||||
o.append("<title>Where Felhom stands</title><style>")
|
||||
o.append("@page{size:A3 landscape;margin:8mm}"
|
||||
"*{margin:0;padding:0;box-sizing:border-box}"
|
||||
"body{background:#0b1220;color:#e6edf7;font-family:-apple-system,BlinkMacSystemFont,"
|
||||
"'Segoe UI',Roboto,sans-serif;-webkit-font-smoothing:antialiased;padding:26px 20px}"
|
||||
".wrap{max-width:1600px;margin:0 auto}"
|
||||
".grid7{display:grid;grid-template-columns:repeat(7,1fr);gap:12px;align-items:start}"
|
||||
".grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;align-items:start}"
|
||||
".grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;align-items:start}"
|
||||
"h1{font-size:22px;font-weight:600;letter-spacing:-.2px}"
|
||||
"a{color:#60a5fa}"
|
||||
"@media print{body{padding:0}.grid7{grid-template-columns:repeat(7,1fr)}}")
|
||||
o.append("</style></head><body><div class=\"wrap\">")
|
||||
|
||||
o.append("<h1>Where Felhom stands</h1>")
|
||||
o.append('<div style="color:#8fa0b8;font-size:12.5px;margin:7px 0 4px;max-width:1100px;line-height:1.5">'
|
||||
'What we built, what happens when things go wrong, and what is still missing. '
|
||||
'<b style="color:#cbd7e8">Generated from '
|
||||
'<code>where-felhom-stands.yaml</code></b>, which cites the capability map, register row or '
|
||||
'evidence document behind every claim — and which is checked by '
|
||||
'<code>scripts/check_stands.py</code>.</div>')
|
||||
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:16px">Verified %s against '
|
||||
'felhom-agent <code>28ba8593b8</code>, felhom-controller <code>c732fe1283</code>, hub '
|
||||
'<code>56f8aa611c</code>. <b style="color:#fbbf24">%d status(es) moved in that pass</b> — '
|
||||
'each is marked on the page.</div>' % (meta.get("date", "?"), len(changed)))
|
||||
|
||||
leg = ['<div style="display:flex;gap:22px;flex-wrap:wrap;background:#121b2c;border:1px solid #1f2c44;'
|
||||
'border-radius:6px;padding:11px 14px;margin-bottom:18px">']
|
||||
for k, name, desc in LEGEND:
|
||||
leg.append('<div style="display:flex;gap:8px;align-items:flex-start">%s'
|
||||
'<span style="font-size:11.5px"><b>%s</b> <span style="color:#7c8aa3">(%d)</span>'
|
||||
'<span style="display:block;color:#6b7a91;font-size:10.5px">%s</span></span></div>'
|
||||
% (dot(k), name, counts.get(k, 0), esc(desc)))
|
||||
leg.append('<div style="display:flex;gap:8px;align-items:flex-start"><span style="font-size:11.5px">'
|
||||
'<b style="color:#7dd3fc">needs-hardware</b><span style="display:block;color:#6b7a91;'
|
||||
'font-size:10.5px">only a running box could settle it</span></span></div>')
|
||||
leg.append("</div>")
|
||||
o.append("".join(leg))
|
||||
|
||||
o.append('<div style="font-size:15px;font-weight:600;margin:0 0 9px">The journey</div>')
|
||||
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:10px">Seven stages, left to '
|
||||
'right, as they happen to a person.</div>')
|
||||
o.append('<div class="grid7">')
|
||||
for n in range(1, 8):
|
||||
inner = "".join(claim_html(c) for c in claims
|
||||
if c.get("band") == "journey" and c.get("stage") == str(n))
|
||||
o.append(panel('<span style="color:#4b5b76;font-family:ui-monospace,monospace">%d</span> %s'
|
||||
% (n, esc(STAGES[n])), inner))
|
||||
o.append("</div>")
|
||||
|
||||
fails = [c for c in claims if c.get("band") == "failures"]
|
||||
o.append('<div style="font-size:15px;font-weight:600;margin:22px 0 9px">When things go wrong</div>')
|
||||
o.append('<div style="color:#6b7a91;font-size:11.5px;margin-bottom:10px">%d situations, and what '
|
||||
'actually happens in each.</div>' % len(fails))
|
||||
o.append('<div class="grid3">')
|
||||
for c in fails:
|
||||
o.append(panel(esc(c.get("title", "").split(" — ")[0])[:74], claim_html(c)))
|
||||
o.append("</div>")
|
||||
|
||||
o.append('<div style="margin-top:26px;padding-top:12px;border-top:1px solid #1f2c44;color:#4b5b76;'
|
||||
'font-size:10.5px;line-height:1.6">'
|
||||
'Generated by <code>scripts/render_stands.py</code> from <code>where-felhom-stands.yaml</code>. '
|
||||
'Do not hand-edit this file — edit the data and regenerate. '
|
||||
'The 2026-08-09 React bundle is kept as '
|
||||
'<code>where-felhom-stands-2026-08-09-snapshot.html</code> and is not maintained.'
|
||||
'</div>')
|
||||
o.append("</div></body></html>")
|
||||
|
||||
open(OUT, "w", encoding="utf-8").write("\n".join(o))
|
||||
print("wrote %s (%d bytes) — %d claims, %d changed"
|
||||
% (os.path.relpath(OUT, ROOT), os.path.getsize(OUT), len(claims), len(changed)))
|
||||
print(" statuses: " + ", ".join("%s=%d" % kv for kv in sorted(counts.items())))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user