diff --git a/documentation/architecture/00-capability-map.md b/documentation/architecture/00-capability-map.md index 792db2e..b06dcb2 100644 --- a/documentation/architecture/00-capability-map.md +++ b/documentation/architecture/00-capability-map.md @@ -121,7 +121,7 @@ | Scenario | Components | Status | Evidence | Gap / roadmap | |---|---|---|---|---| -| An ABSENT backup-target drive raises its OWN alarm, paired with a matching recovery | agent v0.114.0, controller v0.184.1+, hub v0.81.0 | **PARTIAL** | `SESSION-C-2026-07-29` C5: the drive-absent gate fires (4 s) and an alarm reaches the hub — but it is the **generic** `storage_disconnected`, while the return fires the **specific** `backup_target_restored`, so the pair cannot be matched. `backup_target_absent` never fired (count 0) | The specific alarm and its severity, Hungarian copy and hub routing are all still unexercised end-to-end → **R-116** | +| An ABSENT backup-target drive raises its OWN alarm, paired with a matching recovery | agent v0.114.0, controller v0.184.1+, hub v0.81.0 | **PARTIAL** | `SESSION-C-2026-07-29` C5: the drive-absent gate fires (4 s) and an alarm reaches the hub — but it is the **generic** `storage_disconnected`, while the return fires the **specific** `backup_target_restored`, so the pair cannot be matched. `backup_target_absent` never fired (count 0) | RE-TESTED 2026-07-30 on the fully shipped stack (`R116-PART5-2026-07-30`) — **still generic**; the v0.115.0 fix was ineffective and the mechanism is not isolated. The specific alarm, its severity, Hungarian copy and hub routing remain unexercised end-to-end → **R-116** | | Health-degradation email (edge-triggered, cooldowns, Hungarian) via hub → Resend | controller, hub | **IMPLEMENTED** | 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 | 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 | | Event catalog: app_start_failed, dead-app, offbox_enlarge_blocked, claim/reset codes, critical severity | controller, hub v0.31/48/50/55 | **PROVEN-LIVE** | live-delivered: `CAMPAIGN-6D` P3-DELIVERY (enlarge-block, op+customer); `DRILL-day0-vm` F-4 (claim code); `DRILL-day0-take2` F-15 (reset code) | `app_start_failed`/`dead-app` delivery is unit-only (6C inconclusive) — the pipeline + 3 event families are live, those two are not | | Prefs safety: empty-email wipe guard | controller v0.137 + hub v0.71.0 | **IMPLEMENTED** | 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 | Born from a live incident; controller 0.160.0 guards both its push legs, so the hub belt covers older/rogue boxes | diff --git a/documentation/audits/R116-PART5-2026-07-30.md b/documentation/audits/R116-PART5-2026-07-30.md new file mode 100644 index 0000000..565c818 --- /dev/null +++ b/documentation/audits/R116-PART5-2026-07-30.md @@ -0,0 +1,94 @@ +# R116-PART5-2026-07-30 — the fix shipped, C5 still fails, and the mechanism is NOT isolated + +**Run:** `RUNBOOK` R-116 Part 5, executed by CC on DooPlex, 2026-07-30. +**Result: FAILED.** Agent **0.115.0** — the R-116 fix, installed from the Day-0 manifest — did **not** +change the outcome. The absent-drive alarm is still the generic one. + +| Claim | Verdict | +|---|---| +| **C5** — `backup_target_absent` on detach, `backup_target_restored` on return, paired | ❌ **FAIL** — detach still fires `storage_disconnected`; return fires the specific event. Same asymmetry as Session C | +| **Discrimination** — target ⇒ specific, non-target ⇒ generic | ❌ **FAIL** — the *target* produced the generic event, so the two are still indistinguishable. Not achieved | +| **No over-correction** | ✅ **PASS** — 0 ABSENT lines with the drive present; target `degraded:false` | + +## 1. What ran — the fully shipped stack + +| Artifact | Version | How | +|---|---|---| +| agent | **0.115.0** | **from the Day-0 manifest** — `Artifact manifest served for customer sess-d (agent=0.115.0 golden=0.185.1)`; host row `sess-d-0452c4 … 0.115.0 ONLINE` | +| controller | **0.185.1** | from the vouched golden — **no hand-deploy this run**, deliberately, so the whole stack is the shipped article | + +Part 4's three positive observables were all obtained **before** the run: registry newest `0.115.0`, +hub vouches `0.115.0`, felhom-pve reports `felhom-agent 0.115.0` active with 0 errors. The publish +step that was forgotten twice was not forgotten. + +Fence reconfirmed: `local-lvm` **38.83 %** before and after. Guest 9201 and `drill-r50` untouched. + +## 2. The observation + +``` +05:07:12 [gate] drive ABSENT /mnt/felhom-drives/cel — stopped+blocked 0 app(s) +05:07:15 Event pushed: storage_disconnected (error) — Meghajtó váratlanul leválasztva: Cel meghajto + backup_target_absent : 0 storage_disconnected : 1 + +05:11:43 [gate] drive RETURNED /mnt/felhom-drives/cel +05:11:43 Event pushed: backup_target_restored (info) +``` + +Identical to Session C. The gate fires correctly (R-113 holds, 0 s–3 s), `SetDisconnected` is reached, +and the wrong event goes out. + +## 3. Why the fix did not fire — NOT ISOLATED, and that is the honest answer + +The v0.115.0 change sets `di.GuestPath` on the Observe row when +`di.GuestPath == "" && di.BackupTarget && t.BackingDevice == ""`. It is unit-tested and red-proofed +against a fixture built from the Session-C shape. On the live box it changed nothing. + +**What was established:** + +- With the drive detached, `pvesm status` reports the storage as + `felhom-backup dir inactive 0 0 0`, and PVE emits + `unable to activate storage 'felhom-backup' - directory is expected to be a mount point but is not + mounted: '/mnt/cel'`. So the storage is *listed but inactive with zeroed fields* — a shape the unit + fixture did not model (it supplied a `MountPath` and a `disconnected` state). + +**What was NOT established, and must not be guessed at again:** the actual `/disks` payload. Three +attempts to read it failed on token extraction across the ssh → guest → container layers. A +**control query with the drive PRESENT also returned 0 rows**, which proves the query itself was +broken, not the payload. Without that control this run would have recorded a third false mechanism — +after "the union row writes false" (wrong, corrected) and "no row carries the guest path" (unverified). + +**So the honest state is: the fix addresses a shape that does not occur on the live box, and which +shape does occur is unknown.** The leading hypothesis — that an *inactive* storage reaches Observe with +an empty `MountPath`, so `StablePathForRaw("")` returns `""` and the fallback sets nothing — is +consistent with the `pvesm` output but **is not evidence**. + +**The next session's first job is a working `/disks` read**, with a present-drive control run *first*, +before any further code. Everything after that is guesswork until it exists. + +## 4. Also observed + +- **The stale bind survives the return and still names a dead device node.** After the drive came back + as `/dev/sdc`, `findmnt /mnt/felhom-drives/cel` still read `/dev/sdb[/felhom-data]` while the state + reported `degraded:false`. Third consecutive run showing this; still not investigated. +- **Discrimination could not be reached.** Because the target itself produced the generic event, there + was no point staging the non-target leg: the two cannot be told apart regardless of what it emits. + Reported as a FAIL, not as Session C's trivial pass. + +## 5. Teardown + +VM 9302 destroyed `--purge`, `p5-images` removed, scratch dir removed. **`pvesm status` after == before** +(`local-lvm` 38.83 %). Guest 9201 and `drill-r50` untouched. + +**Customer `sess-d` + host `sess-d-0452c4` remain** — the delete gate refuses while the host still reads +ONLINE, as in both prior runs. Deletable once it ages to DOWN (>1 h): + +``` +POST /configs/sess-d/delete ack_hosts=1 ack_reset=1 ack_purge=1 confirm_id=sess-d expect_hosts=1 +``` + +## 6. Standing of agent v0.115.0 + +It is **published and vouched**, and it is **inert** — it adds a guest path in a case that apparently +does not arise, so it neither fixes nor breaks anything observable. It was NOT reverted: reverting is +itself a change, the runbook forbids fixing mid-run, and the code is well-tested and harmless. Whether +to keep, extend or revert it is the next session's decision, taken with a working `/disks` read in hand. diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 0618a02..6fc3dd1 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -16,7 +16,7 @@ State: `BLOCKED` · `READY` · `WAITING-ON-OPERATOR` · `WATCHING`. Every row ha | **R-110** | **`main` is the installer's publish channel — there is no staging.** `manifests/webpage.yaml` git-syncs `/scripts/` from `--branch=main` on a 30 s period and nginx serves that working tree directly (`location /scripts/`, `root …/current`). So pushing `scripts/felhom-host-install.sh` **is** publishing it: within thirty seconds it is what every subsequent `felhom-bootstrap.sh` fetch (`scripts/iso/felhom-bootstrap.sh:96`) and every operator-run day-0 command (`customer_unified.html:563`) receives. There is no tag, no pinned-version path, no staging copy and no rollback other than another push — for the artifact that runs as **root on a virgin box**, the single most privileged thing Felhom ships | **WAITING-ON-OPERATOR (S)** | operator ruling | **Two consequences worth stating:** E-2d is not a gate *before* exposure — 1.22.0 has been the live installer since it hit `main` on 2026-07-29 — and the precaution recorded on the old R-94 row as "do not point every new box at an installer that has never run" **was never available to take**. **Open question for the operator, not a defect to fix blind:** whether `/scripts/` should serve a pinned release (tag-tracked path, or a versioned directory with the customer command naming a version) or whether `main`-tracking is the accepted shape for a one-operator product. Exposure today is zero — there are no boxes installing — which is exactly why it is cheap to decide now. **SECOND INSTANCE, found 2026-07-29 by the E-2d run and filed here rather than as a new ID:** `felhom-host-install.sh` fetches **nine** files from `raw/branch/main` (`:2072`–`:2206`) and the hub manifest vouches a sha for exactly **one** (`wrapper_sha256` → `felhom-pbs-apply`; re-checked this run, no drift). E-2a's `felhom-backup-target-apply` (`:2116`) is installed **0755 to `/usr/local/sbin` and root-fenced in sudoers**, validated only by `bash -n` — a root-executed artifact taken from `main` with no pinned integrity, which is this row's class exactly | CC | | **R-111** | ~~**The Day-0 artifact channel is 17 agent releases stale — a box installed today gets agent `0.96.0`, not `0.113.0`.**~~ `felhom-host-install.sh` does not use `main`: it reads the hub-vouched manifest (`:423-436`) and fetches Gitea generic packages (agent `:1945`, golden `:2573`). Gitea's newest are **agent 0.96.0** and **golden 0.161.0**, and the hub's manifest selects exactly those — so a fresh box lands on **agent 0.96.0 + controller 0.161.0** (global floor `v0.156.0` < the golden's 0.161.0, so no self-update) against `main`'s 0.113.0 / 0.185.1. Agent 0.113.0 reached both demo boxes by **direct deploy and was never published** | **SHIPPED 2026-07-29 — the channel now serves agent 0.113.0 + golden 0.185.1** | — | **FIXED the same day it was found.** Agent **0.113.0** built from the clean tree @ `58b598b` and published (`scripts/publish-agent.sh`), sha `5f3247f756cb658e…`, round-trip GET verified. Golden **0.185.1** baked on the nested drill VM embedding controller `0.185.1`, published, sha `dba00f3e845c415e…` — bake clean: `Result=success`, overlay2, **all 3 mounts included** (rootfs+mp0+mp1), 0 FATAL/exclusions, upload HTTP 201, token-leak grep 0; log `drill/bake-0.185.1.log`; GL-1 teardown done (guest 9100 purged, secrets shredded, disk restored to `virgin`). Hub Day-0 manifest moved **both together in one POST** so it never vouched a new agent against an old golden; `min_agent` **0.93.0 → 0.113.0**, which is what controller v0.185.0 declares (`felhom-controller/CHANGELOG.md:15`) — **zero fleet impact, verified: all three enrolled hosts already run agent 0.113.0, so no box is held.** `wrapper_sha256` preserved verbatim (re-checked against `configs/felhom-pbs-apply` — no drift). **The global controller floor was deliberately NOT raised**: the golden now bakes 0.185.1, so a fresh box needs no self-update, and raising it would have been an unnecessary fleet-wide write. Original finding follows. **Found 2026-07-29 by the E-2d Phase 0 gate, which stopped the run before a VM was created.** 17 unpublished releases (v0.97.0–v0.113.0) strand the **entire R-82 tiered-backup arc** plus **F-CRIT-2** (a failed backup looking fresh — 7 days silent) and **F-REBOOT** (a guest rebooted mid-backup never returns): a new customer's box would install without them. **Blocks E-2d's C3/C4/C5** — those test endpoints and events that do not exist in 0.96.0/0.161.0. The **controller is fine** (registry has 0.185.1, floor-driven self-update), so the gap is specific to the two Gitea-generic artifacts. **Mirror of R-110, not a duplicate:** R-110 = the installer publishes instantly with no staging; R-111 = the agent/golden publish gate exists and was never walked. Fix should decide whether publishing joins the release train rather than staying a remembered step (R-29's shape, one layer up). Evidence: `audits/E2D-fresh-vm-2026-07-29.md` **DEFERRED LEG, AND IT RECURRED → R-115.** This row's shipped half stands and is not reopened: the bump happened, was verified, and was proven end-to-end by the E-2d install. But its own closing line — *decide whether publishing joins the release train rather than staying a remembered step* — was never acted on, and agent 0.114.0 reproduced the exact condition the same afternoon. The recurrence is filed as **R-115**, not as a reopen, because the stale-channel finding is closed while the process defect that caused it is a distinct problem with a distinct fix. | CC | | **R-115** | **Publishing is a remembered step, and it was forgotten within eight hours of being documented as forgettable.** A box installs the agent from a Gitea generic package the hub explicitly vouches, never from git. Nothing in the build, deploy or session-end path publishes or checks that a version was published — so "deployed" and "installable" are independent states that drift silently. **Two instances, both real:** **R-111** (2026-07-29 morning) — 17 agent releases v0.97.0–v0.113.0 stranded, so a new customer would have installed without the entire R-82 tiered-backup arc plus F-CRIT-2 and F-REBOOT; found only because the E-2d Phase 0 gate happened to look. **Agent 0.114.0** (same afternoon) — the R-113 fix, built and pushed at `b58d7bc`, deployed to felhom-pve, and **unpublished until this task**, which blocked Session C: a fresh drill box would have installed 0.113.0 and proven the bug rather than the fix | **WAITING-ON-OPERATOR (M)** | operator ruling on the release process | **The finding is the RECURRENCE, not either instance** — both instances are fixed. R-111's own text already named this leg (*"decide whether publishing joins the release train rather than staying a remembered step"*) and closed SHIPPED without it; the leg then recurred the same day, which is the evidence that a note is not a mechanism. **Class: → R-29, one layer up** — a control that exists and is never walked; deliberately NOT given its own ID. **The decision is the operator's; the options, mechanisms first:** (a) **publish as a step in the build/release path**, so deployed and installable cannot diverge; (b) **a gate that refuses to deploy a version that is not published+vouched** — the strongest, and it fails closed; (c) a session-end checklist entry; (d) accept it as manual and add a pre-Session-C verification. **(a) and (b) are mechanisms; (c) and (d) are reminders — and R-29's whole finding is that reminders do not hold.** No code this session by design | CC | -| **R-116** | **The drive-absent alarm and its recovery are a MISMATCHED PAIR — absent fires the GENERIC `storage_disconnected`, return fires the SPECIFIC `backup_target_restored`.** `backup_target_absent` never fires at all (count 0 across a full Session-C run). An operator gets an alarm they cannot match to its recovery, which is exactly what `notifyDriveReturned`'s own comment forbids | **READY (S) — P1** | — | **PROVEN LIVE 2026-07-29 (`audits/SESSION-C-2026-07-29.md` §5).** Root cause at source: `notifyDriveAbsent` (`intermediary.go:635-646`) branches on `isTarget[a.Path]` where `a.Path` is the GUEST path, and `driveTargetByPath` (`:602-616`) builds that map as `out[GuestPath] = d.BackupTarget`. **The drive is TWO `/disks` rows and the flag and the guest path sit on different ones:** the `felhom-backup` storage row carries `BackupTarget: true` (`felhom-agent/internal/localapi/disks.go:211`) but gets a `GuestPath` only while its role classifies as user-data; the registry union row carries the `GuestPath` but **never assigns `BackupTarget` at all** (`disks.go:265-267`). While the device is absent the flagged row loses its guest path, so the union row writes `false` and the specific branch is skipped — on RETURN the rows rejoin and the specific event fires, which is the observed asymmetry. **v0.184.1 fixed the KEYING** (both keys are mapped) **and not this** — the flag is missing from the row that has the guest path. **Only reachable because R-113 made the gate fire at all**; before that the branch was never taken. Fix likely belongs agent-side (the union row should carry the flag), so decide the repo before implementing. Blocks E-2's C5 | CC | +| **R-116** | **The drive-absent alarm and its recovery are a MISMATCHED PAIR — absent fires the GENERIC `storage_disconnected`, return fires the SPECIFIC `backup_target_restored`.** `backup_target_absent` never fires at all (count 0 across a full Session-C run). An operator gets an alarm they cannot match to its recovery, which is exactly what `notifyDriveReturned`'s own comment forbids | **STILL OPEN — fix shipped (agent v0.115.0) and INEFFECTIVE; mechanism NOT isolated** | a working `/disks` read | **PART 5 RE-VALIDATION FAILED 2026-07-30 (`audits/R116-PART5-2026-07-30.md`).** A fresh box running the FULLY SHIPPED stack — agent **0.115.0 from the Day-0 manifest** + controller 0.185.1 from the vouched golden — still fired the **generic** `storage_disconnected` on detach and the **specific** `backup_target_restored` on return. `backup_target_absent` count 0. Identical to Session C. **Discrimination FAILS** (the target itself produced the generic event, so the non-target leg was pointless to stage — reported as a fail, not as Session C's trivial pass). **Over-correction guard PASSES** (0 ABSENT lines with the drive present). **The v0.115.0 fix targets a shape that does not occur live, and WHICH shape does occur is NOT ISOLATED.** With the drive detached PVE reports the storage `inactive` with zeroed fields — a shape the unit fixture did not model. Three attempts to read the real `/disks` payload failed on token extraction, and **a present-drive control also returned 0 rows, proving the QUERY was broken, not the payload** — without that control this would have been a third false mechanism, after "the union row writes false" (wrong, corrected) and "no row carries the guest path" (unverified). **NEXT SESSION'S FIRST JOB: a working `/disks` read with a present-drive control run FIRST, before any code.** v0.115.0 is published, vouched and **inert**; not reverted (reverting is itself a change, and the runbook forbids fixing mid-run) | CC | | **R-113** | ~~**The drive-absent gate CANNOT FIRE on device loss — E-2b's alarm is wired to an unreachable condition.**~~ `planDriveGates` (`intermediary.go:216-262`) computes presence by OR-ing `d.BoundUnderParent` into `present[GuestPath]`, and the agent derives `BoundUnderParent` from `GuestSeesMount()` — *"is this path a mount target in the guest's `/proc//mountinfo`"* (`localapi/disks.go:210`). The raw drive mount is a **device-bound systemd unit** and dies with the device; **the agent's own bind under the shared parent is NOT device-bound, so its mountinfo entry outlives the device**. The gate reads the surviving bind as "present" ⇒ no `Stop` action ⇒ `notifyDriveAbsent` never called | **SHIPPED + PROVEN-LIVE** (agent v0.114.0, 2026-07-29) | — | **PROVEN LIVE `audits/SESSION-C-2026-07-29.md`.** Target drive hot-detached on a fresh box running the SHIPPED agent 0.114.0 (from the Day-0 manifest, not a hand build). **The gate fired in 4 seconds** — E-2d measured ZERO over 4½ minutes — and `SetDisconnected` was reached. It fired on exactly the shape that defeated it: raw `/mnt/mentes` NOT mounted while the bind `/mnt/felhom-drives/mentes` still read `/dev/sdb[/felhom-data]`. **Over-correction guard PASSED:** with both drives present, 0 ABSENT lines and the target stayed healthy. **Note: C5 still FAILED — but on a different defect (→ R-116), not on this one.** **SESSION C IS NOW UNBLOCKED.** Agent **0.114.0 published and vouched** 2026-07-29 (sha `5e4c15ebee2d7583…`, round-trip GET verified; hub manifest read back showing it) — a fresh drill box now installs the fix under test instead of the bug. The golden was NOT re-baked and `min_agent` was NOT raised: the golden bakes the controller, not the agent, and controller v0.185.0 declares MinAgent 0.113.0, which 0.114.0 already satisfies. **FIX: `BoundUnderParent` is now a CONJUNCTION — bound under the parent AND the drive's raw host mount still mounted** (`devicePresent`, new `deviceCheck` seam), at BOTH `/disks` construction sites. The raw mount is the device-bound systemd unit that dies with the device; the agent's bind is not — so the raw mount IS the device signal, grounded in E-2d's measurement rather than inference. **Conjunction, deliberately:** the device half alone would regress boot ordering (raw mounts early, bind lands ~18 s later — that window must keep reading absent), so existing behaviour is byte-identical and only the unreachable case is closed. **Unknown is never absent** (`devicePresent("")` = true) — a false absent stops a working customer's apps. **Controller UNCHANGED, no MinAgent bump:** `BoundUnderParent` has exactly one functional consumer (`planDriveGates:226`); a new `DevicePresent` bool was rejected because absent-from-JSON decodes to `false`, which would have made every drive on an older agent read ABSENT. +6 tests (208→214), 4 red-proofs run and reverted. **Deployed to demo-felhom; over-correction guard verified in production** — raw mount present, drive still reads present, 10/10 apps untouched, no gate action, no false alarm. **demo-hp deliberately left on 0.113.0** (the spec scoped deploy to felhom-pve) — it still carries the bug. **⚠️ SESSION C BLOCKER: the hub Day-0 manifest vouches agent 0.113.0**, so a fresh drill box would install WITHOUT this fix and validate nothing — publish + vouch 0.114.0 first (R-111's trap, same shape). **Awaiting live proof of exactly one leg:** device loss → gate `Stop` → `SetDisconnected` → `backup_target_absent` on the wire. Original finding: **PROVEN LIVE 2026-07-29 on a fresh box.** Target drive hot-detached under a running agent; over 4½ min (budget was 60 s): the **agent** said `enrolled drive absent by UUID` every 20 s, the controller logged **0** `[gate]` lines, and the hub received **zero** events — no `backup_target_absent` **and no generic `storage_disconnected`**. Measured with the device gone: `/mnt/mentes2` NOT mounted, `/mnt/felhom-drives/mentes2` still `/dev/sdb[/felhom-data]`. **Not a virtualisation artefact** — the asymmetry is device-bound-mount vs manual-bind, identical on metal (caveat: proven on SCSI hot-detach; physical unplug not staged). **Mirror scenario reasoned, not observed:** both the specific and generic events come from the same `a.Stop` branch, so the generic one is equally unreachable. **Sixth instance of seam-built-but-never-wired** — E-2b wired the seam, to a condition that cannot occur. Evidence: `audits/E2D-fresh-vm-2026-07-29.md` §5.2 **Session C scope UNCHANGED by Session B** — R-114/R-112 shipped as controller v0.186.0 and neither touches the agent; the leg awaiting proof is still device loss → gate `Stop` → `SetDisconnected` → `backup_target_absent` on the wire. One rebuild now validates all three. | CC | | **R-112** | **E-2's degraded banner and offer have NO UI CONSUMER — the endpoint is correct and the customer never sees it.** `GET /api/storage/backup-target` returns byte-exact copy (proven live), and **nothing fetches it**: `grep 'backup-target'` across all `*.html`/`*.js`/`*.css` → **0 hits**; no template references `OfferPath`/`Degraded`/the copy; `resolveBackupTargetState` + `degradedMessageFor` are consumed **only** by the JSON handler — **no page handler injects the state** | **SHIPPED + PROVEN-LIVE** (controller v0.186.0, 2026-07-29) | — | **PROVEN LIVE `audits/SESSION-C-2026-07-29.md` — the banner reached a customer's page for the first time.** Never-configured box: banner element 1, never-configured copy 1. After the wizard: offer block 1 with `data-path="/mnt/felhom-drives/mentes"`. Healthy after assign: all four markers 0, **proven POSITIVELY** — idle delta 0 `/backup/tiers` calls, page-load delta +1, single caller ⇒ the seam ran and chose silence. **FIXED: the state now has a consumer.** Server-rendered on `/backups` via `backupsHandler` → `backupTargetView` → `backups.html`, following the existing `SingleCopyWarning` banner pattern — NOT a 19th JS fetch, because a banner that needs JavaScript to appear is one more thing that can silently not happen. `backupTargetView` returns **nil** for healthy AND unknown, so those render nothing at all. **Scenario-E seam test drives `backupsHandler` over httptest and asserts the RENDERED HTML** — deleting the one line that sets `data["BackupTarget"]` reproduces the old state and fails every render assertion. **SEAM PROVEN LIVE on demo-felhom by a DIFFERENTIAL positive observable, not by an absent banner:** idle 8 s → 0 new `/backup/tiers` agent calls; each `/backups` load → exactly +1, and that call has only one caller (`resolveBackupTargetState`). The box is healthy (`degraded:false, target:felhom-backup`) and the page correctly rendered **nothing** — which matches its real state but, being a negative, is NOT by itself proof of wiring. **Still unproven live:** that a customer sees actual copy — impossible on a healthy box. **The decisive contrast: templates fetch 18 distinct `/api/storage/*` endpoints; `backup-target` and `backup-target/assign` are the only two with zero references.** The handler's own comment calls itself *"the dashboard's source for the degraded banner and the offer"* — an invariant comment asserting a consumer that does not exist (7th instance of that class). v0.185.1 shipped as *"the offer endpoints were mounted where nothing routed to them"* — it fixed the **router mount** and stopped one layer short of the **render**; its test `TestBackupTargetRoutesLiveUnderTheStorageAPIMount` pins dispatch, not reachability, which is exactly what `CLAUDE.md`'s seam rule warns about. **Fifth instance of seam-built-but-never-wired.** **Fix R-114 FIRST** — wiring this alone would start showing customers the wrong message. Evidence: `audits/E2D-fresh-vm-2026-07-29.md` §5.1 | CC | | **R-114** | **On target-drive loss the customer is told the wrong story and offered the drive that just vanished.** With the assigned target absent, the endpoint returned `degraded:true, target:"felhom-backup"` **plus** the *"a rendszermentés ugyanazon a lemezen van, mint a rendszer"* message — false, the target is a drive that has disappeared, not the system disk — **and** `offer_path` pointing at the missing drive as the remedy | **SHIPPED + PROVEN-LIVE** (controller v0.186.0, 2026-07-29) | — | **PROVEN LIVE `audits/SESSION-C-2026-07-29.md`.** With the target absent the page rendered the ABSENT copy (1), the system-disk copy 0, the offer block 0 — both of E-2d's falsehoods gone. API carried `message:"A rendszermentés meghajtója nem érhető el…"` with `target:felhom-backup`. **FIXED: the third state exists.** New `BackupTargetState.TargetAbsent` separates *configured-and-gone* from *never-configured*. `Degraded` keeps its meaning (is there a problem) so the wire contract is unchanged for every consumer; `TargetAbsent` answers which problem, because the remedies are OPPOSITE — attach any second drive vs reconnect *that* one. Copy routed through `degradedMessageFor` (still one decision point) and taken **verbatim** from the hub's `backup_target_absent` email so banner and mail tell one story. **Offer suppressed on the branch itself**, deliberately not left to `firstOfferableDrive`'s `Disconnected` skip — that flag is set by R-113 in another repo, and this state must be right without it. Red-proof: deleting the branch reproduces E-2d's exact payload, offering `/mnt/felhom-drives/mentes2`, the drive that had vanished. **MinAgent unchanged 0.113.0** — R-114 reads `BackupTarget`/`MountPath`/`GuestPath`/`Role`, none of which R-113 altered, so demo-hp is not held. **NOT live-validated: Scenario C cannot occur on a healthy box.** `resolveBackupTargetState` falls through to the generic degraded branch whenever no disk satisfies `d.BackupTarget && d.MountPath != ""`, never distinguishing **never configured** from **configured and now missing**. Shares R-113's root cause — two disagreeing presence signals — but is a different code path with a different fix. **Currently invisible ONLY because of R-112; fix this before wiring that.** Also seen: after reattach the drive returned as `/dev/sdc` while the stable bind still recorded `/dev/sdb`, and the state read healthy. Evidence: `audits/E2D-fresh-vm-2026-07-29.md` §5.3 | CC |