# REPORT — R-114 + R-112: tell the truth about the backup target, then show it (v0.186.0, 2026-07-29) **Overwritten** per the standing rule. Controller **v0.185.1 → v0.186.0**. Session B of three (A → R-113 agent v0.114.0 shipped-unvalidated · **B → this** · C → one drill rebuild proving all three). `felhom-agent` untouched. **NOTHING HERE IS LIVE-VALIDATED** — see the bottom. ## Baselines felhom-controller `cdaeb369726b` v0.185.1 · felhom-agent `b58d7bcf39a9` v0.114.0 (untouched) · felhom.eu `338b2ccf86`. All clean and pushed at start. **MinAgent decision: unchanged at 0.113.0.** R-114 reads `BackupTarget`, `MountPath`, `GuestPath`, `Role` — **none of which R-113 altered**; it changed `BoundUnderParent`, which this code does not read (`internal/agentapi/client.go:303-338`). Consequence, stated rather than discovered: **demo-hp, still on agent 0.113.0, is not held** and needs no move for this release. ## Part 1 — R-114: the third state `resolveBackupTargetState` had two outcomes — a disk claims the target (healthy), or nothing does (degraded, "the backup is on the system disk"). *Configured, and its drive is gone* had no branch, so it fell into the second and inherited its message **and** its offer. Observed live: `degraded:true, target:"felhom-backup"` + the system-disk copy (false) + `offer_path` naming the drive that had just vanished (`audits/E2D-fresh-vm-2026-07-29.md` §5.3). - New `BackupTargetState.TargetAbsent`. `Degraded` keeps its meaning — *is there a problem* — so the wire contract is unchanged for every consumer; `TargetAbsent` answers *which* problem, because the two have opposite remedies. - Discriminator `targetIsConfiguredDrive`: a real storage id, versus the builtin `local` or an unset tier (both = never configured). - Copy routed through `degradedMessageFor`, so one place still decides what a customer reads. - **Offer suppressed on the branch itself**, not left to `firstOfferableDrive`'s `Disconnected` skip — that flag comes from the agent-side gate in another repo, and this state must be right without it. ## Part 2 — R-112: the state gets a consumer The endpoint was byte-correct and **nothing asked for it** — templates fetch 18 distinct `/api/storage/*` endpoints and these were the only two with zero references (§5.1). Server-rendered on `/backups`, following the existing `SingleCopyWarning` banner at `backups.html:11`. `backupsHandler` → `backupTargetView` → template. **Not a 19th JS fetch**: the state is already resolved when the page is built, and a banner needing JavaScript to appear is one more thing that can silently not happen. `backupTargetView` returns **nil** for healthy and unknown, so a template typo cannot decorate a working box. The offer control POSTs to the existing assign endpoint behind the standard inline confirm, never auto-submits, and surfaces `restart_required` honestly. **Placement:** the E-2 design doc names no surface — `07-backup-architecture.md` has no banner or degraded reference, and the only `E-2` string in the capability map is the unrelated `CAMPAIGN-6E-2`. So the choice is mine: the backups page already hosts a same-severity backup warning and the copy is about `rendszermentés`. ## Files `internal/web/backup_target_offer.go` (third state, view, tiers seam) · `internal/web/server.go` (`tiersFn`) · `internal/web/handlers.go` (one line wiring the view) · `internal/web/templates/backups.html` (banner + offer + assign script) · two new test files · `CHANGELOG.md` · `REUSE.md` · `controller/README.md`. ## Tests: 326 → 338 (+12) in `internal/web`. Suite green (27 packages), both template gates pass. **Scenario E — the seam test, which is the one whose absence let E-2 ship.** It does *not* call the resolver and assert a string (that proves the resolver, which was never broken). It drives `backupsHandler` over `httptest` and asserts the **rendered HTML**, travelling handler → `backupTargetView` → `resolveBackupTargetState` → `degradedMessageFor` → the production `backups` template. | Red-proof | Mutation | Failure text produced | |---|---|---| | 1 | delete the `targetIsConfiguredDrive` branch | `TargetAbsent is false for target "felhom-backup"…` **and** `offered "/mnt/felhom-drives/mentes2" while the configured target is absent — E-2d observed this exact payload offering the drive that had just vanished` | | 2 | drop the absent case from `degradedMessageFor` | `the customer is told the backup is on the SYSTEM DISK, which is false…` + all three copy fragments missing | | 3 | delete `data["BackupTarget"] = …` from the handler | `the absent-drive copy never reached the page…` + every render assertion in the never-configured case | All three reverted; files verified byte-identical after. **Negative assertions:** healthy and unknown are checked for the absence of **all three** copy strings and of the banner element — not for the absence of an error. **A test caught its own imprecision:** the first Scenario-C assertion matched the bare id `backup-target-assign`, which also appears inside the page script that always renders, so it reported a control that was not there. Tightened to the control's own markup. ## NOT LIVE-VALIDATED — awaiting Session C - **Scenario C cannot occur on a healthy box.** The demo guest has a present target, so the absent-drive banner is unproven live by construction. - Untested live: the offer control's click → assign → `restart_required` surfacing. - The live check in this session proves only that the seam is wired and which render a healthy box produces — that is R-112's whole point, but it is not proof of R-114. ## Observations — filed, not fixed 1. **The absent copy now lives in two repos** — here and hub `internal/notify/templates.go:93` — with nothing binding them but `TestAbsentCopyMatchesTheHubEmailWordForWord`, which only fails if *this* side drifts. Reword the hub and the test still passes while the banner and the email disagree. 2. **`resolveBackupTargetState` bypassed the existing `disksFn` seam** and called the client directly, which is why its branching had no test at all before this session. Now routed through `fetchDisks` with a sibling `tiersFn` — worth checking whether other agent-reading paths do the same.