answers 2026-10-12 Parts D, F, G: controller 0.308.0 delivered, the capability map restructured, the notification design; 128 -> 97
gates / gates (push) Successful in 6m12s
gates / gates (push) Successful in 6m12s
- 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
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -53,7 +53,7 @@ Risk tags: **clean** · **needs-rework** · **hazard** (entangles a delete-targe
|
||||
- The **disk/storage/host half deletes wholesale to the agent**: all of `storage/`,
|
||||
`monitor/watchdog.go`, the restic/cross-drive/disk-layout/drive-mount parts of `backup/`,
|
||||
`report/infra_backup*`+`infra_pull`, and the host-physical parts of `system/`.
|
||||
- The **setup wizard (`setup/`) is obsolete** — the agent provisions the controller.
|
||||
- The **setup wizard (`setup/`) was obsolete and is DELETED** (controller v0.308.0, R-554, `09` §3 decision 222) — the agent provisions the controller; a box that cannot get its configuration shows one waiting page (`bootstrap.WaitForConfig`).
|
||||
- **The single biggest hazard is `backup/`**: the keep side (DB dumps, Docker-volume
|
||||
archive, per-app restore — needed by `appexport/` and the backup UI) and the delete side
|
||||
(restic, cross-drive, drive-mount) are **interleaved inside the same files**
|
||||
@@ -209,7 +209,6 @@ own; every caller that is not the customer must decide for itself whether the ap
|
||||
| `selftest/` | Startup checks (docker/dirs/catalog/hub/**restic repos**/mountpoint) | backup, config, settings, system |
|
||||
| `selfupdate/` | Self-update: pull image, edit compose, `up -d` | config |
|
||||
| `settings/` | `settings.json` persistent state: **storage paths/disconnect/decommission**, cross-drive cfg, notif prefs, geo, integration state, DB-validation cache | — |
|
||||
| `setup/` | **First-run wizard** (scan drives, hub-restore, manual config) | backup, config, report, settings, web |
|
||||
| `stacks/` | Docker Compose lifecycle, deploy + memory validation, metadata (`.felhom.yml`), HDD-data delete | config, crypto, system |
|
||||
| `storage/` | **Physical disk** scan/format/attach/mount/migrate/fstab/safety | backup, settings, util |
|
||||
| `sync/` | Catalog git-sync (pull templates) | config |
|
||||
@@ -297,7 +296,7 @@ own; every caller that is not the customer must decide for itself whether the ap
|
||||
| File | Class | Reason | Risk |
|
||||
|---|---|---|---|
|
||||
| `notify/notifier.go` | **KEEP/MODIFY** | Direct hub event channel (own API key) — keep; prune infra event types that move to the agent (`storage_disconnected`, `crossdrive_*`, `disaster_recovery_*`). | clean |
|
||||
| `recovery/info.go` | **DELETE (obsolete)** | Generates a DR text guide (OS install, docker-setup.sh, hub restore UI); DR is now agent+hub provisioning. | clean |
|
||||
| `recovery/info.go` | **KEEP (rewritten v0.308.0)** | `recovery-info.txt` (only when a retrieval password is set): points at the dashboard's Restore page and the ten-word recovery code, in the box's language (R-554). | clean |
|
||||
| `scheduler/scheduler.go` | **KEEP** | Generic cron/interval, Budapest TZ. | clean |
|
||||
| `selftest/selftest.go` | **PORT** | Keep docker/dirs/catalog/hub checks; drop restic-repo + system-data **mountpoint** checks (→agent). | needs-rework |
|
||||
|
||||
@@ -323,7 +322,7 @@ own; every caller that is not the customer must decide for itself whether the ap
|
||||
|---|---|---|---|
|
||||
| `settings/settings.go` (1101 L) | **MODIFY (split)** | Keep notif prefs, integration state, geo, DB-validation cache, cross-drive *intent*. The **storage-path registry** (`StoragePath` with `Disconnected`/`DisconnectedAt`/`StoppedStacks`/decommission) is disk-management state → reshape to **per-volume placement** fed by the agent's storage manifest; disconnect/decommission/migrate state leaves. (UUID is *not* a persisted field — runtime-derived from fstab.) | hazard |
|
||||
|
||||
### `setup/` — all DELETE (obsolete); the agent provisions the controller
|
||||
### `setup/` — DELETED in controller v0.308.0 (R-554); kept here as the record
|
||||
| File | Class | Reason | Risk |
|
||||
|---|---|---|---|
|
||||
| `handlers.go`,`setup.go`,`csrf.go`,`network.go` | **DELETE (obsolete)** | First-run wizard (hub-restore, manual config, LAN-IP detection). | needs-rework |
|
||||
|
||||
@@ -469,3 +469,48 @@ was ruled that way on its own merits and does not pre-judge the redesign.
|
||||
|
||||
**Filed as a PRODUCT DECISION, not a defect** — see the register. It is the operator's call to take
|
||||
separately, and no part of it was implemented here.
|
||||
|
||||
### 8.1 The design — „only what you can act on" [DESIGN, R-388 — `09` §3 decision 200, 2026-10-12; NOT BUILT]
|
||||
|
||||
*Operator ruling 2026-10-12 (S5): design now, build in slices later. Written by CC from the direction above and the
|
||||
live page (controller, `settings_notifications.html`: **18 event switches** plus the address, the cooldown and the
|
||||
app-mail settings).*
|
||||
|
||||
**The question:** what does a household see on its notification page, and what does each choice send?
|
||||
|
||||
**What a household can act on, read off today's 18 detectors:**
|
||||
|
||||
| Group (what the household sees) | Detectors behind it | Who acts |
|
||||
|---|---|---|
|
||||
| **1. „A gépem és a meghajtóim"** — your drives and space | `storage_disconnected`, `storage_reconnected`, `disk_warning`, `disk_critical`, `offbox_enlarge_blocked` | the household (plug it back, free space, enlarge) |
|
||||
| **2. „Az alkalmazásaim"** — your apps | `app_stopped_unhealthy`, `app_start_failed`, `app_update_held`, `app_update_undone` | mostly us; the household may want to know an app is down |
|
||||
| **3. „Amit mi intézünk"** — what we handle | `backup_failed`, `db_dump_failed`, `backup_integrity_failed`, `crossdrive_failed`, `expected_backup_missed`, `expected_dbdump_missed`, `node_down`, `health_critical`, `health_recovered` | us (the operator always gets these, with the detail) |
|
||||
|
||||
**Options:**
|
||||
|
||||
- **A — three group switches** (recommended). The page shows the three groups above, each one switch, with one
|
||||
sentence saying what it sends. A group switch sets or clears its detectors in the existing `enabled_events` list,
|
||||
so **slice 1 changes no wire and no hub code**. Group 3's mail later changes wording to „we noticed X and are
|
||||
handling it" (slice 2). Cost: one controller slice + one hub slice. A new detector joins a group; the page does
|
||||
not grow.
|
||||
- **B — one switch + a weekly summary.** „Tell me only what I can fix" (group 1) and a weekly digest of everything
|
||||
else. Cost: a new digest mail (hub), a new schedule, and the household loses same-day news of a down app.
|
||||
|
||||
**Pick: A.** It follows the direction (the household decides three things, not 18), keeps every existing
|
||||
mechanism, and each slice is reversible on its own. B can be added later as a fourth choice if households ask.
|
||||
|
||||
**What the operator still gets:** everything, unchanged — the operator channel never reads the household's switches
|
||||
(`processOperator` is independent of `enabled_events`).
|
||||
|
||||
**The first slice (controller only):** the settings page shows the three groups; saving writes the same
|
||||
`enabled_events` list as today (a group = all its detectors); a household whose saved list mixes a group (some on,
|
||||
some off) sees that group as „egyedi" (custom) with a link to the per-detector list, which stays reachable under a
|
||||
„Részletek" fold — so no household's saved choice is changed or lost. Tests: each group round-trips; a mixed saved
|
||||
list is shown as custom and saved back unchanged; the default list maps to groups 1 + 2 + 3 on.
|
||||
|
||||
**Questions for the operator (at most two):**
|
||||
1. **Group 3 for households: on or off by default?** On = the household hears „we noticed and are handling it"
|
||||
(looked after); off = silence while we fix it. Pick: **on**, with the slice-2 wording, because silence during a
|
||||
multi-day backup fault reads as „nobody noticed" if the household ever finds out.
|
||||
2. **The cooldown field („hány óránként legfeljebb"):** keep it on the page or move it under „Részletek"? Pick:
|
||||
**under Részletek** — it is an engineering choice, which is what this redesign removes.
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,300 @@
|
||||
# Capability map — detail, section C. Protection & recovery (the product promise)
|
||||
|
||||
> **What this file is.** The long text of every row of section C 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="c1"></a>
|
||||
## C1 — Whole-guest backup lands OFF the guest's own physical device; a single-drive box is recorded DEGRADED rather than silently normal (installer Case A/B)
|
||||
|
||||
*Map row: [C1](../00-capability-map.md#c1) · former map line 145.*
|
||||
|
||||
**Scenario (as written):** Whole-guest backup lands OFF the guest's own physical device; a single-drive box is recorded **DEGRADED** rather than silently normal (installer Case A/B)
|
||||
|
||||
**Components (as written):** agent v0.113, host-install v1.22.0
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `E2D-fresh-vm-2026-07-29` C1 (real 1.22.0 install, rc=0, `Day-0 provision SUCCESS`) + C2 (both DEGRADED lines verbatim, `local_backup_target=local`, install did not abort)
|
||||
|
||||
**Gap / roadmap (as written):** Case A (a second drive already present at install) has never fired naturally — only Case B has
|
||||
|
||||
---
|
||||
|
||||
<a id="c2"></a>
|
||||
## C2 — Nightly DB dumps (postgres/mariadb autodiscovery), atomic writes
|
||||
|
||||
*Map row: [C2](../00-capability-map.md#c2) · former map line 146.*
|
||||
|
||||
**Scenario (as written):** Nightly DB dumps (postgres/mariadb autodiscovery), atomic writes
|
||||
|
||||
**Components (as written):** controller v0.118
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-2` T-BAK-FULL (pg+mariadb autodiscovered); atomicity `CAMPAIGN-6B` P4 + `CAMPAIGN-6E` B1/B2 (SIGKILL mid-write → only `.tar.tmp` touched, last-good byte-unchanged); DB restore `CAMPAIGN-6D` P-FAB
|
||||
|
||||
**Gap / roadmap (as written):** (Cited `CAMPAIGN-3` F7 is the *finding* of non-atomic writes, and T-RST-DB was auth-hollow — corrected to the 6B/6E fix-proofs.) T-6E-1 dir-fsync asymmetry (LOW) → R-10 **DB replay route → `07-backup-architecture.md` §8 row 3**
|
||||
|
||||
---
|
||||
|
||||
<a id="c3"></a>
|
||||
## C3 — Tier-2 secondary-drive copy: class-driven legs, v2 relpath layout, NAS-target exclusion, safe-remove boundary
|
||||
|
||||
*Map row: [C3](../00-capability-map.md#c3) · former map line 147.*
|
||||
|
||||
**Scenario (as written):** Tier-2 secondary-drive copy: class-driven legs, v2 relpath layout, NAS-target exclusion, safe-remove boundary
|
||||
|
||||
**Components (as written):** controller v0.135
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-6E-2026-07-15` (P-TIER2 deep-4 PASS), `CAMPAIGN-6C`
|
||||
|
||||
**Gap / roadmap (as written):** **Route + RTO → `07-backup-architecture.md` §8 rows 1, 2, 3b, 4, 5.** **R-403 (controller v0.230.0, 2026-08-31) changes NO status on this row and that is stated rather than left ambiguous:** the nightly copy now refuses to replace a complete unit package with an empty one (`07` §8.2), which removes a way the route could be DESTROYED between uses — it does not change what the route can be relied on for, and every leg it covers is the same one it covered yesterday. Proven live on demo-hp: the same state that deleted 120 082 104 B on v0.229.0 preserved all 7 files on v0.230.0. **R-102 CLOSED 2026-08-31 (controller v0.229.0):** the copy's `recovery-unit/` mirror is now restorable — „Teljes visszaállítás a másolatból" / `POST /backup/tier2/unit-restore` — and was proven live on `demo-hp` with the primary unit moved aside (`audits/DRILL-r102-tier2-unit-2026-08-31/`, §8 row 3b, 28.65 s). R-103 closed with it: the refusal that used to name a button on another page now offers the action on the row itself
|
||||
|
||||
---
|
||||
|
||||
<a id="c4"></a>
|
||||
## C4 — Offsite (restic → Hetzner Storage Box): mandatory class only, raw-data quota, enlargement gate, retention regrouping
|
||||
|
||||
*Map row: [C4](../00-capability-map.md#c4) · former map line 148.*
|
||||
|
||||
**Scenario (as written):** Offsite (restic → Hetzner Storage Box): mandatory class only, raw-data quota, enlargement gate, retention regrouping
|
||||
|
||||
**Components (as written):** controller v0.134, agent, hub
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-6D-2026-07-15` (mandatory-only P-IMMICH; enlargement gate fired at real 50GiB quota P3-DELIVERY); `VALIDATION-offbox-storagebox-2026-07-09` (byte-perfect round-trip)
|
||||
|
||||
**Gap / roadmap (as written):** Raw-data quota (SP-1) + retention regrouping (SP-2) are `SPIKE-restic-snapshot-shape` **dry-run** verdicts — mechanism validated, not fired in a live product run; only the enlargement gate is live-fired. **Reinstall-continuity (controller v0.142.0, 2026-07-17):** a recreated data volume that orphaned the repo (new passphrase can't open the old keys) is now CLASSIFIED (`wrong password or no key found`) → explicit ORPHANED card + event (not nightly-spam) + a move-aside (never-delete) reset (unclaimed auto / claimed confirm), instead of a raw nightly restic error. Fake-based scenarios + red-proofs. **The live leg FIRED on its own during the 2026-07-18 rehearsal** (`tests/VALIDATION-n100-rehearsal-2026-07-18.md`, S7): after a RESET + re-enable, the first offsite run hit the previous lifecycle's ciphertext and the guard **classified it, pushed `offbox_repo_orphaned`, skipped the run and showed the card (16:58:14)** rather than nightly-spamming a raw restic error; the operator-confirmed reset then **moved the repo aside (never deleted) to `.orphaned-20260718` and re-initialised (16:59:26→16:59:32)**, and the next run produced 2 snapshots / 48.717 MiB. The guard behaved exactly as designed — **the finding is that it had to fire at all** (R-32: RESET destroys custody, so the ciphertext it leaves behind is dead by design and should be purged, while the move-aside guard stays correct for reinstall-WITHOUT-RESET). `DIAGNOSE-offbox-repo-orphaned-2026-07-17` **Route + RTO → `07-backup-architecture.md` §8 rows 4, 10, 12, 15** (incl. the R-95 delete exposure and the R-104 stale-lock defect). **2026-08-04 (R-193/R-197, `audits/SPIKE-offsite-credential-recovery-2026-08-04.md`) — the row is NOT overclaiming and the guard is not the gap; the CADENCE is.** This row already recorded that a recreated data volume orphans the repo, and the spike confirms the mechanism at source: `WriteOffboxSecrets` (`offbox.go:392`) mints a fresh 256-bit repo password whenever `<DataDir>/offbox/repo_password` is absent, and **no automatic path ever consults the escrowed one** — `InjectOffboxPassword` has exactly one caller, a web form a human pastes into. What was NOT recorded is that this now fires on an **ordinary, planned, unattended guest rebuild**, on every box: measured on BOTH demo boxes 2026-08-03/04 by comparing `host_escrow.restic_pw_sha256` against `host_escrow_superseded.restic_pw_sha256` (demo-hp `8e03eddf…`→`8a9e33aa…`, demo-felhom `48741892…`→`c60c8bc7…`), orphaning **15 snapshots / 40.9 MB** and **36 snapshots / 1.14 GB** respectively. **demo-felhom is the important half:** it kept its DELIVERY (a stale staged secret restored the target in 76 s) and lost its REPOSITORY anyway, with **nothing marking the escrow stale for 13 h** — `escrow_stale` is wired to `ReissueCredentials`, the one path that does NOT change the repo password (**R-196**), and absent from the rebuild path that does. Status unchanged: the classify-and-move-aside guard remains PROVEN-LIVE and correct, and is predicted (not yet measured) to refuse the 2026-08-05 run on both boxes rather than start a silent fresh history
|
||||
|
||||
---
|
||||
|
||||
<a id="c5"></a>
|
||||
## C5 — Offsite restore: local-preferred scratch, unit-only default, full two-step, missing-only place-to-live
|
||||
|
||||
*Map row: [C5](../00-capability-map.md#c5) · former map line 149.*
|
||||
|
||||
**Scenario (as written):** Offsite restore: local-preferred scratch, unit-only default, full two-step, missing-only place-to-live
|
||||
|
||||
**Components (as written):** controller v0.134/134.1/135
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (2026-07-20)
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-6D` accept legs (immich end-to-end from offsite alone)
|
||||
|
||||
**Gap / roadmap (as written):** **2026-07-19:** `audits/DIAG-immich-restore-2026-07-19.md` finds **no offsite path loads a DB dump** — all three buttons are file-only (R-43). The mechanics in this row's title are each proven; the phrase "**immich end-to-end from offsite alone**" is what is contested, since a DB-indexed app cannot be reconstituted by any offsite action. **RULED 2026-07-19 (Viktor): 6D's destruction hit the FILE TREE ONLY — the database survived in its named volume** (`immich_postgres_data` is a named volume in both the v2 and v3 template eras), so "immich end-to-end from offsite alone" **overclaimed scope**: the file half was proven, the DB half was never destroyed and therefore never restored. Row downgraded PROVEN-LIVE → **PARTIAL**, scope-corrected. Evidence: `audits/DIAG-immich-restore-2026-07-19.md` (no offsite path could replay a DB at all) + the P-FAB destructive re-import (the proven-replay evidence, on the LOCAL path). **2026-07-19, controller v0.148.0:** the DB half now exists in code (R-43 + R-44) and its replay reached a live box — but **round 2 found it aborts against a running app** (`audits/DIAG-immich-restore-round2-2026-07-19.md`, H4: the replay races immich's own schema repair; `clip_index` recreated by the app 2 s before the dump's CREATE INDEX). **2026-07-20, controller v0.153.0: H4 IS CLOSED (R-47)** — both restore paths now replay into a DB-ONLY window (`StartStackServices` brings up the database service alone; the app starts only after the replay exits 0), with a fail-closed refusal when a dump has no identifiable DB service. *(The earlier note here said "closes in v0.149" — that was wrong: v0.149.0 was the F3 dashboard `BackupStatus` fix. R-47 shipped in v0.153.0.)* **2026-07-20: the clean run HAPPENED** — endpoint-level supervised reconstitute of immich from snapshot `49e7cb46` (the very snapshot that aborted in round 2): stop → DB-service-only start → replay rc-0 → full start, no `already exists`, operation reported SUCCESS, immich's own DatabaseService logged `No schema drift detected` twice, 11 assets `active`, 4/4 containers healthy, 231 `public` indexes. **Operator confirmed the immich timeline renders correctly after the reconstitute** (screenshot held, 2026-07-20). Evidence: `felhom-controller/REPORT.md` §4b. **2026-07-20, LATER THE SAME DAY — the destructive drill RAN and the row now earns PROVEN-LIVE.** The operator deleted the photos in immich own UI **and emptied the trash** (the step whose absence makes a drill prove nothing — the round-1 lesson), then restored through the customer-facing UI. **`40 file(s) placed`** against the 6 of the earlier non-destructive run — the files were really gone and really came back — plus 1 DB dump replayed rc-0, 11 assets `active`, `No schema drift detected`, timeline confirmed by the operator. This is the destroy-then-recover proof the 6D downgrade asked for, and it was taken through the customer own buttons, not endpoint shortcuts. Evidence: `felhom-controller/REPORT.md` 4e **Route + RTO → `07-backup-architecture.md` §8 rows 3, 4** — the matrix also records that no offsite action unpacks the named-volume tars it captures (→ R-107)
|
||||
|
||||
---
|
||||
|
||||
<a id="c6"></a>
|
||||
## C6 — Manual `.fab` export/import: class-scoped capture, browser up/download, tunnel-proof chunking
|
||||
|
||||
*Map row: [C6](../00-capability-map.md#c6) · former map line 150.*
|
||||
|
||||
**Scenario (as written):** Manual `.fab` export/import: class-scoped capture, browser up/download, tunnel-proof chunking
|
||||
|
||||
**Components (as written):** controller v0.125/128/130/136
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-6D` P-FAB / Accept #1 (1.7 GB full circle, byte-identical, app boots); chunking `CAMPAIGN-6B` P2 (100 MiB via real CF edge, 120 MiB→413)
|
||||
|
||||
**Gap / roadmap (as written):** Chunking proven at the real CF edge via `curl --resolve`; the **rendered browser file-picker** upload leg is still Viktor's open full-circle test (6C ran it NOT-RUN). C6B-F1 was the 6B *finding*; fix verified in 6D
|
||||
|
||||
---
|
||||
|
||||
<a id="c7"></a>
|
||||
## C7 — Guest-loss DR: PBS restore with full-fidelity layout from archive, restore-test verification
|
||||
|
||||
*Map row: [C7](../00-capability-map.md#c7) · former map line 151.*
|
||||
|
||||
**Scenario (as written):** Guest-loss DR: PBS restore with full-fidelity layout from archive, restore-test verification
|
||||
|
||||
**Components (as written):** agent v0.75/0.76, PBS
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-2` T-P9-DESTROY-RESTORE (whole-guest `pct restore` of 9201 → running+healthy) + T-PBS-VERIFY (`verify_state: ok`, 13 snapshots); `DRILL-GL6-2026-07-08` Phase 0d (restore-test `mount_parity: ok`)
|
||||
|
||||
**Gap / roadmap (as written):** (Cited `VALIDATION-newbox-restore` is offbox **restic** file-restore, wrong tier — corrected.) Real **offsite** guest-loss round-trip still R1-blocked → S5 DR drill **Route + RTO → `07-backup-architecture.md` §8 rows 6, 8, 9** — measured 84–112 s local / 1101 s PBS into a scratch guest; a restore to a DIFFERENT host is unmeasured
|
||||
|
||||
---
|
||||
|
||||
<a id="c8"></a>
|
||||
## C8 — PBS-DR secret self-heal on reused-peer re-provision
|
||||
|
||||
*Map row: [C8](../00-capability-map.md#c8) · former map line 152.*
|
||||
|
||||
**Scenario (as written):** PBS-DR secret self-heal on reused-peer re-provision
|
||||
|
||||
**Components (as written):** hub v0.56
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** hub v0.56.0 (`pbsdrheal/reconciler.go`, `RestageHostPBSSecret`, all §10 red-proofs); `SPIKE-pbsdr-selfheal-2026-07-15` (root cause)
|
||||
|
||||
**Gap / roadmap (as written):** Reconciler is **scoped to one host** (`PBSDRHEAL_ONLY_HOST`), not fleet-wide; already fired live hands-free on drill qm300 (07-15) — real-customer firing + fleet-wide widening pending
|
||||
|
||||
---
|
||||
|
||||
<a id="c9"></a>
|
||||
## C9 — Box survives an unattended app or guest-network failure (a dead app member, a boot-orphaned app, a dead DHCP client) — it is noticed, and where safe it is repaired
|
||||
|
||||
*Map row: [C9](../00-capability-map.md#c9) · former map line 153.*
|
||||
|
||||
**Scenario (as written):** Box survives an **unattended app or guest-network failure** (a dead app member, a boot-orphaned app, a dead DHCP client) — it is noticed, and where safe it is repaired
|
||||
|
||||
**Components (as written):** controller v0.156.0→**v0.190.0**, agent v0.92.1
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (2026-07-21; boot-orphan leg rebuilt and re-proven 2026-08-02 — **6 of 6 hard resets**, repeat count cited per N.5)
|
||||
|
||||
**Evidence (as written):** All three legs exercised on the live demo box, operator-present, in one session — `felhom-controller/REPORT.md` + `felhom-agent/REPORT.md` (2026-07-21). **Dead primary:** `docker stop immich-server` 12:50:40 CEST → `degraded` 13 s later → **exactly one** `app_start_failed` + dashboard banner → restart → banner self-cleared (the 2026-07-20 shape that was silent for 18 h). **Boot orphan:** `pct reboot 9201` → `[bootrecon] 1 boot-orphaned app(s) found: [bookstack]` → started in 1 attempt of 2, **zero alerts** (success inside the boot grace is silent); `StartedAt` proves Docker's `unless-stopped` did NOT resurrect it — only the sweep did, which also answers P1 and confirms the F5 hypothesis. **Dead DHCP client:** deliberate replay of the incident — `kill -9` 12:43:18 → detected on process liveness 57 s later while the lease was still live → healed 12:45:18 with the incident's verbatim invocation; **the tunnel never dropped (`cloudflared Up 29 hours`)**, i.e. the outage was prevented rather than merely observed
|
||||
|
||||
**Gap / roadmap (as written):** **The gap this validation surfaced → R-55, now FIXED (controller v0.157.0, 2026-07-21).** For a *drive-backed* app a customer's deliberate Stop did NOT survive a reboot — the boot bind gate recreated and started every deployed drive-backed app unconditionally. Pre-existing, not introduced by R-52 (whose own gate was observed correct). The gate now also requires the app to still HAVE containers, which is R-52's own `existing-Exited vs absent` predicate: a UI Stop is `compose down` and removes them. So **"a stopped app stays stopped" now holds for drive-backed apps too — PROVEN LIVE 2026-07-21** (TASK-F Part 3, operator-present). immich was stopped through the real UI endpoint (`compose down` → 0 containers), calibre-web and bookstack left running, then `pct reboot 9201`: the gate recreated calibre-web and logged `1 drive-backed app(s) left stopped — zero containers means the customer stopped them on purpose`; immich came back **stopped**, where the identical fixture had brought it back running hours earlier. Zero alerts, ~15 s to steady state. The **static-guest** half of the network leg stays deliberately out of scope → **R-50**. **2026-08-02 — the boot-orphan leg now rests on a RECORDED signal, not an inference (R-166, controller v0.189.0).** Both the R-52 sweep and the R-55 gate above decided "the customer stopped this" from *zero containers*, which is also what a power cut mid-compose and an interrupted deploy leave behind — so two real faults were read as deliberate stops and stranded silently (R-157 mechanism B). The customer's intent is now written to `app.yaml` (`desired_state`) by their own action and read directly. **PROVEN-LIVE on 9201** in three flows: a UI Stop persisted `stopped` and survived a controller restart with the app still down and NOT listed as a candidate; an app recorded `running` whose containers were removed out-of-band was recovered by name (`[bootrecon] 1 boot-orphaned app(s) found: [calibre-web]` → started in 1 attempt) — **the case that was invisible before**; and a legacy `app.yaml` with no field was skipped exactly as before and was never inferred to be stopped. **Interrupted app-data operations are covered separately and are NOT proven-live** — `backup.AppStopGuard` restarts apps left stopped by a killed volume dump / offsite reconstitute / `.fab` export, and that leg is unit-proven + red-proofed only (killing the controller mid-backup on a live box was not exercised): **IMPLEMENTED, not PROVEN-LIVE.** **R-170 and R-171 closed the same day (controller v0.190.0).** **R-157 mechanism A** — the sweep observed ONCE at T+5 s, while docker was still restoring, and never re-checked (3 of 6 hard resets). It is now a settle-then-sweep window: sample every 5 s, settled after 3 identical samples, ONE sweep at the end, terminating on settled or a 50 s budget (sized so settle+budget+one retry stays inside the 90 s dead-app grace; a test rejected 60 s at 95 s). **Repeat count, per this map's own rule: 6 of 6 hard resets on the shipped build brought every app back, and an app the customer had stopped stayed down in all 6** (window settle times 10/40/10/10/15/15 s — i.e. it routinely waited 2–8× longer than the old fixed 5 s). A same-app before/after on one box is the sharpest evidence: the pre-fix window logged `no boot-orphaned apps` for calibre-web at 18:08:35; the fixed one found and recovered it at 18:18:50. **R-170** — `shouldRecreateOnBoot` now reads intent too, so the two boot gates agree; proven live in one reboot (calibre-web `running`+zero containers recreated, immich `stopped` left alone). **R-171 — a regression v0.189.0 introduced, found by reading the diff and CONFIRMED on hardware before any fix was written:** the sweep started an app whose drive was absent, burned both attempts and raised a false dead-app alarm. The *write* hazard was blocked only by an ACCIDENTAL filesystem permission (host-root-owned mountpoint + unprivileged guest) that no code owns and no test pinned — which is why it was fixed rather than noted. New fail-safe `bootrecon.StartGate` (cannot determine ⇒ do not start), also covering quiesce and in-flight app-data operations. **One defect in the fix itself, found by live validation and not by review:** the window sampled the Manager's 10 s-refreshed cache, so "settled" could mean "the cache did not update"; `sampleBootFleet` now refreshes first. Evidence: `audits/DIAG-bootrecon-drive-absent-2026-08-02.md`, `felhom-controller/REPORT.md`
|
||||
|
||||
---
|
||||
|
||||
<a id="c10"></a>
|
||||
## C10 — Crash/power-loss mid-backup/mid-migration → self-heal on next run
|
||||
|
||||
*Map row: [C10](../00-capability-map.md#c10) · former map line 154.*
|
||||
|
||||
**Scenario (as written):** Crash/power-loss mid-backup/mid-migration → self-heal on next run
|
||||
|
||||
**Components (as written):** controller, agent
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-6D` P5-REST (SIGKILL mid-offbox → auto-restart ~15s, run marked failed not false-success, no stale lock); `CAMPAIGN-6E` B1-B3
|
||||
|
||||
**Gap / roadmap (as written):** (Cited `CAMPAIGN-2` T-RBT-* legs were empty / auth-hollow — corrected.) Live mid-**migration** crash→self-heal is the weakest sub-claim (P5-REST is mid-backup)
|
||||
|
||||
---
|
||||
|
||||
<a id="c11"></a>
|
||||
## C11 — An app can be withdrawn from the catalog without orphaning the customers running it (available / hidden / abandoned)
|
||||
|
||||
*Map row: [C11](../00-capability-map.md#c11) · former map line 155.*
|
||||
|
||||
**Scenario (as written):** An app can be **withdrawn from the catalog without orphaning the customers running it** (available / hidden / abandoned)
|
||||
|
||||
**Components (as written):** controller v0.158.1, catalog metadata
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (2026-07-21)
|
||||
|
||||
**Evidence (as written):** TASK-F Part 1. Verified on 9201 through the real endpoints: `lifecycle: abandoned` arrived via the normal catalog sync; plant-it renders 0 times on the Alkalmazások page (control app renders 10); a direct `POST /api/stacks/plant-it/deploy` → **HTTP 409 "Ez az alkalmazás jelenleg nem telepíthető."**; the app page carries the permanent notice and offers no Telepítés button. `felhom-controller/REPORT.md` (2026-07-21)
|
||||
|
||||
**Gap / roadmap (as written):** Deployed instances keep FULL function in every state — lifecycle governs what is offered, never what runs. Orphan detection deliberately never sees the field (red-proofed): a withdrawn template stays in the catalog tree, or every deployed instance would read `Elavult` and be offered deletion. Unknown values fail OPEN; the deploy gate fails CLOSED. R-57
|
||||
|
||||
---
|
||||
|
||||
<a id="c12"></a>
|
||||
## C12 — Box survives a site/network change (relocation, different subnet, DHCP re-lease) with the control plane intact
|
||||
|
||||
*Map row: [C12](../00-capability-map.md#c12) · former map line 156.*
|
||||
|
||||
**Scenario (as written):** Box survives a **site/network change** (relocation, different subnet, DHCP re-lease) with the control plane intact
|
||||
|
||||
**Components (as written):** agent v0.96.0 (island NIC), host-install v1.19.0, controller (unchanged), bootstrap
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-07-25)**
|
||||
|
||||
**Evidence (as written):** **R-50 SHIPPED and deployed to the whole fleet.** The control plane now rides a host-internal, portless island bridge (`vmbr9`, `169.254.253.1/30`↔`.2/30`) with a fixed private address that no LAN/DHCP/site move can invalidate. Proven end-to-end: the spike's F1 replay (renumber the LAN → agent stays bound on the island, control plane HTTP 200; the LAN-literal contrast reproduces the original `bind: cannot assign requested address` daemon-death) + cold-reboot survival (`SPIKE-island-bridge-2026-07-25.md`), the migration runbook run verbatim (`RUNBOOK-island-migration.md`), a fresh provision auto-attaching the island `net1` (A4), and the live migration of **both demo boxes** (demo-hp + demo-felhom, 2026-07-25) — island `/storage` HTTP 200, LAN DNS pinned to the LAN IP (Finding-1), **apps served throughout (0 container restarts)**, hub reporting 0.96.0. **Origin:** `audits/AUDIT-vacation-remote-ops-2026-07-20.md` — the real relocation where the agent's LAN-literal bind took storage/PBS/quiesce/restore-test/DR down silently; that is now structurally impossible on a migrated box
|
||||
|
||||
**Gap / roadmap (as written):** **Fleet: DONE.** Remaining: **R-74** — bring the island to Peti's 2-node cluster (SDN vnet / bridge parity), its own supervised runbook. Related historical: R-51 (dead-primary alerting), R-52 (boot desired-state reconciliation), both shipped
|
||||
|
||||
---
|
||||
|
||||
<a id="c13"></a>
|
||||
## C13 — The customer is warned BEFORE a filesystem fills — per filesystem, in Hungarian, naming the drive and the free space, edge-triggered
|
||||
|
||||
*Map row: [C13](../00-capability-map.md#c13) · former map line 157.*
|
||||
|
||||
**Scenario (as written):** **The customer is warned BEFORE a filesystem fills** — per filesystem, in Hungarian, naming the drive and the free space, edge-triggered
|
||||
|
||||
**Components (as written):** controller **v0.191.0/.1/.2**, hub **v0.89.0** (R-167, decision D-c)
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-08-02)**
|
||||
|
||||
**Evidence (as written):** `audits/SPIKE-r165-mp1-merge-2026-08-02.md` (context) + `felhom-controller/REPORT.md`. Exercised on guest 9201 against a REAL filesystem (`/mnt/sys_drive` filled with `fallocate`): **`disk_warning` at 90% used / 4.7 GB free** → hub `notification_log` `customer | disk_warning | sent` with the dynamic Hungarian rendered; grown to 1.7 GB free → **`disk_critical`** → `customer | sent`; file removed → `critical → ok … cleared silently, re-armed` and the persisted state emptied. **Exactly two events across three boots** — the boot in between produced none, which is the edge trigger holding
|
||||
|
||||
**Gap / roadmap (as written):** **Nothing warned before this.** The only prior signal was the healthcheck's generic `health_degraded` at 90%, for REGISTERED STORAGE PATHS ONLY — it never looked at the docker area or the system-data area, never gave a free-byte figure and never named a drive. **The two event types already existed with NO PRODUCER** (`disk_warning`/`disk_critical`: allowlisted, copy'd, in `DefaultEnabledEvents`, checkbox'd) — the **sixth** *built-but-never-wired* instance here; this ships their producer rather than a seventh near-duplicate type. **Two threshold terms, whichever trips first, and the live proof vindicated the design:** the critical crossing fired on the FREE-BYTE term (1.7 GB) at only **91%** used — a percentage-only rule would have missed it. The hub's generic `customerMessages` entries were REMOVED, because `FormatCustomerEmail` prefers the entry over the message and would discard the label and figures. **Known gap → R-177:** there is no operator-triggerable run-now path; the check is daily 03:30 + once at startup, so confirming a cleared warning on a support call needs a controller restart or a wait
|
||||
|
||||
---
|
||||
|
||||
<a id="c14"></a>
|
||||
## C14 — A failed per-app Tier-1 backup reaches the OPERATOR — EVERY failing app, in ONE mail per run, and every failure recorded whether or not it is mailed
|
||||
|
||||
*Map row: [C14](../00-capability-map.md#c14) · former map line 158.*
|
||||
|
||||
**Scenario (as written):** **A failed per-app Tier-1 backup reaches the OPERATOR — EVERY failing app, in ONE mail per run, and every failure recorded whether or not it is mailed**
|
||||
|
||||
**Components (as written):** controller **v0.194.0**, hub **v0.90.1** (R-158 → R-167 → **R-182**)
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-08-03)**
|
||||
|
||||
**Evidence (as written):** `felhom-controller/REPORT.md`. Two real capture failures on guest 9201 (`mkdir …/backups: permission denied`) → both accepted and stored by the hub, `operator | recovery_unit_capture_failed | sent`, and the positive observable **`customer | recovery_unit_capture_failed | skipped | operator_only`** read from the hub's `notification_log`. One event per app, loop continuing
|
||||
|
||||
**Gap / roadmap (as written):** **Before this the failure was a `[WARN]` line and nothing else** — the manager carried three notify seams and none for the unit capture, so `/backups/apps`, the page you open to ask whether ONE app is backed up, was the one page that never said. **Deliberately NOT `backup_failed`:** that type is customer-enabled by default and carries Hungarian copy, so reusing it — which R-158's own proposal said — would email the customer about a failure they cannot act on. **D-c routes it to the operator and overrides the proposal.** Operator-only is enforced by `notify.operatorOnlyEvents`, NOT by the absence of a `customerMessages` entry (the v0.78.0 defect); a red-proof removing the register entry shows the customer receiving it. **ROW REWRITTEN 2026-08-03 (R-182) — the 2026-08-02 claim was TRUE OF ONE APP AND FALSE OF THE REST, and it is worth saying which.** The signal existed and worked; what it did not do was scale past the first failing app. Measured: nine per-app events reached the hub in one day and **two** operator mails went out, because the cooldown key is `customerID:eventType(+tier)` and this type carries `app` but no `tier` — so the first refused app took the hour and the rest were dropped **before `LogNotification`**, leaving no row on any channel. The old row said "One event per app, loop continuing", which was true of what the CONTROLLER emitted and not of what the operator received. **Now:** the per-app event is the RECORD (hub `recordOnlyEvents`: stored + logged every time, never mailed) and `backup_run_failures` is the NOTIFICATION — one mail per run listing every failed app, its leg and its reason, with the counts and free space. A suppressed operator event of ANY type now leaves a `suppressed` row naming its key. **Proven live on demo-hp 2026-08-03** by a real 64.6 GiB fill (thin pool held 30.78 → 30.78): `notification_log` shows `recovery_unit_capture_failed | operator | recorded` ×2, `backup_run_failures | operator | sent` ×1 naming BOTH apps, and `backup_run_failures | customer | skipped | operator_only`. A second run in the same hour produced a second digest; after freeing space a run completed with 2 volume dumps and **no** digest. The suppression row proved itself on an unplanned event — `disk_critical | suppressed | key=demo-hp:disk_critical` — a collapse that yesterday would have left nothing at all. **The digest's silence is safe only because** the hub's deadline check raises `expected_backup_missed` from report freshness independently of any mail (`monitor/deadline.go:396,417`); that check is load-bearing for this row **⚠ CHECKED 2026-08-08 (R-258) AND THIS ROW WAS NOT CLAIMING SOMETHING UNTRUE — but the adjacent thing WAS false and the row's own narrative invites the wrong reading.** This row is about the OPERATOR notification path (`notification_log`, the per-run digest) and it claims nothing about what `/backups/apps` DRAWS. Independently of it, the page's per-app tier-1 tick was wrong until controller v0.210.0: it went green on the mere presence of a restore point and turned red only when the box's most recent DB dump — **whichever app it belonged to** — had failed. So an app whose own backup failed could show a tick while this row's operator mail correctly reported the failure; the two halves disagreed, and only the operator half was true. The tick now reads THIS app's own dump result and shows **no icon** when there is no result for it. Reading this row as evidence that the customer's page answers "is ONE app backed up" would have been wrong for five days.
|
||||
|
||||
---
|
||||
|
||||
<a id="c15"></a>
|
||||
## C15 — A local backup is bounded by the box's FREE SPACE, not by a partition set at build time — the appliance ships ONE data volume, and a capture that would exhaust it is refused per app rather than allowed to stop the container runtime
|
||||
|
||||
*Map row: [C15](../00-capability-map.md#c15) · former map line 159.*
|
||||
|
||||
**Scenario (as written):** **A local backup is bounded by the box's FREE SPACE, not by a partition set at build time** — the appliance ships ONE data volume, and a capture that would exhaust it is refused per app rather than allowed to stop the container runtime
|
||||
|
||||
**Components (as written):** golden `build-golden.sh` **v3.0.0**, agent **v0.120.0**, controller **v0.193.1** (R-165 / D-a / B2, completed by R-181)
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-08-03) — BOTH halves**
|
||||
|
||||
**Evidence (as written):** `REPORT.md` (R-178 reinstalls) + `audits/SPIKE-r165-phase0-2026-08-03.md` (P1/P2/P3) + the bake transcript. **The golden bake is real evidence and is cited as such:** `build-golden.sh v3.0.0` produced `including mount point mp0 ('/var/lib/felhom')` with **no `mp1` line at all**, and its own guards printed `/var/lib/docker is a real mount`, `/mnt/sys_drive is a real mount` and `both paths are ONE filesystem`. Archive published (registry HTTP 200, sha `54e2a4c4…`). The B2 floor is unit-proven with 3 red-proofs and live on 9201
|
||||
|
||||
**Gap / roadmap (as written):** **The row's FIRST clause is now PROVEN-LIVE; its SECOND is not, and they are separated deliberately.** **Proven (R-178, 2026-08-03):** *"a local backup is bounded by the box's FREE SPACE, not by a partition set at build time"* — both demo boxes reinstalled from this golden, by two different supply paths (demo-hp `--golden <local volid>`; demo-felhom the normal manifest route with **`verified sha256 54e2a4c431daf580… matches the hub manifest`**), each showing `mp0` at `/var/lib/felhom` with **no `mp1`**, both consumer paths real mounts on ONE filesystem (`stat -c %d` = `64519` on all three), 3/3 reboots each, and claim → deploy → backup → **restore** with a planted marker returning byte-identical. Space available to a recovery unit measured at **65 GiB / 233 GiB**, against the **19 GiB / 45 GiB** those boxes' `mp1` slices offered. **NOT proven — and measured FALSE in part:** *"a capture that would exhaust it is refused per app rather than allowed to stop the container runtime"*. The floor fired live for the first time (demo-hp 06:40:03) and does refuse per app, delete nothing, and alert — **but it is checked only in `captureAllRecoveryUnits`, while `runVolumeDumps` writes the bulk with no floor check at all**, so the leg that exhausts the volume is the unguarded one; and the refusal's claim that the previous unit is untouched was measured false (a 182,272 B dump replaced by 2,147,666,432 B under a manifest still dated 06:34:26). → **R-181, CLOSED THE SAME DAY (controller v0.193.0 + v0.193.1) and the second half is now PROVEN-LIVE TOO.** The reserve became a **per-app, per-run ADMISSION decision** taken before the app's FIRST write and covering all three legs (DB dump, volume dump, capture) — they write under one per-app root, which is what lets one verdict cover them honestly — and it gained a **size term**, so an app is no longer admitted at 96% and then allowed to write 2 GB. **Re-proven by filling demo-hp deliberately, once for EACH term, using the method that found the defect.** *Headroom @ 08:59:46* (906 MB free / 99%): both apps refused, **the whole `backups/primary` tree byte-identical — `TREE_SHA` 111d1760c18d3440f700634ab325f8b8 before and after**, opengist's tar still at its original 182,272 B; **no `Stopping <app> for safe volume dump` line at all**, which is the positive-by-absence observable that matters because that line IS present in the 08:58 baseline run; 0 volume dumps; one alert per app, HTTP 200. Space freed, re-run @ 09:01:33 → both captured normally. *Size @ 09:03:00*, reproducing the original sequence with a real 2 GiB file in opengist's volume (previous tar **2,147,666,432 B**, the exact figure the defect was measured at) and the filesystem at **91% used / 2.9 GB free — both headroom terms deliberately clear**: opengist refused `(size)` while **privatebin was ADMITTED and dumped normally**, proving the term is per-app rather than a global halt. **The refusal's wording was NOT weakened to fit** — the behaviour moved so the wording became true, and it is verified by tree fingerprint rather than by reading the log line, which is what lied. The `fallocate` instrument was re-proven on the rebuilt box before use (5 GiB step moved guest `df` while thin-pool `data_percent` held **36.83 → 36.83**), and teardown returned the pool to **29.43%**, below its own baseline. The golden **is now VOUCHED** (2026-08-03, hub `Artifact manifest set: … golden=0.192.0`), so fresh installs pick up the merged layout. Every box in the field that has not been reinstalled is still on the SPLIT layout and is unaffected: nothing assumes the merged shape at runtime, the controller's system_data_path is a path rather than a volume, and agent v0.120.0 FOLDS the retired `-sysdata-grow` into the single grow so an older `felhom-host-install.sh` still provisions the same total capacity
|
||||
|
||||
---
|
||||
|
||||
<a id="c16"></a>
|
||||
## C16 — Soft-quota: usage bar, pre-push enlargement block, customer notification
|
||||
|
||||
*Map row: [C16](../00-capability-map.md#c16) · former map line 160.*
|
||||
|
||||
**Scenario (as written):** Soft-quota: usage bar, pre-push enlargement block, customer notification
|
||||
|
||||
**Components (as written):** controller v0.109/134, hub v0.41/55
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** 6D/6E; hub OffsiteChecker
|
||||
|
||||
**Gap / roadmap (as written):**
|
||||
|
||||
---
|
||||
|
||||
<a id="c17"></a>
|
||||
## C17 — A customer (not the operator) performs a restore via UI alone
|
||||
|
||||
*Map row: [C17](../00-capability-map.md#c17) · former map line 161.*
|
||||
|
||||
**Scenario (as written):** **A customer (not the operator) performs a restore via UI alone**
|
||||
|
||||
**Components (as written):** all
|
||||
|
||||
**Status (as written):** **MISSING** (as evidence)
|
||||
|
||||
**Evidence (as written):** —
|
||||
|
||||
**Gap / roadmap (as written):** Alpha will produce this; script it into R-3. **2026-07-19:** the C6 evidence attempt ran and found a **product gap instead of evidence** — `audits/DIAG-immich-restore-2026-07-19.md`. A customer-driven UI restore of a DB-indexed app cannot currently succeed (R-43 file-only restore, R-44 stale dump), so this row cannot flip until those close. Row stays MISSING **by finding, not by absence of attempt** — the rehearsal system working, not failing. **2026-07-19: the blocking product gaps are CLOSED in controller v0.148.0** (R-43 + R-44 shipped), so this row is now blocked only on the evidence run itself, not on missing capability. It flips the moment the §9 acceptance produces screenshots + the outcome flash + a snapshot ID. **2026-07-19 round 2 — PARTIAL EVIDENCE ONLY, row NOT flipped** (`audits/DIAG-immich-restore-round2-2026-07-19.md`): a deliberate run from snapshot `49e7cb46` did recover all 11 assets (`status=active`, files resolve), but the operation **reported failure** and left immich reporting schema drift, because the replay aborted against the running app (H4). Photos back ≠ clean acceptance. **2026-07-20: H4 closed in controller v0.153.0 (R-47) on BOTH paths, AND THE EVIDENCE RUN HAPPENED.** *(The "closing in v0.149" wording above was wrong — v0.149.0 was the F3 dashboard fix; R-47 shipped in v0.153.0.)* The C6 drill ran end-to-end **through the UI**: photos deleted, **trash emptied**, the full files+database restore pressed on `/backups/restore`, 40 files placed + 1 DB dump replayed rc-0, 11 assets back, no drift, timeline visually confirmed. The method note below is now DEMONSTRATED, not merely written down. Evidence: `felhom-controller/REPORT.md` 4e. **Residual: the run was performed by the OPERATOR, not by a customer** — for this row literal wording the alpha still owes one genuinely customer-driven pass, but no product gap blocks it. Method note for R-3's script: deleting in an app's own UI usually means *trash*, not deletion, so a drill written that way merges 0 files, flashes success and proves nothing — a real drill must empty the trash **and** verify the app's *content*, not the file count **Lane split → `07-backup-architecture.md` §3**: this row is Lane 1 (customer, unassisted). §8 rows 1–5 are the routes it would exercise
|
||||
|
||||
---
|
||||
@@ -0,0 +1,215 @@
|
||||
# Capability map — detail, section D. Storage & devices
|
||||
|
||||
> **What this file is.** The long text of every row of section D 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="d1"></a>
|
||||
## D1 — A second drive appearing is OFFERED as the backup target; accepting moves it; registration and the drive gate confer no role by themselves
|
||||
|
||||
*Map row: [D1](../00-capability-map.md#d1) · former map line 167.*
|
||||
|
||||
**Scenario (as written):** A second drive appearing is OFFERED as the backup target; accepting moves it; registration and the drive gate confer no role by themselves
|
||||
|
||||
**Components (as written):** controller v0.186.0, agent v0.113
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `SESSION-C-2026-07-29` C4: offer rendered with `data-path`, decline path proven (target stayed `local`, no `felhom-backup` storage, agent.json unchanged), `restart_required:true`, agent did NOT self-restart, wrapper created the storage at the drive's OWN mountpoint
|
||||
|
||||
**Gap / roadmap (as written):** Accept was driven through the endpoint the button POSTs, not a browser click — no browser automation exists on DooPlex
|
||||
|
||||
---
|
||||
|
||||
<a id="d2"></a>
|
||||
## D2 — Drive wizard: scan/format/mount/enroll, incl. legacy-boot LVM-root hosts
|
||||
|
||||
*Map row: [D2](../00-capability-map.md#d2) · former map line 168.*
|
||||
|
||||
**Scenario (as written):** Drive wizard: scan/format/mount/enroll, incl. legacy-boot LVM-root hosts
|
||||
|
||||
**Components (as written):** controller, agent v0.87
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `DISPOSITION-ia-finding2-systemdisks-2026-07-13` (legacy EFI+LVM host, root not offered, byte-identical); enroll/format live in `storage-lifecycle-acceptance-2026-06-15` (E10 re-enroll, data intact); agent fence self-test refuses `/dev/sda`
|
||||
|
||||
**Gap / roadmap (as written):** (Cited `CAMPAIGN-2` T-STG-ENROLL/SEC-FORMAT were auth-hollow CSRF-403.) **Fresh-USB wizard enroll+format through the customer UI PROVEN-LIVE (controller v0.141.0, 2026-07-17):** a 64 GB scratch USB driven through the real `/api/storage/init` endpoints (login+CSRF) → confirm → detached format (~27 s mkfs) → mount → register → mounted+registered at `/mnt/felhom-drives/scratch1`. **F6 (initialize-to-usable) now covered:** the wizard runs the chain as a detached, disconnect-safe, pollable job (3-step progress) with an agent format-status poll for a slow mkfs **2026-07-26 — a SILENT failure class on the channel every agent-backed capability depends on (this row, data migration, USB enrollment, guest RAM, quiesce/PBS) is now DETECTED (controller v0.173.0, R-77). No row status flips.** `controller.yaml` and `bootstrap.json` could disagree on `local_api.endpoint` indefinitely with no signal: the R-50 island migration rewrote the latter, the fleet kept dialling the former, and for 17.5 h the only alert was a generic "agent unreachable" that read as an infrastructure blip. Drift now raises its own event type (`local_api_endpoint_drift`) naming both values. It is DETECTION ONLY — the authority ruling is R-78 — so the class is now loud, not prevented. Evidence: `audits/DIAG-agent-channel-2026-07-26.md`.
|
||||
|
||||
---
|
||||
|
||||
<a id="d3"></a>
|
||||
## D3 — Data migration between drives (all / per-app), crash-safe
|
||||
|
||||
*Map row: [D3](../00-capability-map.md#d3) · former map line 169.*
|
||||
|
||||
**Scenario (as written):** Data migration between drives (all / per-app), crash-safe
|
||||
|
||||
**Components (as written):** controller
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-6C` 4P-5 (scope=app round-trip, byte-identical); `storage-lifecycle-acceptance-2026-06-15` (two migrate-all runs via dashboard UI, sha256 byte-identical)
|
||||
|
||||
**Gap / roadmap (as written):** (Cited `CAMPAIGN-2` T-STG-MIGRATE-* were auth-hollow.) "crash-safe" is design-level (copy→verify→remove) — no clean live crash-during-migration PASS
|
||||
|
||||
---
|
||||
|
||||
<a id="d4"></a>
|
||||
## D4 — NAS (NFS/SMB-client) verify-before-commit, uid-1000 probe, categorized Hungarian errors, DSM-validated
|
||||
|
||||
*Map row: [D4](../00-capability-map.md#d4) · former map line 170.*
|
||||
|
||||
**Scenario (as written):** NAS (NFS/SMB-client) verify-before-commit, uid-1000 probe, categorized Hungarian errors, DSM-validated
|
||||
|
||||
**Components (as written):** controller v0.113–117, agent v0.81/84/85
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `SPIKE-nas-verify-2026-07-11`, `SPIKE-nas-dsm-2026-07-11`, `CAMPAIGN-3-2026-07-11` (boot/reassert fixes)
|
||||
|
||||
**Gap / roadmap (as written):**
|
||||
|
||||
---
|
||||
|
||||
<a id="d5"></a>
|
||||
## D5 — Network storage (NAS) is browse + bulk-media only — it may NOT host an app's data namespace
|
||||
|
||||
*Map row: [D5](../00-capability-map.md#d5) · former map line 171.*
|
||||
|
||||
**Scenario (as written):** **Network storage (NAS) is browse + bulk-media only — it may NOT host an app's data namespace**
|
||||
|
||||
**Components (as written):** controller v0.187.0
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (2026-07-30)
|
||||
|
||||
**Evidence (as written):** `audits/R108-network-app-namespace-2026-07-30.md`. Same-box before/after on demo-felhom through the exact endpoint the UI invokes (`POST /api/storage/migrate-app`, authenticated + CSRF): **pre-fix v0.186.0** the target was never examined — both a NAS-shaped and an unregistered path passed straight into `MigrateApp` and failed only on the app name (409); **post-fix v0.187.0** both are refused 400 with a Hungarian reason, while a real local drive still reaches `MigrateApp` (409) proving the guard is not over-broad. On demo-hp (the box with a REGISTERED share) the network-specific refusal fires. Non-effect verified in the registry: no `migrated_to`, nothing decommissioned, app `HDD_PATH` unchanged, no `backups/` on the share
|
||||
|
||||
**Gap / roadmap (as written):** **Closes R-108 and UNBLOCKS D5** (`07-backup-architecture.md` §7.3, §10.1). The share-root `:rslave` FileBrowser bind is deliberately UNCHANGED — load-bearing for automount wake, and unscopable — verified byte-identical by diffing demo-hp's generated compose before/after. Fails closed: `/mnt/felhom-drives` holds both kinds, so an unregistered path under it is un-classifiable and refused. **Not exercised:** the deploy POST and decommission-migrate refusals are unit-tested (non-effect, nil `stackMgr`) but were NOT live-fired — only migrate-app was. `.fab`-export-onto-NAS remains open (→ R-126)
|
||||
|
||||
---
|
||||
|
||||
<a id="d6"></a>
|
||||
## D6 — A Tier-1/Tier-2 app restore works from the DRIVE ALONE — the recovery unit carries the app's secrets (D5)
|
||||
|
||||
*Map row: [D6](../00-capability-map.md#d6) · former map line 172.*
|
||||
|
||||
**Scenario (as written):** **A Tier-1/Tier-2 app restore works from the DRIVE ALONE — the recovery unit carries the app's secrets (D5)**
|
||||
|
||||
**Components (as written):** controller v0.188.0
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (2026-07-30)
|
||||
|
||||
**Evidence (as written):** `audits/D5-drive-alone-restore-2026-07-30.md`, `07-backup-architecture.md` §7.4. On a scratch drill guest on felhom-pve, through the real endpoints (`POST /api/stacks/{app}/deploy` → `POST /api/backup/run` → `POST /backup/restore`): AdventureLog (`SECRET_KEY` **data_key** + `DB_PASSWORD`) restored with the guest's `app.yaml` **moved aside** → `secrets recovered=2/2`, **27.6 s**, `Restore-from-unit completed`. **The observable is the DATA, not the exit code:** the app itself then read the seeded customer row **over TCP with its own credential** (`connected_as=adventurelog over_TCP=True`), 51 Django tables intact, and the discriminator held — the pre-backup row returned while a row added AFTER the backup was gone, so the volume tar was genuinely restored. The unit held **no `.sql` dump**, so the DB came back from the volume tar, which is exactly the case a regenerated password would have broken. **The withheld half is proven too:** Grafana's `type: password` admin login was live in its container and `ENC:` in the guest, yet appeared in **0 files** anywhere under the backup namespace, and the unit's app.yaml header names it as withheld
|
||||
|
||||
**Gap / roadmap (as written):** **Ruling (operator, 2026-07-30): `type: secret` travels, `type: password` NEVER does, minus the `nonPortableSecrets` code register (`vaultwarden/ADMIN_TOKEN`).** Plaintext on the drive, like the data — defensible **only because** the internet-reachable class is withheld; the two are coupled and must not be relaxed independently. **The brief's own proposal (data_key-only) was tested and rejected:** the flag is unreliable (4 encryption keys the catalog itself labels as such are unflagged → **R-127**) and a DB password is not resettable in practice (`POSTGRES_PASSWORD` is ignored once PGDATA is non-empty). Precedence: **the unit wins** over the guest, because the unit's secrets match the data being restored. The fail-closed data-key gate is UNCHANGED. **Not exercised live:** the withheld-class O4 regeneration on restore (unit-tested only). ~~Tier-2's own cross-drive copy of a secret-bearing unit~~ — **EXERCISED LIVE 2026-08-31 (controller v0.229.0):** docmost restored from `/mnt/felhom-drives/hdd_1/backups/secondary/docmost/recovery-unit` with the guest's `app.yaml` **moved aside** AND the primary unit moved aside, `secrets recovered=2/2` (`APP_SECRET`, `DB_PASSWORD`) taken from the MIRRORED unit's `compose/app.yaml`; the guest's `app.yaml` was rebuilt from it at 0600 and the app then read its own rows over TCP with its own credential. Evidence: `audits/DRILL-r102-tier2-unit-2026-08-31/phase8-scenarioD-guest-appyaml-aside.log`. Venue was a **fixture-class scratch guest**, correct per `runbooks/target-selection.md` since D5's claim is about restore CODE, not the install path
|
||||
|
||||
---
|
||||
|
||||
<a id="d7"></a>
|
||||
## D7 — A restore SAYS what it returned, and refuses what it cannot do — the four restore-surface truth defects from the 2026-08-21 drill
|
||||
|
||||
*Map row: [D7](../00-capability-map.md#d7) · former map line 173.*
|
||||
|
||||
**Scenario (as written):** **A restore SAYS what it returned, and refuses what it cannot do** — the four restore-surface truth defects from the 2026-08-21 drill
|
||||
|
||||
**Components (as written):** controller **v0.226.0** (R-353, R-357, R-358, R-360, R-396)
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-08-30) for three of the four; R-357 is IMPLEMENTED only**
|
||||
|
||||
**Evidence (as written):** `audits/evidence-r353-r360-live-2026-08-30/live-validation.txt`, controller `CHANGELOG.md` v0.226.0 + `REPORT.md`. Driven on `demo-hp` through the endpoints the UI invokes (no browser on DooPlex; the residual is client-side rendering). **R-353:** the sentence read off the customer's own wizard page — `A(z) opengist: 1 adatkötet visszaállítva — az alkalmazás újraindult.` with real counts (1 volume of 1 listed, 0 databases of 0 listed, and correctly no database clause). **R-360:** in the exact flag state that produced the bug (display flag true, concurrency flag false) the delete was refused and **a planted canary file survived**. **R-358/R-396:** a `mode=unit` restore wrote `{"schema":1,…,"full":false}` at mode 0600 with no `.tmp` left, and the gate logged `scratch holds a UNIT-ONLY restore … place-to-live stays closed`
|
||||
|
||||
**Gap / roadmap (as written):** **WHAT IS AND IS NOT CLAIMED, split deliberately.** **R-357 (the destructive restore's free-space gate) is IMPLEMENTED, NOT PROVEN-LIVE** — filling a real filesystem is a drill step, not a build step, so it rests on seam tests (`SetOffboxFreeFn`, `SetOffboxSizer`, and the new `SetOffboxLatestSnapshotFn`) whose central assertion is that `StopStack` was never called. **R-353's Scenario B — the "backup held only settings" sentence — was NOT reproduced live either**, and the reason is stated rather than glossed: no app on `demo-hp` still has a data-less unit (the drill's opengist has been recaptured and now lists one volume dump), and falsifying a manifest to produce it is the hand-set-state shortcut this project forbids. That branch is unit-proven only. **This row is about the MESSAGE and the REFUSALS, not the recovery mechanism** — `07-backup-architecture.md` §8 row 3 keeps its PROVEN status because the restore always did return what the unit held; what it could not do was say so
|
||||
|
||||
---
|
||||
|
||||
<a id="d8"></a>
|
||||
## D8 — The box PROVES its own off-site copy still HOLDS something — a backup that is intact and EMPTY is caught without a person
|
||||
|
||||
*Map row: [D8](../00-capability-map.md#d8) · former map line 174.*
|
||||
|
||||
**Scenario (as written):** **The box PROVES its own off-site copy still HOLDS something — a backup that is intact and EMPTY is caught without a person**
|
||||
|
||||
**Components (as written):** controller **v0.231.0** (R-87) + hub **v0.110.0**
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-08-31) for the judgement, the alarm, the cleanup, the rotation, the read-only guarantee and the skip-if-busy hazard control; the SCHEDULED FIRING is IMPLEMENTED only**
|
||||
|
||||
**Evidence (as written):** `documentation/tests/r87-offsite-proof-2026-08-31/`. Driven on `demo-hp` through the endpoint the debug button invokes. **The failing case was produced and caught:** a hollow unit — compose declaring `opengist_data`, manifest declaring nothing — was pushed to the live store, and the proof returned `verdict:"fail"` with `volumes_expected_none_captured: opengist_data`, emitted **exactly one** `offsite_proof_empty` at severity `error`, accepted by the hub **HTTP 200** (which is itself the proof the allowlist entry landed — an unallowlisted type is 400'd and vanishes), and deleted its scratch. **The passing case was proven five times over** (bookstack, calibre-web, docmost, kimai, opengist), 2.2–4.0 s each, and the customer's own verification copies were untouched throughout. **The read-only guarantee was measured with a positively-controlled lock sampler** — it saw a lock appear and vanish across a real `restic check`, and **zero** across the proof, including a direct 6× test of the snapshot-lookup argv. **The skip-if-busy control fired live and unplanned:** a proof launched while the off-site backup run held the flag returned `skipped:true, duration_ms:0` with no verdict and no alarm
|
||||
|
||||
**Gap / roadmap (as written):** **⚠ WHAT A PASS MEANS, AND WHAT IT DOES NOT.** It means the newest off-site snapshot of ONE app contains what that app is supposed to have — judged from the unit's own captured compose, not from the live box. **It does NOT mean a restore puts data back into a running app**: the proof restores to a throwaway folder, looks, and deletes, and `07` §8 matrix row 4 is deliberately NOT moved. **It also does not vouch for the BYTES** — nothing available can: restic 0.14.0's `restore --verify` passed a byte-level corruption with size and mtime preserved (measured, 131 ms on a 213 MB tree), and the unit manifest hashes 4 918 B of a 213 231 242 B unit (R-409). **2026-09-01 (R-414, controller v0.232.0): it can now run on a box with NO registered data drive.** The first unattended firing, on `demo-felhom`, REFUSED — *"nowhere to restore to"* — because that box has `storage_paths: []` and the scratch resolver never consulted the system data path. A **unit-only** restore now falls back there (where a driveless app's unit already lives, `07` §7); a **full** restore still refuses, because the SSD is a state-only tier. And a proof that cannot start now records `cannot_run` instead of nothing, so `last_proof_result` is never ABSENT — absent already means *a controller too old to have the feature*. **PROVEN LIVE on `demo-felhom` 2026-09-01:** `verdict:"pass"` on `61e9cf30` in 2.117 s, recorded, and the scratch deleted. **The nightly firing IS now proven** — it ran unattended on `demo-hp` at 05:30 on 2026-09-01 (`bentopdf PASSED on 9d002b38 in 2.315s`), which this row previously listed as implemented-only. The job is confirmed REGISTERED on `demo-hp` (`Daily job offsite-proof scheduled for 2026-09-01 05:30 CEST`), which is not the same claim, and the fleet is on 0.230.0 until a golden carries 0.231.0
|
||||
|
||||
---
|
||||
|
||||
<a id="d9"></a>
|
||||
## D9 — The off-site store is VERIFIED on a cadence — something checks that the customer's backups are still readable
|
||||
|
||||
*Map row: [D9](../00-capability-map.md#d9) · former map line 175.*
|
||||
|
||||
**Scenario (as written):** **The off-site store is VERIFIED on a cadence — something checks that the customer's backups are still readable**
|
||||
|
||||
**Components (as written):** controller **v0.228.0** (R-359, R-397, R-399)
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-08-30, re-proven at FULL DEPTH 2026-08-31) for the check, the notifier and the hazard control; the SCHEDULED FIRING is IMPLEMENTED only**
|
||||
|
||||
**Evidence (as written):** `documentation/tests/r359-integrity-2026-08-30/`. Driven on `demo-hp` through the endpoint the debug button invokes. A throwaway repo was built, checked healthy (**negative control first**), then one pack corrupted; the live store was checked read-only in **35.0 s**; and the notifier fired end to end — `Event pushed: backup_integrity_ok (info)`. The hazard control was observed live: a second check fired while the first held the single-writer flag returned `skipped:true, duration_ms:0` — **it never ran restic at all**
|
||||
|
||||
**Gap / roadmap (as written):** **⚠ WHAT AN `ok` MEANS — CHANGED 2026-08-31 (R-399, controller v0.228.0): the check now RE-READS THE DATA.** The default is `--read-data-subset=100%`, so an `ok` means every stored byte was downloaded and re-hashed, not merely that the catalogue hangs together. **The reason is measured, and it is why the default must not be turned back down to save four seconds:** a pack corrupted WITHOUT a size change made a structure check return `no errors were found`, exit 0, while every `--read-data*` form caught it. Cost curve on 134.3 MB: structure 35.0 s · 10% 35.9 s · 50% 37.3 s · 100% 39.2 s — **and those do NOT extrapolate**, which is why v0.228.0 ships a slow-check WARN (R-401) rather than a rotation schedule. `off` returns a box to structure depth. **PROVEN-LIVE at the new depth 2026-08-31 on `demo-hp`**, endpoint-level, with the restic argv observed from the guest: default → `… check --read-data-subset=100%`, 38.7 s; `off` → `… check`, 34.7 s. **The weekly firing at the new depth is IMPLEMENTED only** — the job is confirmed REGISTERED on BOTH demo boxes (`Daily job offsite-integrity scheduled for 2026-09-01 06:00 CEST`), which is not the same claim. `demo-felhom` reached 0.228.0 by SELF-UPDATE on the 2026-08-31 floor raise and re-registered the job itself, so the depth change is on the fleet and not only on the box that was deployed to by hand. **This is a readability check and NOT a restore-test** — R-87 remains open and the two are routinely conflated because their register rows are adjacent
|
||||
|
||||
---
|
||||
|
||||
<a id="d10"></a>
|
||||
## D10 — USB drive enrollment + unplug detection + recommission
|
||||
|
||||
*Map row: [D10](../00-capability-map.md#d10) · former map line 176.*
|
||||
|
||||
**Scenario (as written):** USB drive enrollment + unplug detection + recommission
|
||||
|
||||
**Components (as written):** controller, agent
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `storage-lifecycle-acceptance-2026-06-15` E4 (yanked-while-running → agent auto-rebind) + E10 (re-enroll, data intact); `CAMPAIGN-4`/`6A` (3 USB re-establish across device-letter reshuffle)
|
||||
|
||||
**Gap / roadmap (as written):** (Cited `RUNBOOK-usb` could NOT complete a wizard enrollment; `CAMPAIGN-2` legs were auth-hollow.) Fresh-USB **wizard enrollment** specifically still unproven
|
||||
|
||||
---
|
||||
|
||||
<a id="d11"></a>
|
||||
## D11 — Decommission (migrate-first and anyway-paths), eject
|
||||
|
||||
*Map row: [D11](../00-capability-map.md#d11) · former map line 177.*
|
||||
|
||||
**Scenario (as written):** Decommission (migrate-first and anyway-paths), eject
|
||||
|
||||
**Components (as written):** agent, controller
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `storage-lifecycle-acceptance-2026-06-15` E9 (decommission-anyway → bind detached, parent mp untouched, reboot-safe) + E12 (eject drive holding all apps)
|
||||
|
||||
**Gap / roadmap (as written):** (Cited `CAMPAIGN-2` T-STG-DECOM-* were auth-hollow; `SPIKE-decommission` was report-only, button still vestigial.)
|
||||
|
||||
---
|
||||
|
||||
<a id="d12"></a>
|
||||
## D12 — Boot ordering: automount + networking survive reboot; appliance self-heal watchdog
|
||||
|
||||
*Map row: [D12](../00-capability-map.md#d12) · former map line 178.*
|
||||
|
||||
**Scenario (as written):** Boot ordering: automount + networking survive reboot; appliance self-heal watchdog
|
||||
|
||||
**Components (as written):** agent v0.85
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `CAMPAIGN-4-2026-07-13` (F12 fix HOLDS: demo-host reboot + 5-boot storm, 0 ordering cycles, caps 63/63, WG re-handshake) + `CAMPAIGN-6A-2026-07-14` 1D (re-arm reboot-survival across 9 guest + 1 host reboots)
|
||||
|
||||
**Gap / roadmap (as written):** (`CAMPAIGN-3` F10/F11/F12 were the CRITICAL/HIGH *failures*; fixes shipped in agent v0.85 and were re-validated live in 4/6A — cite the validation, not the finding.) Residual: `skip-active` on `pct reboot` carried by the heal path; a NAS outage spanning a guest reboot can strand the share until agent restart (6A)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,364 @@
|
||||
# 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
|
||||
|
||||
---
|
||||
@@ -0,0 +1,179 @@
|
||||
# Capability map — detail, section F. Notifications & monitoring
|
||||
|
||||
> **What this file is.** The long text of every row of section F 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="f1"></a>
|
||||
## F1 — An ABSENT backup-target drive raises its OWN alarm, paired with a matching recovery
|
||||
|
||||
*Map row: [F1](../00-capability-map.md#f1) · former map line 210.*
|
||||
|
||||
**Scenario (as written):** An ABSENT backup-target drive raises its OWN alarm, paired with a matching recovery
|
||||
|
||||
**Components (as written):** agent **v0.116.0**, controller v0.184.1+, hub v0.81.0
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (2026-07-30)
|
||||
|
||||
**Evidence (as written):** `audits/R116-v0116-2026-07-30.md`. On a fresh box built through the **real day-0** on demo-hp, running the agent it installed **unaided from the vouched Day-0 manifest** (0.116.0), both drives enrolled through the real endpoints and device loss a real hot-detach — **the full four-event sequence, two matched pairs, correctly discriminated:** `07:20:04 backup_target_absent (error)` / `07:22:34 backup_target_restored (info)` for the TARGET, and `07:24:04 storage_disconnected (error)` / `07:25:34 storage_reconnected (info)` for a NON-target drive on the same box minutes apart. Gate fired in **3 s**. All four reached the hub — specific alarm, severity, Hungarian copy and hub routing now exercised end-to-end. **Discrimination is proven NON-trivially for the first time**: both prior runs had the target itself emit the generic event, so the mirror proved nothing. **Over-correction guard PASSES** with a positive observable — 0 ABSENT lines and 0 drive events over a 2m14s window with both drives present, target `degraded:false`, while 2 `RETURNED` lines prove the gate was ticking.
|
||||
|
||||
**Gap / roadmap (as written):** **The mechanism was isolated from the captured payload first** (`DIAG-r116-disks-payload-2026-07-30.md`), after two fixes aimed at shapes that do not occur. v0.116.0 joins the two records of one drive on the only identity that survives the device — the CONFIGURED path — so one row carries both the flag and the guest path the gate keys on. **Both smaller-looking fixes were rejected because they regress R-114** (`backup_target_offer.go:79` reads flag+mount_path as *healthy*). **Caveat worth reading: the drill box ran controller 0.185.1 from the golden, which PREDATES R-114** — so its absent-state banner showed the old false "backup is on the system disk" copy. That is the golden being a release behind, not a regression → **R-120**
|
||||
|
||||
---
|
||||
|
||||
<a id="f2"></a>
|
||||
## F2 — Health-degradation email (edge-triggered, cooldowns, Hungarian) via hub → Resend
|
||||
|
||||
*Map row: [F2](../00-capability-map.md#f2) · former map line 211.*
|
||||
|
||||
**Scenario (as written):** Health-degradation email (edge-triggered, cooldowns, Hungarian) via hub → Resend
|
||||
|
||||
**Components (as written):** controller, hub
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** delivery pipeline live-proven for the **enlarge-block** trigger (`CAMPAIGN-6D` P3-DELIVERY, op+customer "Kedves Ügyfél!"); `NotifyHealthChange` ok→warn/fail edge-trigger implemented
|
||||
|
||||
**Gap / roadmap (as written):** The **health-degradation** trigger specifically has never fired an email live in any doc. Demoted (pipeline proven for a different event). Deliverability to HU freemail → R-4
|
||||
|
||||
---
|
||||
|
||||
<a id="f3"></a>
|
||||
## F3 — Event catalog: app_start_failed, dead-app, offbox_enlarge_blocked, claim/reset codes, critical severity
|
||||
|
||||
*Map row: [F3](../00-capability-map.md#f3) · former map line 212.*
|
||||
|
||||
**Scenario (as written):** Event catalog: app_start_failed, dead-app, offbox_enlarge_blocked, claim/reset codes, critical severity
|
||||
|
||||
**Components (as written):** controller, hub v0.31/48/50/55
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** live-delivered: `CAMPAIGN-6D` P3-DELIVERY (enlarge-block, op+customer); `DRILL-day0-vm` F-4 (claim code); `DRILL-day0-take2` F-15 (reset code)
|
||||
|
||||
**Gap / roadmap (as written):** `app_start_failed`/`dead-app` delivery is unit-only (6C inconclusive) — the pipeline + 3 event families are live, those two are not
|
||||
|
||||
---
|
||||
|
||||
<a id="f4"></a>
|
||||
## F4 — Prefs safety: empty-email wipe guard
|
||||
|
||||
*Map row: [F4](../00-capability-map.md#f4) · former map line 213.*
|
||||
|
||||
**Scenario (as written):** Prefs safety: empty-email wipe guard
|
||||
|
||||
**Components (as written):** controller v0.137 + hub v0.71.0
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** controller leg red-proofed 07-15; hub-side no-clobber belt (`handleSavePreferences` preserves a stored non-empty address on an empty-email push) red-proofed 07-22
|
||||
|
||||
**Gap / roadmap (as written):** Born from a live incident; controller 0.160.0 guards both its push legs, so the hub belt covers older/rogue boxes
|
||||
|
||||
---
|
||||
|
||||
<a id="f5"></a>
|
||||
## F5 — Paired recovery notifications + prefs seeding at claim + priority headers (power-outage audit F11/F12/F14-light)
|
||||
|
||||
*Map row: [F5](../00-capability-map.md#f5) · former map line 214.*
|
||||
|
||||
**Scenario (as written):** Paired recovery notifications + prefs seeding at claim + priority headers (power-outage audit F11/F12/F14-light)
|
||||
|
||||
**Components (as written):** hub v0.71.0
|
||||
|
||||
**Status (as written):** **IMPLEMENTED** (recovery leg **PARTIAL** until a live staleness cycle fires it)
|
||||
|
||||
**Evidence (as written):** `hub/CHANGELOG.md` v0.71.0; 17 tests + 4 red-proofs (`REPORT.md` 2026-07-22); Resend `headers` mechanism probed live (HTTP 200) pre-implementation; operator+customer `test` rows live-fired via the controller's own test endpoint
|
||||
|
||||
**Gap / roadmap (as written):** Recovery = explicit eventType branch, severity semantics frozen; customer gate = PAIRING (`notification_log` evidence), not `enabled_events`. Live legs pending: a natural `*_recovered` mail (next real staleness cycle or the reboot-drill arc — never fabricated by blocking reports) and seed-at-claim on a real claim (Peti Friday reinstall). F14-full (operator push channel, ntfy/Telegram) stays open → R-69
|
||||
|
||||
---
|
||||
|
||||
<a id="f6"></a>
|
||||
## F6 — System + container metrics (SQLite, Chart.js, 30-day downsampling)
|
||||
|
||||
*Map row: [F6](../00-capability-map.md#f6) · former map line 215.*
|
||||
|
||||
**Scenario (as written):** System + container metrics (SQLite, Chart.js, 30-day downsampling)
|
||||
|
||||
**Components (as written):** controller
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** metrics collection + `/monitoring` render present (page 200)
|
||||
|
||||
**Gap / roadmap (as written):** The cited `CAMPAIGN-2` T-RES-CPU/T-SOAK-LOOP are H1/H2 harness artifacts (auth-302), not metrics tests; SQLite/Chart.js/30-day downsampling validated in no campaign. Demoted
|
||||
|
||||
---
|
||||
|
||||
<a id="f7"></a>
|
||||
## F7 — Always-on debug rings + on-demand log-bundle pulls with TTL/custody
|
||||
|
||||
*Map row: [F7](../00-capability-map.md#f7) · former map line 216.*
|
||||
|
||||
**Scenario (as written):** Always-on debug rings + on-demand log-bundle pulls with TTL/custody
|
||||
|
||||
**Components (as written):** controller v0.116, agent v0.83, hub v0.46
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** debug rings live-exercised `CAMPAIGN-3` fix-6 (1000-cap ring, ~55min horizon under load)
|
||||
|
||||
**Gap / roadmap (as written):** The **log-bundle-pull TTL/custody** half is changelog-only (no dedicated observability audit doc); ring persistence across restart is a known gap
|
||||
|
||||
---
|
||||
|
||||
<a id="f8"></a>
|
||||
## F8 — Operator alerting (Healthchecks → monitoring@felhom.eu)
|
||||
|
||||
*Map row: [F8](../00-capability-map.md#f8) · former map line 217.*
|
||||
|
||||
**Scenario (as written):** Operator alerting (Healthchecks → monitoring@felhom.eu)
|
||||
|
||||
**Components (as written):** k3s, Resend
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** operator infra, stated in production since 02-04; no corpus validation doc
|
||||
|
||||
**Gap / roadmap (as written):** *(the row had no fifth cell)*
|
||||
|
||||
---
|
||||
|
||||
<a id="f9"></a>
|
||||
## F9 — Backup-deadline alerting (`expected_backup_missed`) is ANCHORED — absence of signal is UNKNOWN, not failure
|
||||
|
||||
*Map row: [F9](../00-capability-map.md#f9) · former map line 218.*
|
||||
|
||||
**Scenario (as written):** Backup-deadline alerting (`expected_backup_missed`) is ANCHORED — absence of signal is UNKNOWN, not failure
|
||||
|
||||
**Components (as written):** hub v0.75.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):** `audits/DIAG-backup-missed-2026-07-26.md` + red-proofs A/B/C + replay of the real 2026-07-26 03:00 reports (all three silent) | **No row status flips** — this signal had a FALSE-POSITIVE class (three instances: hub v0.12.0, v0.73.0, R-81), now anchored at first contact and read across retained host-report history. Still unit-proven only, not live-fired at a real deadline. The *underlying* PBS/offsite-DR tier gap it exposed is → R-82. | Per the status enum, no citation → not PROVEN-LIVE. Demoted pending an operator-cited live alert (candidate re-upgrade — see REPORT)
|
||||
|
||||
---
|
||||
|
||||
<a id="f10"></a>
|
||||
## F10 — A box that is not always on (off at its backup time) makes the missed night up once when it comes back; the household sees a banner; the operator gets the missed-backup alarm, down or not
|
||||
|
||||
*Map row: [F10](../00-capability-map.md#f10) · former map line 219.*
|
||||
|
||||
**Scenario (as written):** A box that is not always on (off at its backup time) makes the missed night up once when it comes back; the household sees a banner; the operator gets the missed-backup alarm, down or not
|
||||
|
||||
**Components (as written):** controller v0.295.0, hub v0.134.0, agent v0.145.0
|
||||
|
||||
**Status (as written):** **PARTIAL — the catch-up and the banner PROVEN-LIVE (9202 + demo-felhom; banner on 9202 by a hand-set ledger); R-872's alarm proven by tests, live at the next 05:00 (dated check); a host SUSPEND reasoned + unit-tested, not measured**
|
||||
|
||||
**Evidence (as written):** `audits/catchup-2026-10-05/partA/`, `partB/`; design `07` §6.1.1
|
||||
|
||||
**Gap / roadmap (as written):** R-872 live; a suspend never measured
|
||||
|
||||
---
|
||||
@@ -0,0 +1,349 @@
|
||||
# Capability map — detail, section G. Fleet & operator (hub)
|
||||
|
||||
> **What this file is.** The long text of every row of section G 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="g1"></a>
|
||||
## G1 — Customer/host management: 8-tab detail, scoped auto-refresh, safe stale-host deletion, capability chips
|
||||
|
||||
*Map row: [G1](../00-capability-map.md#g1) · former map line 225.*
|
||||
|
||||
**Scenario (as written):** Customer/host management: 8-tab detail, scoped auto-refresh, safe stale-host deletion, capability chips
|
||||
|
||||
**Components (as written):** hub v0.47–0.53
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** hub v0.53.0 dead-host roll-up live on the Peti cluster (proxmox1 down 23h); `CAMPAIGN-4-2026-07-13` (operator UI driven live); `DRILL-day0-take2` F-16 (offsite/freeze buttons live)
|
||||
|
||||
**Gap / roadmap (as written):** 8-tab render + capability chips are **render-test-validated** (hub UI is password-gated; CC cannot log in). (Cited "daily operator use" was a no-doc citation; `AUDIT-hub-gui-2026-06-30` predates these features at hub v0.25)
|
||||
|
||||
---
|
||||
|
||||
<a id="g2"></a>
|
||||
## G2 — Config/state change round-trips in seconds (hub↔box immediacy; 15-min cycle stays the backbone): box→hub out-of-cycle report (Dir 1) + hub→box `GET /api/v1/wait` long-poll wake (Dir 2)
|
||||
|
||||
*Map row: [G2](../00-capability-map.md#g2) · former map line 226.*
|
||||
|
||||
**Scenario (as written):** Config/state change round-trips in **seconds** (hub↔box immediacy; 15-min cycle stays the backbone): box→hub out-of-cycle report (Dir 1) + hub→box `GET /api/v1/wait` long-poll wake (Dir 2)
|
||||
|
||||
**Components (as written):** controller v0.139/140, hub v0.58/0.63
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (2026-07-21)
|
||||
|
||||
**Evidence (as written):** Transport proven live through the real DNS-only ingress: `SPIKE-immediate-sync-transport-2026-07-16` + hub v0.58.0 / controller v0.140.0 REPORTs — 240 s no-annotation hold (25 s heartbeat defeats nginx's 60 s `proxy_read_timeout`, no ingress change), 0.047 s wake-on-change, hub `rollout restart` = 1 WARN + 0-storm reconnect; Dir-1 2 s box→hub round-trip live in controller v0.139.0
|
||||
|
||||
**Gap / roadmap (as written):** The operator-UI **save→apply** round-trip is not fired end-to-end live (hub UI password-gated; CC can't log in) → R-23; the wake transport and the ACK→config_version→`ConfigRefresher` delivery chain are each proven, only the UI-triggered bump leg is unexercised. Agent-plane (host-domain desired-state) poke **first slice PROVEN-LIVE** (Direction-2a, agent v0.89.0 + hub v0.59.0, 2026-07-17): contentless ep0-relayed UDP poke → agent immediate desired-state cycle, per `SPIKE-immediate-sync-transport-2026-07-16` P4. Full path live-proven: a real operator manifest save fired `poke: sync-poke delivered to 10.77.0.2`; the box (0.89.0) received it and logged `poke received → triggering an immediate desired-state cycle` → `out-of-band report triggered` — **~31 ms ep0→box, sub-ms to the report cycle** (WG-confined, from 10.77.0.1 to the 10.77.0.2-bound socket); save→tick ≈ ~0.45 s (SSH-dominated), well under ≤2–3 s. R-13 first slice (listener+sender only; the rest of the mutual-repair arc stays open). **System-initiated immediacy wired (hub v0.63.0, this REPORT):** the mutation sites that only OPERATOR actions used to notify now fire the correct plane's notifier when the hub itself mints state — agent-plane pokes at `PBSDRAutoProvision` (the observed slice-C lag), `ReissuePBSDR` (also the pbsdrheal escalation), `handlePBSDRReissue`, and the two admin desired-state api writers; controller-plane bump at `reissueOnReenroll`. Unit-tested + red-proofed, not yet fired on a real system event (folds into the rehearsal bind sequence). Still PARTIAL: the R-23 operator-UI save→apply leg and the agent **fast-tick-until-first-convergence** SECONDARY (the WG-registration leg a poke can't reach pre-tunnel) remain unfired live. **Fast-tick SHIPPED (agent v0.90.0, R-28):** while any desired-state item is unapplied — incl. the pre-tunnel window a poke can't reach — the agent pulses the out-of-band trigger every 30 s and self-disarms on convergence (state-based; four cached sources; LOUD states excluded). LIVE on both demo agents (the `fast-tick armed: 30s …` startup line verified). **REAL-ONBOARDING PROOF DONE — `tests/VALIDATION-n100-rehearsal-2026-07-18.md` (ledger 8, S5):** on a genuine first onboarding on metal, **every post-bind leg landed seconds apart with no ~15-minute stall anywhere** — bind 16:29:55 → credential delivered 16:30:21 (26 s) → agent 0.90.0 up 16:30:49 → **WG registered + tunnel applied 16:30:51 (~2 s)** → poke listener 16:30:54 → controller 16:32:28 → floor-lifted and running current 16:32:39. **Bind → running-current = 2 min 44 s.** The PBS-DR descriptor auto-provisioned on the same cadence (agent `converged state=applied` 16:45:53) — though see the DR-tier row: the descriptor converged while the credential behind it was already stale (R-39). The pre-tunnel fast-tick window is therefore proven in its real setting; the remaining PARTIAL is the R-23 operator-UI save→apply leg alone **2026-07-21 — THE LAST PARTIAL LEG IS CLOSED (R-23(a) restart leg).** The operator saved the global floor to a version the box did NOT run (0.153.0 → **v0.154.0**) and the managed self-update fired **exactly once**: `06:57:13Z` UpdateState pending (`initiated_by=auto-floor`) → `06:57:17Z` agent `controller swap requested` → `06:57:21Z` container restarted → `06:57:29Z` `new controller healthy`. **Save → healthy on the new version = 16 s.** Over a 39-minute window: swap requests **1**, agent-driven bootstrap restarts **1**, rollbacks **0**, container `RestartCount` **0**; `VerifyStartup` confirmed on the next boot and the following periodic check logged `Current version 0.154.0 is up to date` (the at/above-floor branch doing nothing, as designed). The 2026-07-20 attempt could not prove this because it targeted an already-running version. Evidence: `felhom-controller/REPORT.md` §6.
|
||||
|
||||
---
|
||||
|
||||
<a id="g3"></a>
|
||||
## G3 — Customer right-sizes guest RAM from the controller (agent-enforced bounds, live cgroup apply, no reboot)
|
||||
|
||||
*Map row: [G3](../00-capability-map.md#g3) · former map line 227.*
|
||||
|
||||
**Scenario (as written):** Customer right-sizes guest RAM from the controller (agent-enforced bounds, live cgroup apply, no reboot)
|
||||
|
||||
**Components (as written):** agent v0.90.0 + controller v0.143.0 (R-24)
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (grow **and** shrink on metal, 2026-07-18)
|
||||
|
||||
**Evidence (as written):** **`tests/VALIDATION-n100-rehearsal-2026-07-18.md` (ledger 9) — the apply is now proven in both directions on a normal-sized box:** customer zero **shrank 11675 → 8192 MB at 16:50:22** and **grew 8192 → 12288 MB at 17:02:17**, each a live cgroup apply with **no reboot** (`local-api: guest-memory resized` in the agent journal, `[web] memory resized` in the controller log), and the new total rippled into the deploy page's memory math at 17:05:15 (`total=12288MB`). F5 auto-sizing had landed the guest at 11675 MB. Controller-direct (R-24's hub-desired-state framing SUPERSEDED, Viktor 2026-07-17). Agent `GET`/`POST /guest/memory` enforces every bound FRESH (min 2048 / max host_total−2048 / shrink floor max(2048, usage+512)) + verify-after-apply; PVE `SetConfig` hot-applies (**Phase-0 PROVEN** on the nested box: maxmem moves with the guest running, /proc/meminfo ripples via lxcfs, no reboot). Controller "Szerver memória (RAM)" card + code→Hungarian map, gated on `FeatureGuestMemoryResize` (MinAgent 0.90.0). LIVE-validated end-to-end through the real endpoint on the demo (above_max + below_min refusals render the Hungarian, agent English never leaks; SupportYes via the version header)
|
||||
|
||||
**Gap / roadmap (as written):** Row complete as of the 2026-07-18 rehearsal — the refusals were proven on the nested demo, the **applies** on the N100. Cores stay observation
|
||||
|
||||
---
|
||||
|
||||
<a id="g4"></a>
|
||||
## G4 — Publish train: MinAgent floors, gated auto-Reissue, version channels, floor-field-LAST rules
|
||||
|
||||
*Map row: [G4](../00-capability-map.md#g4) · former map line 228.*
|
||||
|
||||
**Scenario (as written):** Publish train: MinAgent floors, gated auto-Reissue, version channels, floor-field-LAST rules
|
||||
|
||||
**Components (as written):** hub v0.45/0.53, agent
|
||||
|
||||
**Status (as written):** **PARTIAL**
|
||||
|
||||
**Evidence and gap (as written — this row's text carries a `|` outside code, so its last cells are kept together as they stood):** `runbooks/publish-train-rules.md`; demo-fleet updates proven | **Box-side floor lift PROVEN-LIVE on a fresh install** (`tests/VALIDATION-n100-rehearsal-2026-07-18.md`): the day-0 golden deployed controller **0.143.0** at 16:32:28 and the managed floor lifted it to **0.145.0 by 16:32:34 — a 5-second, fully unattended update inside the first minute of controller life**, `update-state.json` recording `initiated_by: auto-floor` with `controller_updated` pushed to the hub. So the *mechanism* is no longer nested-only. **Still never proven on a real REMOTE customer** — parked trains `RUNBOOK-publish-0.79/0.81/0.85-*` await Peti → R-1. **Action before first invite: rebuild the golden to 0.145.x** now that this evidence is banked, so fresh boxes don't sit two versions stale | **Fresh-box floor lift proven 2026-07-21 (demo-hp):** a brand-new box came up on golden **0.153.0** and self-updated to the fleet floor **0.156.0** during day-0, unattended — the floor mechanism works on first contact, not only on boxes that have been in the fleet a while. That is the R-23(a) single-swap behaviour observed on a machine with no history at all.
|
||||
|
||||
---
|
||||
|
||||
<a id="g5"></a>
|
||||
## G5 — Agent self-update: A/B slots, crash-loop auto-rollback, operator-signed
|
||||
|
||||
*Map row: [G5](../00-capability-map.md#g5) · former map line 229.*
|
||||
|
||||
**Scenario (as written):** Agent self-update: A/B slots, crash-loop auto-rollback, operator-signed
|
||||
|
||||
**Components (as written):** agent v0.70+
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (demo)
|
||||
|
||||
**Evidence (as written):** `SPIKE-agent-selfupdate-2026-07-05`
|
||||
|
||||
**Gap / roadmap (as written):** Remote-customer proof pending → R-1
|
||||
|
||||
---
|
||||
|
||||
<a id="g6"></a>
|
||||
## G6 — Controller self-update: anonymous registry, no credentials in guest
|
||||
|
||||
*Map row: [G6](../00-capability-map.md#g6) · former map line 230.*
|
||||
|
||||
**Scenario (as written):** Controller self-update: anonymous registry, no credentials in guest
|
||||
|
||||
**Components (as written):** controller v0.112
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE** (demo)
|
||||
|
||||
**Evidence (as written):** 07-10 arc
|
||||
|
||||
**Gap / roadmap (as written):**
|
||||
|
||||
---
|
||||
|
||||
<a id="g7"></a>
|
||||
## G7 — Offsite provisioning: Hetzner API, sub-account per customer, host-key pinning, credential re-issue
|
||||
|
||||
*Map row: [G7](../00-capability-map.md#g7) · former map line 231.*
|
||||
|
||||
**Scenario (as written):** Offsite provisioning: Hetzner API, sub-account per customer, host-key pinning, credential re-issue
|
||||
|
||||
**Components (as written):** hub v0.37–0.39
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** `VALIDATION-offsite-provisioning-e2e-2026-07-09`, `SPIKE-hetzner-api-provisioning-2026-07-09`
|
||||
|
||||
**Gap / roadmap (as written):**
|
||||
|
||||
---
|
||||
|
||||
<a id="g8"></a>
|
||||
## G8 — Per-customer offsite fill + staleness + freeze lever
|
||||
|
||||
*Map row: [G8](../00-capability-map.md#g8) · former map line 232.*
|
||||
|
||||
**Scenario (as written):** Per-customer offsite fill + staleness + freeze lever
|
||||
|
||||
**Components (as written):** hub v0.41
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** `OffsiteChecker` (`hub/internal/monitor/offsite.go`): fill 90/95% vs soft quota, staleness >48h
|
||||
|
||||
**Gap / roadmap (as written):** No live-fired leg: `CAMPAIGN-offsite-overnight-2026-07-10` recorded no quota/fill/staleness emails, and the freeze write-block was **inconclusive** (only the Hetzner `readonly:true` API op succeeded). Demoted
|
||||
|
||||
---
|
||||
|
||||
<a id="g9"></a>
|
||||
## G9 — Box-level Storage Box aggregate (total fill, Σ quotas, oversubscription alert)
|
||||
|
||||
*Map row: [G9](../00-capability-map.md#g9) · former map line 233.*
|
||||
|
||||
**Scenario (as written):** **Box-level Storage Box aggregate (total fill, Σ quotas, oversubscription alert)**
|
||||
|
||||
**Components (as written):** hub v0.64.0 (R-5)
|
||||
|
||||
**Status (as written):** **IMPLEMENTED** (data pipeline PROVEN-LIVE)
|
||||
|
||||
**Evidence (as written):** `monitor.OffsiteBoxChecker` — fetch-throttled Hetzner GET (1/15 min), fill (used/`storage_box_type.size`, 80/90%) + oversubscription (Σ shared+enabled ConfigJSON quotas / capacity, 2.0×), escalation-only operator alert on the customer-less `"pool-box"` scope; Offsite-tab panel + dashboard tile. **Phase-0-pinned** live shape (box 611714) + **live-computed in-cluster:** `0.2% full (2.6 GB of 1.00 TB), Σ shared quota 150 GB, oversub 0.15x`. Tests + 4 red-proofs; hub v0.64.0 REPORT
|
||||
|
||||
**Gap / roadmap (as written):** Two open legs: the **UI render** is unit-verified only (hub UI password-gated → no screenshot); the **alert emails** are unit + red-proof verified but NOT fired live (real pool nominal — a live-fire emails Viktor). Thresholds pending Viktor's ruling (named config keys). READ-ONLY (GET)
|
||||
|
||||
---
|
||||
|
||||
<a id="g10"></a>
|
||||
## G10 — Operator sees PBS DR datastore fill at a glance (Offsite "PBS DR" tab + dashboard gauge)
|
||||
|
||||
*Map row: [G10](../00-capability-map.md#g10) · former map line 234.*
|
||||
|
||||
**Scenario (as written):** **Operator sees PBS DR datastore fill at a glance (Offsite "PBS DR" tab + dashboard gauge)**
|
||||
|
||||
**Components (as written):** hub v0.65.0 + tenantsync v1.2.0 (R-5)
|
||||
|
||||
**Status (as written):** **IMPLEMENTED** (data pipeline PROVEN-LIVE)
|
||||
|
||||
**Evidence (as written):** The PBS DR datastore (`felhom-offsite` on ep0) fill — NOT a Hetzner box. **Option A:** a read-only `usage` op on the `felhom-tenantsync` ep0 forced command (twin of `fingerprint`; `df` on the datastore path — no customer_id, no admin token, NO mutation), polled by `monitor.PBSDRBoxChecker` (OffsiteBoxChecker clone; 15-min throttle; states ok/unavailable/degraded; fill 80/90% on the `"pbsdr-box"` operator scope). `/offsite` split into Restic + PBS DR tabs; two dashboard gauges. **Graceful: hub deploy ⟂ ep0 update** (ep0 ≤ v1.1.0 → gauge "n/a" until updated). **Phase-0-pinned** (`df` on ep0 PBS 4.2.3) + **live-computed in-cluster** (ep0 updated to v1.2.0 this session): `19.1% full (7.1 GB of 37.2 GB)`. 10 Go tests + a bash harness + 3 red-proofs; hub v0.65.0 REPORT
|
||||
|
||||
**Gap / roadmap (as written):** Open legs: **UI render** unit-verified only (hub UI password-gated); the **fill alert email** is unit + red-proof verified, NOT fired live (datastore nominal at 19%). Separate PBS threshold keys (default 80/90); no oversubscription (namespaces, not quotas). READ-ONLY
|
||||
|
||||
---
|
||||
|
||||
<a id="g11"></a>
|
||||
## G11 — The hub reports LOSS OF VISIBILITY into either off-site store (not just how full it is)
|
||||
|
||||
*Map row: [G11](../00-capability-map.md#g11) · former map line 235.*
|
||||
|
||||
**Scenario (as written):** **The hub reports LOSS OF VISIBILITY into either off-site store (not just how full it is)**
|
||||
|
||||
**Components (as written):** hub **v0.106.0** (R-339)
|
||||
|
||||
**Status (as written):** **IMPLEMENTED — deliberately NOT proven-live**
|
||||
|
||||
**Evidence (as written):** Both box checkers count consecutive failed fetch windows and emit `pbsdr_box_unreachable` / `offsite_box_unreachable` (severity `warning`) past a default 3 windows (≈30–45 min), each with a paired `*_recovered` all-clear routed via `recoveredPairedDownTypes` — required because the recoveries are severity `info`, which `severityNotifies` drops. Scopes stay customer-less (`pbsdr-box` / `pool-box`) → operator channel only. Fill logic untouched: a degraded read still drives no band transition. Evidence: `internal/monitor/box_reachability_test.go` + the cross-package wiring test in `internal/notify/`, which asserts an actual operator mail rather than a map entry. **Filed BECAUSE of a measured gap**, not a hypothesis: the 2026-08-18 ep0 outage ran 9 h 37 m with the hub silent
|
||||
|
||||
**Gap / roadmap (as written):** **The gap that remains is R-340**, and it is not small: the ep0 read is the `usage` op, which rides the LOCAL API daemon — the daemon that incident explicitly cleared — so this check would have shown GREEN for that entire outage. It closes "ep0 is unreachable as a host"; it does not close what actually happened. **No live or constructed outage has exercised the emit path**, and one cannot be manufactured against ep0 (Tier 2, protected)
|
||||
|
||||
---
|
||||
|
||||
<a id="g12"></a>
|
||||
## G12 — Secrets hygiene: bearer in k8s Secret, no secrets in git, single-quote credential store
|
||||
|
||||
*Map row: [G12](../00-capability-map.md#g12) · former map line 236.*
|
||||
|
||||
**Scenario (as written):** Secrets hygiene: bearer in k8s Secret, no secrets in git, single-quote credential store
|
||||
|
||||
**Components (as written):** hub v0.53, conventions
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** 07-13 closing bundle
|
||||
|
||||
**Gap / roadmap (as written):**
|
||||
|
||||
---
|
||||
|
||||
<a id="g13"></a>
|
||||
## G13 — Operator login password changeable from UI
|
||||
|
||||
*Map row: [G13](../00-capability-map.md#g13) · former map line 237.*
|
||||
|
||||
**Scenario (as written):** Operator login password changeable from UI
|
||||
|
||||
**Components (as written):** hub v0.54
|
||||
|
||||
**Status (as written):** **IMPLEMENTED**
|
||||
|
||||
**Evidence (as written):** 07-13
|
||||
|
||||
**Gap / roadmap (as written):**
|
||||
|
||||
---
|
||||
|
||||
<a id="g14"></a>
|
||||
## G14 — The operator sees boxes left behind: per-customer controller floors with their age (and which a global raise cannot move), each box's agent against the vouched one, a 7-day "agent behind" alarm and a "floor raise skipped boxes" mail
|
||||
|
||||
*Map row: [G14](../00-capability-map.md#g14) · former map line 238.*
|
||||
|
||||
**Scenario (as written):** **The operator sees boxes left behind: per-customer controller floors with their age (and which a global raise cannot move), each box's agent against the vouched one, a 7-day "agent behind" alarm and a "floor raise skipped boxes" mail**
|
||||
|
||||
**Components (as written):** hub **v0.135.0** (R-604, R-530)
|
||||
|
||||
**Status (as written):** **IMPLEMENTED — the page PROVEN-LIVE, the alarm and the mail unit-proven**
|
||||
|
||||
**Evidence (as written):** `audits/hub-safety-2026-10-05/partD/` (live System page: three per-customer floors, Tester 2 `0.142.0 → 0.145.0`); `osupdates/r530_agent_alarm_test.go`, `web/r604_floor_held_back_test.go`
|
||||
|
||||
**Gap / roadmap (as written):** the mail fires only on a GLOBAL raise below an override — not exercised live
|
||||
|
||||
---
|
||||
|
||||
<a id="g15"></a>
|
||||
## G15 — The hub database survives the loss of DooPlex: a nightly consistent copy, encrypted, on ep0; restore-tested weekly; an alarm when either stops
|
||||
|
||||
*Map row: [G15](../00-capability-map.md#g15) · former map line 239.*
|
||||
|
||||
**Scenario (as written):** **The hub database survives the loss of DooPlex: a nightly consistent copy, encrypted, on ep0; restore-tested weekly; an alarm when either stops**
|
||||
|
||||
**Components (as written):** hub **v0.136.0** (R-173), `scripts/hub-db-backup/`, homelab-manifests rules
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-10-05)** — first push, ep0 listing, restore test (4 hosts, 4 sealed, 0 readable), token limits, a key rebuilt from the paper copy decrypts, the saved seal key opens 4/4 console passwords in the restored copy; the alarm by `promtool` rule test + red-proofs ; **2026-10-09: the whole recovery (§3 steps 1–5) PROVEN on a throwaway k3s** — the live hub image started on the restored copy, customers 4/4 and hosts 4/4 equal live, 4/4 console passwords revealed
|
||||
|
||||
**Evidence (as written):** `audits/hub-db-offsite-2026-10-05/`; `audits/dooplex-survival-2026-10-09/partD-*.txt`; `runbooks/RUNBOOK-hub-db-offsite-backup.md`
|
||||
|
||||
**Gap / roadmap (as written):** A restored hub mails households pending notices at once — a test restore has no network (runbook §3)
|
||||
|
||||
---
|
||||
|
||||
<a id="g16"></a>
|
||||
## G16 — Gitea (all code), the operator's password manager (Vaultwarden, since 2026-10-09 afternoon, R-923) and DooPlex's secrets survive the loss of DooPlex: a nightly encrypted copy on ep0; restore-tested weekly; an alarm when either stops; a failure mail
|
||||
|
||||
*Map row: [G16](../00-capability-map.md#g16) · former map line 240.*
|
||||
|
||||
**Scenario (as written):** **Gitea (all code), the operator's password manager (Vaultwarden, since 2026-10-09 afternoon, R-923) and DooPlex's secrets survive the loss of DooPlex: a nightly encrypted copy on ep0; restore-tested weekly; an alarm when either stops; a failure mail**
|
||||
|
||||
**Components (as written):** `scripts/dooplex-offsite/` (R-232), homelab-manifests rules
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-10-09)** — first push 57 s, read back with the read-only token (27 805 files, 10 repos pass `git fsck`); Gitea restored into a throwaway and started: 10/10 repos, product `main` = live, a file byte for byte, a login; alarm by `promtool` + red-proofs; failure mail reached the inbox
|
||||
|
||||
**Evidence (as written):** `audits/dooplex-survival-2026-10-09/`; `runbooks/gitea-restore.md`
|
||||
|
||||
**Gap / roadmap (as written):** The container registry is NOT copied (images rebuild from the code); the on-box backup tree is still one writable path (R-232 c); the copy opens only with keys that must be on paper (`runbooks/break-glass-sheet.md`; the signing keys are in no copy, R-924)
|
||||
|
||||
---
|
||||
|
||||
<a id="g17"></a>
|
||||
## G17 — The operator surface refuses a cross-site form post on BOTH login paths (session token; Basic auth + `X-Felhom-Operator`)
|
||||
|
||||
*Map row: [G17](../00-capability-map.md#g17) · former map line 241.*
|
||||
|
||||
**Scenario (as written):** **The operator surface refuses a cross-site form post on BOTH login paths (session token; Basic auth + `X-Felhom-Operator`)**
|
||||
|
||||
**Components (as written):** hub **v0.135.0** (R-135)
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE (2026-10-05)**
|
||||
|
||||
**Evidence (as written):** `audits/hub-safety-2026-10-05/partA/live.txt` (Basic, no header → 403 even on an unknown path; with the header → passes; header without credentials → 401); `web/r135_csrf_test.go` (39 paths)
|
||||
|
||||
**Gap / roadmap (as written):**
|
||||
|
||||
---
|
||||
|
||||
<a id="g18"></a>
|
||||
## G18 — Box operating-system security updates (Proxmox host, guest Debian, Docker engine)
|
||||
|
||||
*Map row: [G18](../00-capability-map.md#g18) · former map line 242.*
|
||||
|
||||
**Scenario (as written):** Box operating-system security updates (Proxmox host, guest Debian, Docker engine)
|
||||
|
||||
**Components (as written):** agent v0.143.0, hub v0.133.0
|
||||
|
||||
**Status (as written):** **PARTIAL — the GUEST and HOST Debian fast lanes and the DOCKER engine slow lane are PROVEN-LIVE (2026-10-04), with the System page, the fleet view and the alarms; the KERNEL lane is MISSING**
|
||||
|
||||
**Evidence (as written):** Guest: `audits/os-guest-lane-2026-10-04/`. Host + fleet + alarms: `audits/os-host-lane-2026-10-04/`. Docker + System page + crash guard: `audits/os-docker-crash-2026-10-04/` — live-restore on with the same container ids on every box; Docker 29.8.2 on both demo boxes; operator-approved Docker release; a signed undo and a signed ring-1 step; a replay refused; the crash guard restarted demo-hp twice and kept it off the third time. Design `architecture/11-os-updates.md` §5.8, §5.9, §8
|
||||
|
||||
**Gap / roadmap (as written):** **No automatic undo** (guest: last night's backup; host: by-hand runbook; Docker: a signed undo job); existing boxes get root-owned files by the signed config bundle since 2026-10-04 (R-840 CLOSED; a box from before agent 0.143.0 needs one by-hand bootstrap — Tester 2: R-862; `audits/r840-config-bundle-2026-10-04/`); test approvals now end with the test (R-859); the agent's sudoers is root-equivalent (R-861); the kernel lane (R-836); facts reach the hub late after a boot (R-853). **2026-10-05 (agent v0.144.1):** R8 measures the real download (R-865); a killed pass still reports (R-868, live); the debug pass runs with the hub away (R-866, live); **a power cut mid-update was proven by day on demo-hp — the box came back by itself in 37 s, but the next pass fails until `dpkg --configure -a` is run by hand (R-876, P2, open)** — `audits/night-fixes-2026-10-05/`. **2026-10-05 afternoon (agent v0.145.0): R-876 FIXED and proven live — after a second crash mid-unpack the next pass repaired dpkg by itself (`REPAIR … journal=1`) and finished** — `audits/catchup-2026-10-05/partD/`
|
||||
|
||||
---
|
||||
|
||||
<a id="g19"></a>
|
||||
## G19 — An ENGLISH-SPEAKING household's first hour: download, install, pair, bind, claim, two apps
|
||||
|
||||
*Map row: [G19](../00-capability-map.md#g19) · former map line 243.*
|
||||
|
||||
**Scenario (as written):** **An ENGLISH-SPEAKING household's first hour: download, install, pair, bind, claim, two apps**
|
||||
|
||||
**Components (as written):** controller **v0.259.0** + hub **v0.119.0** + ISO 1.29.0 + the whole catalog
|
||||
|
||||
**Status (as written):** **PROVEN-LIVE on 0.258.0 with one blocker; THE BLOCKER IS FIXED AND PROVEN, THE WALK IS NOT REPEATED**
|
||||
|
||||
**Evidence (as written):** `audits/DRILL-first-hour-en-0258-2026-09-20.md` — a fresh install 2026-09-20, one intervention (R-494), stop rule not reached. Then `audits/i18n-closing-2026-09-21/live/` — the three blockers fixed and each proven on a live box or in the operator's inbox: the claim page answers English through the real cookie path; the Backup page's tier names follow the language; and the setup mail carries **four plain-ASCII English words** where the drill's carried `képző-szkítia-ásatás`, one day apart in the same inbox.
|
||||
|
||||
**Gap / roadmap (as written):** **R-596, R-597 and R-598 are CLOSED.** What this row still does NOT claim: **the fixed journey has not been walked end to end by a stranger on a fresh install.** Three fixes proven at the endpoint are not an hour proven by a person, and this project's own rule is that fixes are not a journey (see the recovery-journey row). **Also not walked:** the recovery code (needs ep0), backup/restore/remove/power-cut (proven 2026-09-14), and the two Backup-page *warnings* themselves — guest 9201 is healthy and a healthy box renders none, so they are covered by handler render tests, not live. **Verdict: nothing known now stands between an English-speaking tester and their box — and that is a different sentence from "the walk passed".**
|
||||
|
||||
---
|
||||
|
||||
<a id="g20"></a>
|
||||
## G20 — A deletion of a customer's off-site history is NOTICED within a day
|
||||
|
||||
*Map row: [G20](../00-capability-map.md#g20) · former map line 244.*
|
||||
|
||||
**Scenario (as written):** **A deletion of a customer's off-site history is NOTICED within a day**
|
||||
|
||||
**Components (as written):** hub **v0.111.0** (R-431)
|
||||
|
||||
**Status (as written):** **IMPLEMENTED — not yet PROVEN-LIVE**
|
||||
|
||||
**Evidence (as written):** 09-01
|
||||
|
||||
**Gap / roadmap (as written):** `hub/internal/monitor/offsite.go` — third signal beside FILL and STALENESS. **On the hub deliberately:** a detector on the box is one the deletion can silence. Alarms when the reported count falls by more than HALF and by at least 5, guarded by `StatsKnown` (R-331), the declared `State` (R-204) and run success (R-100). **Threshold reasoned, not invented:** over 12 898 reports every decrease lands on ZERO and predates `stats_known`; in the 380-report `stats_known` window there are none. **ACCEPTANCE: 9 009 real points replayed → ZERO alarms** (`offsite_r431_test.go`, fixture committed). **What PROVEN-LIVE would need and this does NOT have:** a real drop observed on a live box producing a real mail — the live firing done at ship time was driven through the hub's own path with synthetic counts, which is an end-to-end delivery proof, not a proof that a genuine deletion is caught.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,62 @@
|
||||
# Capability map — detail, section H. Business & legal (outside the platform — added 2026-10-03)
|
||||
|
||||
> **What this file is.** The long text of every row of section H 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="h1"></a>
|
||||
## H1 — Website legal pages: ÁSZF, adatkezelési tájékoztató, impresszum
|
||||
|
||||
*Map row: [H1](../00-capability-map.md#h1) · former map line 253.*
|
||||
|
||||
**Scenario (as written):** Website legal pages: ÁSZF, adatkezelési tájékoztató, impresszum
|
||||
|
||||
**Components (as written):** website
|
||||
|
||||
**Status (as written):** **MISSING**
|
||||
|
||||
**Evidence (as written):** `website/kapcsolat.html:123-128` asks for data-processing consent and links to no notice
|
||||
|
||||
**Gap / roadmap (as written):** → R-813 (finding), R-809
|
||||
|
||||
---
|
||||
|
||||
<a id="h2"></a>
|
||||
## H2 — Customer contract and data-processing agreement
|
||||
|
||||
*Map row: [H2](../00-capability-map.md#h2) · former map line 254.*
|
||||
|
||||
**Scenario (as written):** Customer contract and data-processing agreement
|
||||
|
||||
**Components (as written):** —
|
||||
|
||||
**Status (as written):** **MISSING**
|
||||
|
||||
**Evidence (as written):** —
|
||||
|
||||
**Gap / roadmap (as written):** → R-809
|
||||
|
||||
---
|
||||
|
||||
<a id="h3"></a>
|
||||
## H3 — Billing and invoicing
|
||||
|
||||
*Map row: [H3](../00-capability-map.md#h3) · former map line 255.*
|
||||
|
||||
**Scenario (as written):** Billing and invoicing
|
||||
|
||||
**Components (as written):** —
|
||||
|
||||
**Status (as written):** **MISSING**
|
||||
|
||||
**Evidence (as written):** —
|
||||
|
||||
**Gap / roadmap (as written):** → R-809
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user