hub v0.137.0 source + burn-down round 2 in felhom.eu: R-277 R-581 R-600 R-544 R-855 R-134 R-92 R-292 R-599 R-725 R-728 (hub), R-819 R-857 R-555 R-364 R-587 (gates/tools), R-571 R-129 R-124-runbook (docs); 28 rows closed incl. catalog + agent v0.147.0 rows, R-350 merged into R-132, R-888 opened, R-887 mechanism (249 -> 222)
gates / gates (push) Successful in 2m3s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 19:43:37 +02:00
parent e0d884565f
commit 557629d2bf
65 changed files with 2680 additions and 122 deletions
@@ -370,6 +370,23 @@ own; every caller that is not the customer must decide for itself whether the ap
| `storage_handlers.go` (1600 L) | **DELETE (→agent)** | Format/attach/mount/disconnect/migrate-drive/decommission disk UI. Any survivor is a **thin client calling the agent API** (e.g. per-volume placement requests). | hazard |
| `templates/` (HTML, non-Go) | **PORT** | Remove disk-wizard + DR pages; keep app/deploy/backup/settings pages. | needs-rework |
#### Alert placement — inline under the storage bars, or the top banner (R-571)
**[FACT, read from source 2026-10-05, felhom-controller `114ff27`, `controller/internal/web/alerts.go`]** Every
dashboard alert is an `Alert` with two placement fields: `PageOnly` (the pages it may appear on; empty = every
page) and `Inline` (rendered by the page template in place, not by the layout's banner). `GetAlerts` returns
the list (endpoint-drift, agent-channel and dead-app alerts first, then the rest sorted error > warning > info,
capped at five plus an overflow line), and `layout.html` paints only those that are not `Inline` and match the
page; `GetInlineAlerts(page)` hands the dashboard and monitoring pages their inline ones.
**Exactly one warning is inline today:** the „storage is not on a separate drive" health warning. It is
`PageOnly: dashboard, monitoring` and `Inline: true`, so it sits quietly under the storage bars; **every other
warning, including every off-site failure (`07` §6.7), renders in the top banner on every page.** The choice
is made by the warning's KIND (`monitor.WarnKindStorageNotSeparate`, read with `report.WarningKindAt`), never
by its words — the earlier Hungarian-substring test would have moved the warning to the red banner on every
page the day the sentence was translated (R-553, pinned by `TestR553_DiskWarningPlacementSurvivesWordingChange`).
A new inline warning needs its own kind, not a text match.
### `scripts/`
| File | Class | Reason | Risk |
|---|---|---|---|
@@ -249,7 +249,8 @@ the identity bundle's shape is `{tunnel_token, pbs_token, wg_private_key, restic
parts a host-loss recovery would read (INV Part D2.3): `hosts.dr_record_json` is `{}` on all three
hosts; `host_escrow.directive_json` is `{}` on both escrowed hosts; `dr_recipe.host_half.drives` is
`[]` on every customer including two with enrolled data drives; and `dr_recipe.host_half.pbs.namespace`
reads `"root"` while the real namespaces are `demo-felhom` / `demo-hp`. → **R-105**, **R-106**.
reads `"root"` while the real namespaces are `demo-felhom` / `demo-hp`. → **R-105**, **R-106**. *Since agent v0.147.0 (R-124) a genuine root namespace is
recorded as `""` (PBS's own spelling) beside `namespace_state: resolved`; the word `root` is no longer written.*
---
@@ -795,6 +796,32 @@ digests all resolve today (`audits/version-travel-2026-09-26/A7/`). Options are
it; older images of the app are deleted. A restore that needs an older version re-pulls it — as before; the limit
above is unchanged, and kept data (decision 40) is not touched by the image clean-up.
### 6.7 Why an off-site run failed — the failure classifier (R-571)
**[FACT, read from source 2026-10-05, felhom-controller `114ff27`]** When an off-site (restic) run fails, the
controller names ONE cause before it writes the note the customer's page shows days later
(`ClassifyOffsiteFailure` in `controller/internal/backup/offbox.go`; the head line is the bundle key
`note.offsite.fail_<class>`, followed by the run time and the sanitised error). The classes, in the order
they are tested:
| Class | Decided by | What it means for the household |
|---|---|---|
| `orphaned` | our sentinel `ErrOffboxOrphaned` | the remote store was made with a key this box no longer has; nothing new reaches it until the operator acts |
| `quota` | our sentinel `ErrOffsiteQuota` (the pre-run soft-quota gate, R-553) | the backup did not fit the remote space; the run was refused before upload |
| `locked` | our sentinel `ErrOffsiteLocked`, or restic's lock text (R-104) | an interrupted earlier run left the store locked and both self-heal layers failed |
| `no_units` | text: „produced no snapshots" | there was nothing to send — no chosen app had a backup on any drive |
| `no_repo` | restic's text: „unable to open config file" / „is there a repository…" | nothing exists at the remote location |
| `transport` | ssh/restic/rclone text: connection refused/reset, timeout, permission denied, host key, handshake, DNS, unreachable | the remote store could not be reached (network or sign-in) |
| `unknown` | everything else | the cause is not known — the page says so instead of guessing |
**Two kinds of signal, and the difference matters.** The first three are OUR sentinels: they survive a
translation of our own text, which is why R-553 replaced the Hungarian-word match for `quota`. The text
signatures are **restic's, ssh's and rclone's own English output** — external strings we neither write nor
translate. A new restic or OpenSSH version that rewords an error moves that failure to `unknown`; it never
moves it to a wrong class. The order is deliberate: a cause that cannot be told apart returns `unknown`
rather than being folded into a neighbour. Where the warning is SHOWN on the dashboard is a separate rule —
`02-controller-module-map.md`, „Alert placement".
## 7. The recovery chain (D3) — the reason this document exists
**[DESIGN] 3-2-1 describes copies. It does not describe recovery.**