Files
felhom.eu/documentation/architecture/capability-map-detail/E.md
T
admin 0d463a859e
gates / gates (push) Successful in 6m12s
answers 2026-10-12 Parts D, F, G: controller 0.308.0 delivered, the capability map restructured, the notification design; 128 -> 97
- closed: R-922 R-255 R-266 R-333 R-577 R-554 R-562 (controller 0.308.0 on demo-hp, demo-felhom, Tester 1), R-288 (map 212 KB -> 48 KB, detail per section)
- R-521 narrowed to F7; R-388 design in 08 §8.1 (two questions for the operator)
- privacy draft: why the registered address stays (S1); 02 module map: setup wizard deleted
- STATUS + REPORT-answers-2026-10-12.md; docs.md routing line names capability-map-detail/

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-12 15:54:44 +02:00

365 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Capability map — detail, section E. Access, networking & household use
> **What this file is.** The long text of every row of section E of
> [`00-capability-map.md`](../00-capability-map.md), moved here **verbatim** on 2026-10-12 (R-288,
> R-290 folded in; `09` §3 decisions 214 and 229) from the map at commit `1b45bb4d`. Each entry is one
> former table row, its cells laid out one per paragraph; the cell text itself was not edited. The
> map row carries the capability's ONE status word and its dated evidence pointer; this file keeps
> the argument, the corrections and the history. **Where this text and the map row disagree on the
> status, the map row is current** (see the map header for the words that were normalised).
>
> A new correction goes **under the entry it corrects, dated** — never into the map row.
<a id="e1"></a>
## E1 — Remote access via Cloudflare Tunnel + Traefik (per-app subdomains)
*Map row: [E1](../00-capability-map.md#e1) · former map line 184.*
**Scenario (as written):** Remote access via Cloudflare Tunnel + Traefik (per-app subdomains)
**Components (as written):** cloudflared, traefik
**Status (as written):** **PROVEN-LIVE**
**Evidence (as written):** `CAMPAIGN-2` T-FLT-CF
**Gap / roadmap (as written):** Per-customer zone-scoped CF tokens (blast-radius ruling)
---
<a id="e2"></a>
## E2 — LAN access when internet is down (lan_resolver)
*Map row: [E2](../00-capability-map.md#e2) · former map line 185.*
**Scenario (as written):** LAN access when internet is down (lan_resolver)
**Components (as written):** agent
**Status (as written):** **IMPLEMENTED**
**Evidence (as written):** —
**Gap / roadmap (as written):** Never drilled as a customer experience ("net down — can I reach my photos?") → R-19
---
<a id="e3"></a>
## E3 — Phone photo backup
*Map row: [E3](../00-capability-map.md#e3) · former map line 186.*
**Scenario (as written):** Phone photo backup
**Components (as written):** immich (classified)
**Status (as written):** **PROVEN-LIVE**
**Evidence (as written):** 6D end-to-end restore proof
**Gap / roadmap (as written):**
---
<a id="e4"></a>
## E4 — Documents/OCR
*Map row: [E4](../00-capability-map.md#e4) · former map line 187.*
**Scenario (as written):** Documents/OCR
**Components (as written):** paperless-ngx (classified)
**Status (as written):** **PROVEN-LIVE**
**Evidence (as written):** `CAMPAIGN-6C` 4P-1 (deploy paperless-ngx, ingest 3 docs via consume flow, OCR + PDF/A ~90s) + 4P-2/3/5
**Gap / roadmap (as written):** Consume-folder ingestion awkward without SMB → R-7
---
<a id="e5"></a>
## E5 — Files from Windows Explorer / Mac Finder (SMB server)
*Map row: [E5](../00-capability-map.md#e5) · former map line 188.*
**Scenario (as written):** Files from Windows Explorer / Mac Finder (SMB **server**)
**Components (as written):** controller v0.145.0 + `felhom-samba:1.0.0`
**Status (as written):** **PROVEN-LIVE**
**Evidence (as written):** `felhom-controller` REPORT.md (v0.144.0) + `controller/sharing.md`; transport verdict `audits/SPIKE-lan-discovery-2026-07-18.md`
**Gap / roadmap (as written):** „Megosztás" page: enable + one household password + shares (new folder or picked existing, per-share read-only). Fourth protected infra stack (host-net, smbd+**nmbd**+wsdd). Live on demo: 445 reachable, NetBIOS `FELHOM` resolves, write/read byte-compare PASS, write to a read-only share REFUSED, SMB writes land as uid 1000. **Explorer leg PASSED 2026-07-18 (Viktor):** Network → FELHOM → both shares open; a real Explorer *save* into `dokumentumok` landed owned uid 1000, and a write into the read-only `filmek` was refused by Windows with the folder left untouched. Share data RIDES BOTH BACKUP TIERS (R-7b, controller v0.145.0, Model B′ sibling shares source): tier-2 cross-drive legs + an offsite `_shares` restic snapshot carrying the share definitions and the credential copy, with a „Megosztások" restore. **All four legs PROVEN-LIVE on demo 2026-07-18** — tier-2 tree md5-verified; offsite snapshots `e0b9d723` (Viktor 12:18:16Z) and `4e2b15ec` both carrying manifest + `passdb.tar`; restore round-trip returned a deleted probe file byte-identical and a deleted share DEFINITION with its original flags without overwriting live files; samba liveness → hub-accepted `health_critical`. Remaining human leg: SMB positive auth with the real household password
---
<a id="e6"></a>
## E6 — Media to TV via DLNA
*Map row: [E6](../00-capability-map.md#e6) · former map line 189.*
**Scenario (as written):** Media to TV via DLNA
**Components (as written):** —
**Status (as written):** **MISSING**
**Evidence (as written):** —
**Gap / roadmap (as written):** Jellyfin app exists; DLNA/SSDP unvalidated → R-6, R-8
---
<a id="e7"></a>
## E7 — File access via browser
*Map row: [E7](../00-capability-map.md#e7) · former map line 190.*
**Scenario (as written):** File access via browser
**Components (as written):** FileBrowser (infra app, auto-mount sync)
**Status (as written):** **IMPLEMENTED**
**Evidence (as written):** FileBrowser runs healthy + userdata-bound (`storage-lifecycle-acceptance-2026-06-15`, `CAMPAIGN-3`)
**Gap / roadmap (as written):** Actual browse/download **through FileBrowser** is exercised in no doc. (Cited `CAMPAIGN-2` T-PAGE-ALL renders only the controller dashboard pages, not FileBrowser.) Demoted. **2026-07-26, controller v0.172.0 (R-75) — status DELIBERATELY UNCHANGED.** The canonical drop-zone now has its own FileBrowser source („Beolvasás" → `/srv/beolvasas`, a separate bind of `<system namespace>/userdata/import`) and the app page carries a per-app deep link into it. Verified live on demo-hp: the source and bind are in the generated config, the app page renders `https://files.enkisfelhom.hu/files/Beolvas%C3%A1s/paperless`, and a file written through FileBrowser's OWN mount was consumed and deleted by paperless in ~30 s. **That is still not a browse.** Nothing in this arc drove the FileBrowser HTTP UI — no browser exists on DooPlex — so the row's standing caveat survives intact and the upgrade to PROVEN-LIVE remains unearned. What it would take: a human click-through, or an authenticated `/api/resources` round-trip against the live instance. See `controller/import-and-data-paths.md`
---
<a id="e8"></a>
## E8 — Indítópult (app launcher) — one-tap grid of the household's openable apps
*Map row: [E8](../00-capability-map.md#e8) · former map line 191.*
**Scenario (as written):** **Indítópult (app launcher) — one-tap grid of the household's openable apps**
**Components (as written):** controller v0.163.0
**Status (as written):** **IMPLEMENTED**
**Evidence (as written):** New FIRST sidebar page `/launcher`: colored tiles (deterministic slug color or `.felhom.yml` `brand_color`) + white glyph/monogram, one per openable app (tile ⟺ „Megnyitás" — subdomain presence is the single criterion; controller excluded). Operational → `<a target=_blank>` to the public URL; stopped → greyed + state badge, no link. `/` stays the Vezérlőpult. Endpoint-level + render-test verified; `felhom-controller/REPORT.md` (2026-07-24)
**Gap / roadmap (as written):** Live operator click-through of a real tile → app pending (browser automation not available on DooPlex). Follow-up: curate `brand_color` for top catalog apps (R-72). Sharing the launcher outside the household is now the capability-URL guest link — see the row below
---
<a id="e9"></a>
## E9 — Indítópult megosztás (vendég link) — capability URL `/s/<token>` serves a standalone read-only guest launcher (no account, no admin session); optional per-share password; QR
*Map row: [E9](../00-capability-map.md#e9) · former map line 192.*
**Scenario (as written):** **Indítópult megosztás (vendég link)** — capability URL `/s/<token>` serves a standalone read-only guest launcher (no account, no admin session); optional per-share password; QR
**Components (as written):** controller v0.165.0
**Status (as written):** **IMPLEMENTED**
**Evidence and gap (as written — this row's text carries a `|` outside code, so its last cells are kept together as they stood):** 160-bit `crypto/rand` token, constant-time match (empty stored = disabled = byte-identical to the mux default 404); guest headers `noindex`/`no-referrer`/`no-store`; optional SEPARATE bcrypt share password + its own per-IP attempt map; signed cookie `HMAC(token`\ | `passwordHash)` keyed with `session_secret` (rotate-token OR change-password invalidates all cookies); token redacted in logs (`/s/<redacted>`). Groups A–G (14 tests) + 3 red-proofs; **§13 endpoint-level live validation on 9201 all-pass** (`felhom-controller/REPORT.md` 2026-07-24). **Design ruling: member accounts SUPERSEDED by this capability-URL model; per-member tile visibility parked under the SSO arc (R-15).** | Full operator browser click-through + a validation doc pending → then PROVEN-LIVE. Accepted residuals: link-preview crawlers fetch once (noindex prevents indexing); reverse-proxy/CF access logs hold the path (ops-tier); the modal link carries the request Host (LAN-IP admin ⇒ LAN-IP link)
---
<a id="e10"></a>
## E10 — Forgot dashboard password → instant reset code
*Map row: [E10](../00-capability-map.md#e10) · former map line 193.*
**Scenario (as written):** Forgot dashboard password → instant reset code
**Components (as written):** controller v0.123, hub
**Status (as written):** **PROVEN-LIVE**
**Evidence (as written):** `DRILL-day0-take2-2026-07-12` F-15 (live re-run of the exact failure path: hash applied 1s after request, code accepted first try)
**Gap / roadmap (as written):**
---
<a id="e11"></a>
## E11 — The dashboard in English — the household picks its language; Hungarian unchanged
*Map row: [E11](../00-capability-map.md#e11) · former map line 194.*
**Scenario (as written):** **The dashboard in English** — the household picks its language; Hungarian unchanged
**Components (as written):** controller v0.247.0 (spike) → v0.250.0 (slice 1, R-556 CLOSED)
**Status (as written):** **PARTIAL — PROVEN-LIVE for every dashboard template** (all 36 templates carry their copy in the bundles; the switch is on every dashboard page)
**Evidence (as written):** `audits/i18n-slice1-2026-09-17/C/live/` (and `A/`, `B/`, `audits/i18n-2026-09-17/live/`) — on demo-hp 0.250.0 each Hungarian page equals its 0.249.0 fetch once the new switch form is removed (live numbers aside; login, claim, catch-all byte-identical); `POST /settings/language` made every page English and back; the hub stored `hu`, `en`, `hu` (16:45:10Z, 16:46:06Z, 16:46:43Z). Unit: `TestI18nParity` (106 states vs fixtures from unconverted templates), `TestI18nParityCoversEveryMarker`, `TestI18nEnglishPages`, `TestDirectRenderHandlersFollowLanguage`, all red-proofed. Design `architecture/10-localisation.md`
**Gap / roadmap (as written):** Go-side messages and three app-name page titles (R-557 after R-553, R-566), hub e-mails (R-558), console/download page (R-559), catalog copy (R-560), guide + English stranger drill (R-561); ASCII-only Hungarian invisible to the English page test (R-565). **Hungarian households see the switch only once the fleet floor reaches 0.250.0**
---
<a id="e12"></a>
## E12 — The box tells visitors apart — a stranger's wrong passwords lock only the stranger (dashboard, setup gate, apps that read the visitor from the right)
*Map row: [E12](../00-capability-map.md#e12) · former map line 195.*
**Scenario (as written):** **The box tells visitors apart — a stranger's wrong passwords lock only the stranger** (dashboard, setup gate, apps that read the visitor from the right)
**Components (as written):** controller v0.286.1 (`felhom-tunnel`, traefik trust of cloudflared only, `clientaddr.go`), catalog router resets
**Status (as written):** **PARTIAL**
**Evidence (as written):** `audits/visitors-2026-10-01/A/L2-demo-hp-real-tunnel.txt` (real tunnel: the dashboard counter keyed on the visitor `37.191.56.193`, locked after 5 while forging a new X-Forwarded-For each try); `A/L1-9202-live.txt` (simulated tunnel: the household from another address in at once); `A/bookstack-3.6.txt`
**Gap / roadmap (as written):** The second OUTSIDE address on the real tunnel was refused by Cloudflare before the box → R-779; right-walking app settings → R-776; Emby/Jellyfin LAN rights → R-777
---
<a id="e13"></a>
## E13 — The family gate — a permanent door with each family member's own login in front of an app (Grimmory with its e-reader exceptions, MeTube with none)
*Map row: [E13](../00-capability-map.md#e13) · former map line 196.*
**Scenario (as written):** **The family gate — a permanent door with each family member's own login in front of an app** (Grimmory with its e-reader exceptions, MeTube with none)
**Components (as written):** controller v0.287.0 (`internal/family`, `stacks/family_gate.go`, the Család card), catalog `family_gate:` + `check-family-gate.py`, golden 0.287.0
**Status (as written):** **PROVEN** on 9202 through the product: a stranger reached nothing (36 requests, LAN + simulated tunnel), members with their own logins, reset/remove/logout end access at the next request, a stranger's guesses lock only the stranger, each exception keeps the app's own login and a look-alike stays gated, the controller down → error never the app; survives an update and a remove + restore. Demo boxes on 0.287.0. Not yet: a household's phone through the real internet.
**Evidence (as written):** `audits/family-gate-2026-10-02/A/items.txt`, `B/box/`
**Gap / roadmap (as written):** R-780 closed; R-775 (narrowed), R-796, R-797
---
<a id="e14"></a>
## E14 — Multiple household users / per-person accounts
*Map row: [E14](../00-capability-map.md#e14) · former map line 197.*
**Scenario (as written):** Multiple household users / per-person accounts
**Components (as written):** —
**Status (as written):** **MISSING**
**Evidence (as written):** —
**Gap / roadmap (as written):** Single dashboard password; acceptable for alpha → R-15
---
<a id="e15"></a>
## E15 — A second login step for the dashboard (a TOTP code or a passkey)
*Map row: [E15](../00-capability-map.md#e15) · former map line 198.*
**Scenario (as written):** A second login step for the dashboard (a TOTP code or a passkey)
**Components (as written):** —
**Status (as written):** **MISSING**
**Evidence (as written):** —
**Gap / roadmap (as written):** One password, one bcrypt hash (`controller/internal/web/auth.go:37-44`) → R-811 (added 2026-10-03)
---
<a id="e16"></a>
## E16 — The household can leave Felhom, or outlive it — the box runs without the hub, the household owns its domain, tunnel and off-site account, and can export everything
*Map row: [E16](../00-capability-map.md#e16) · former map line 199.*
**Scenario (as written):** The household can leave Felhom, or outlive it — the box runs without the hub, the household owns its domain, tunnel and off-site account, and can export everything
**Components (as written):** —
**Status (as written):** **MISSING** (as a written answer)
**Evidence (as written):** the LOST-hub half only: `_recovery-inventory-2026-07-28.md` §D2.4, `07` §8 row 11b
**Gap / roadmap (as written):** Leaving and hand-over are answered nowhere → R-810 (spike, added 2026-10-03)
---
<a id="e17"></a>
## E17 — The host agent cannot reach root without the operator key: exact sudo patterns, no agent-written file installed where root reads it without a content check, the agent binary only by an operator-signed update checked as root
*Map row: [E17](../00-capability-map.md#e17) · former map line 200.*
**Scenario (as written):** **The host agent cannot reach root without the operator key: exact sudo patterns, no agent-written file installed where root reads it without a content check, the agent binary only by an operator-signed update checked as root**
**Components (as written):** agent **v0.146.1** (R-861; delivered by a step bundle, R-880)
**Status (as written):** **PROVEN-LIVE on both demo boxes (2026-10-05) — with three named residuals**
**Evidence (as written):** `audits/hub-safety-2026-10-05/partF/` (real sudo: 23 of 29 attacks allowed before, 0 after; 64 capability commands allowed; `sudo -l` 93/93 on demo-hp and demo-felhom after the bundle; capability probe 67/67; a staged unit over `/etc/sudoers.d` refused live)
**Gap / roadmap (as written):** `03` §3.1: the controller-swap image ref (guest-scoped), the felhom-op SSH key (hub-delivered, unsigned; felhom-op's sudo is scoped), the escrow ceremony relays R → R-861
---
<a id="e18"></a>
## E18 — WireGuard base infra always-on; OOB operator access (felhom-sshd, /32 peer)
*Map row: [E18](../00-capability-map.md#e18) · former map line 201.*
**Scenario (as written):** WireGuard base infra always-on; OOB operator access (felhom-sshd, /32 peer)
**Components (as written):** agent v0.72, hub v0.35
**Status (as written):** **IMPLEMENTED**
**Evidence (as written):** `SPIKE-oob-wg-operator-peer-2026-07-05`, `SPIKE-felhom-sshd-2026-07-05`
**Gap / roadmap (as written):** Mutual-repair desired-state arc not built → R-13. **CHECKED 2026-08-08 (R-260) and this row was NOT claiming something untrue** — it claims the capability is implemented, never that it is monitored, so no correction was owed. What WAS untrue is narrower and sat one layer down: **the hub's own OOB health check could not see whether the operator's key was installed.** `HostOOBRow` mirrored five of the agent's eight OOB fields, so `operator_key_configured` — emitted every heartbeat since agent v0.72.0, i.e. from this row's own vintage — was discarded by `encoding/json` on arrival, and `oobDegraded` returned `ok` for a box with felhom-sshd active, reachable, a valid config, a configured peer and **no operator key at all**. `operator_peer_configured`, which it did read, only says the peer IP is in desired-state — that OOB is MEANT to work, not that entry is possible. Fixed hub v0.99.0; the missing key now degrades and the alert NAMES it; a stanza too old to carry the field is reported distinctly and is never a silent ok. Pinned end-to-end from raw report JSON by `TestHostOOB_MissingOperatorKey_EndToEnd` and `TestHostOOB_NoKeyField_IsNotSilentlyOK_EndToEnd`
---
<a id="e19"></a>
## E19 — The operator can see WHERE a managed host is — its LAN address and its WireGuard address, on the host page
*Map row: [E19](../00-capability-map.md#e19) · former map line 202.*
**Scenario (as written):** The operator can see WHERE a managed host is — its LAN address and its WireGuard address, on the host page
**Components (as written):** agent **v0.119.0**, hub **v0.85.0**
**Status (as written):** **PROVEN-LIVE** (2026-07-31)
**Evidence (as written):** `audits/host-addresses-visible-2026-07-31.md`
**Gap / roadmap (as written):** Before this the LAN IP was **not reportable at all** — `HostMetrics` carried no address of any kind — and the WG IP existed only in `/offsite`'s peer table keyed by pubkey (peer→host, never host→peer). New wire field `addresses[]`, one row per (interface, address); `IsGlobalUnicast()` is the whole filter, chosen by MEASURING both demo boxes, and it needs no veth/fwbr denylist because that plumbing carries no IP. Rendered live on both 0.119.0 hosts matching their `ip addr` ground truth exactly. **Two honesty properties carry the risk and are both red-proofed:** WireGuard shows the hub ALLOCATION and whether the box CONFIRMS holding it (allocation alone cannot tell a live tunnel from a peer never applied), and an agent below 0.119.0 renders **UNKNOWN, never "no addresses"** — proven live on `drill-r50-0a4f9a` (0.113.0). **Not covered:** a two-LAN-bridge box and a real WG drift, neither of which exists to observe
---
<a id="e20"></a>
## E20 — The operator can see whether a managed host's guests still have working networking — and how often the watchdog had to repair them
*Map row: [E20](../00-capability-map.md#e20) · former map line 203.*
**Scenario (as written):** The operator can see whether a managed host's **guests still have working networking** — and **how often the watchdog had to repair them**
**Components (as written):** agent **v0.92.0** (emitter, 2026-07-21), hub **v0.104.0** (reader, 2026-08-13)
**Status (as written):** **IMPLEMENTED**
**Evidence and gap (as written — this row's text carries a `|` outside code, so its last cells are kept together as they stood):** `backlog/OPEN-ITEMS.md` R-319; `hub/internal/web/hosts_guestnet_test.go` (7 tests, fixtures copied verbatim from `demo-felhom-8363b5`'s live `host_reports` row) | The agent emitted `guest_net` on every heartbeat for **twenty-three days** while the string occurred **nowhere** in `felhom.eu/hub/` — stored as raw text in `report_json`, read by nothing (R-260/R-264, the first of that census's readers to be built). **The fact that carries the risk is `heals_last_hour`, not `state`:** a guest the watchdog keeps repairing is healthy at every instant anyone looks, so rendering the state alone would give it a green tick — the failed-disk-drawn-as-a-healthy-empty-disk shape. `heal_succeeded` is decoded beside it, because six FAILED repairs is a guest that is down while six successful ones is a nuisance. **Unknown is never drawn as healthy:** three absences, three sentences (agent < 0.92.0; a capable agent that sent nothing; a guest whose own state the watchdog did not assert), and a malformed stanza degrades to unknown without a 500. **Three red-proofs, each mutation asserted applied by grep before its run**, including the one that matters — removing the unknown branches and watching a silent machine render as healthy. **Positive control that it is WIRED and not merely written: the wire-contract gate's checked-tag count rose 182 → 190** as the eight `guest_net` allowlist entries were deleted (an allowlisted tag is skipped, so leaving them would have meant these fields were never checked) | **IMPLEMENTED, not PROVEN-LIVE, and the distinction is the honest half.** Every scenario is proven against the real wire in tests, and the healthy case renders correctly for the live fleet — but **no machine has ever been observed with a climbing repair count on this card**, because neither demo box has needed a repair since the watchdog shipped. The signal this card exists for has therefore never been seen firing on hardware. It moves to PROVEN-LIVE the first time a real repair count is watched appearing. **No alarm was added, deliberately** (R-319): the incident behind this was about nobody being able to SEE the condition, and a new email on a fleet of two demo machines is untested noise — revisit when a third machine exists or when a count is seen climbing
---
<a id="e21"></a>
## E21 — Break-glass management-plane recovery
*Map row: [E21](../00-capability-map.md#e21) · former map line 204.*
**Scenario (as written):** Break-glass management-plane recovery
**Components (as written):** agent v0.71, hub v0.84
**Status (as written):** **IMPLEMENTED**
**Evidence (as written):** `runbooks/break-glass.md`
**Gap / roadmap (as written):** hub v0.84.0 adds an **operator-SESSION** retrieval path (host page → Console access → Reveal; `POST /hosts/{id}/reveal-recovery-credential`, CSRF-gated, writes a customer-visible `recovery_credential_revealed` event) beside the pre-existing **global-key** one (`GET /api/v1/admin/hosts/{id}/recovery-credential`), which is untouched and stays the route for when the hub UI itself is down. **The credential half is now PROVEN (2026-07-31):** the vaulted `demo-hp-bb76ea` password was verified against the box's own `/etc/shadow` hash AND minted a real PVE ticket — `POST /api2/json/access/ticket` → **HTTP 200, `root@pam`, 367-char ticket**, the exact API the login form submits to. Still IMPLEMENTED rather than PROVEN-LIVE overall, because the path has not been exercised on a REAL lockout (SSH was available throughout). Discovered during that check: the card's Copy button shipped `disabled` until a Reveal and silently no-opped, leaving ANOTHER host's password in the clipboard — fixed in hub v0.86.0. **Sealed at rest since hub v0.135.0 (R-133 CLOSED):** the off-site seal and key; live 2026-10-05: 4 legacy rows sealed at start-up, 0 left plain, and the demo-hp reveal still minted a PVE ticket (HTTP 200; a wrong password 401) — `audits/hub-safety-2026-10-05/partB/`. A database backup now needs `OFFSITE_SECRET_KEY` too → R-173
---