b331f18424
Two defects E-2d found on a real box, fixed in this order deliberately: the
message is corrected BEFORE it is put on screen, because switching on a banner
that lies is worse than a silent one.
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"). The state "configured, and its drive is gone" had no branch, so
it fell into the second and inherited its message AND its offer. Observed live
with the target detached: degraded:true, target:"felhom-backup" plus the
system-disk copy (false -- the backup was on a drive that had vanished) plus
offer_path naming that same vanished drive as the remedy.
New BackupTargetState.TargetAbsent discriminates. 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 --
attach any second drive, versus reconnect THAT one. Copy routed through
degradedMessageFor so one place still decides what a customer reads. The offer
is suppressed on the branch itself, NOT left to firstOfferableDrive's
Disconnected skip: that flag is set by the agent-side gate in another repo
(R-113), and this state must be correct independently of it.
R-112 — the state finally has a consumer. The endpoint was byte-correct and
nothing in the product ever asked for it: templates fetch 18 distinct
/api/storage/* endpoints and backup-target[/assign] were the only two with zero
references. Server-rendered on /backups now, 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. The offer control POSTs to the existing assign endpoint behind the standard
inline confirm, never auto-submits, and surfaces restart_required honestly
instead of adding a self-restart.
Scenario E (the seam test) drives backupsHandler over httptest and asserts the
RENDERED HTML -- handler -> view -> resolver -> template. It deliberately does
not call the resolver and assert a string, which would prove the resolver that
was never broken. Deleting the one line that sets data["BackupTarget"]
reproduces the R-112 state and fails every render assertion.
Tests 326 -> 338 (+12) in internal/web; suite green (27 packages); both template
gates pass. Three red-proofs run and reverted, files byte-identical after.
MinAgent 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). demo-hp on agent 0.113.0 is not held.
The absent copy is verbatim the hub's customerMessages["backup_target_absent"]
so the banner and the email tell one story -- filed as a two-repo drift risk,
not solved.
NOT LIVE-VALIDATED. Scenario C cannot occur on a healthy box; Session C proves it.
97 lines
6.2 KiB
Markdown
97 lines
6.2 KiB
Markdown
# 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.
|