REPORT: v0.245.0 — the recovery-code ask, red-proofs and the two-box live validation
gates / gates (push) Successful in 15s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-16 21:20:31 +02:00
parent ad398b60d9
commit 714d5bce09
+45 -39
View File
@@ -1,57 +1,63 @@
# REPORT — controller v0.244.0: the backup page stops promising what it does not hold
# REPORT — controller v0.245.0: the household is asked for the recovery code
**2026-09-16.** Three defects the 2026-09-16 drill measured on a fresh box, all in the same family:
the product said a thing that was not true about a customer's data.
**2026-09-16.** R-543. Off-site backup is ON by default and does not RUN until the household creates
its recovery code. **The pause is the design and is untouched here** — the escrow is zero-knowledge,
their code is the only key, and a run without one would write a copy nobody could ever open. What
was missing is that nothing asked them, while the page promised the very copy that had never run.
## What shipped
**R-537 — the label is now per tier.** `BackupContents` was one string, computed from the app's shape
(`HasHDDData || HasVolumeData → "Adatok"`) and rendered on the Tier-1, Tier-2 and Tier-3 rows alike.
One string cannot be true for three tiers that capture different things: a Tier-1 unit holds compose
+ app.yaml + DB dumps + volume tars and has **no file-copy step at all** (`CaptureRecoveryUnit`;
`RecoveryManifest` has no field for one), so for the four class-A apps the customer's own files are
carried by Tier 2 and Tier 3 and by nothing else. The row now carries `Tier1Contents`,
`Tier23Contents` and `DriveFilesNote`, and the template renders the right one per row.
**The reminder bar.** While the off-site tier is configured and its escrow state is not `escrowed`,
every authenticated page carries „A távoli mentés szünetel, amíg nem hozod létre a helyreállítási
kódot." linking `/backup/escrow`. It is the **R-241 bar, second instance** — same session-cookie
dismissal („Most nem"), back at the next visit, gone for good when escrowed. No second banner system
was built.
**R-538 — the restore refuses instead of lying.** `RestoreFromRecoveryUnitAt` now returns
`*ErrUnitLacksFileLegs` **before the lock, before the stack is stopped and before any volume is
replaced**, when the app declares drive-side file legs the unit cannot hold. The web handler turns
that into a Hungarian sentence naming the route that CAN return the files — the off-site wizard's
„Teljes visszaállítás (fájlok + adatbázis)", or the second drive's „Fájlok visszaállítása" — and says
plainly when no copy exists. `UnitRestoreOptions{AcceptMissingFiles}` is the explicit, separately
worded second step; it is a per-call argument and never a field on the Manager.
*Where it hangs, and why it matters:* `executeTemplate` (server.go), the single render choke point,
not the three `addRecoveryBanner` call sites. A per-handler helper would have covered the pages
someone remembered — the seam-built-but-never-wired shape this repo has shipped four times. The
login and claim pages render through `ExecuteTemplate` directly and never pass through it; a session
check (`hasAdminSession`) additionally keeps it off the public guest share page.
*Why the refusal runs that early:* in the measured failure the replayed database also stopped
referencing the app's own wastebasket, which still held every byte on the drive. A refusal that has
already stopped the app would have destroyed the customer's last route while declining to help.
**R-536 — „telepítve" now means installed.** The API emits `app_deploy_started` beside its 202;
`app_deployed` moved to the async path's own end via `stacks.SetDeployDoneHook`, and
`app_deploy_failed` (warning) replaces the silence an interrupted install used to get. The
accept-time `app.yaml` is deliberately kept on failure: it is the crash-safe record written with
`Deployed:false`, it holds the settings the customer typed, and the state every surface reads is
`not_deployed`.
Also folded in: the pending „0 B" whole-system tile fix (R-517 follow-up).
**The tier-1 sentence renders by state.** v0.244.0 fixed the label (R-537) and put a new promise in
its place: „a távoli másolat … védi", printed from the app's shape alone. `driveFilesNoteFor` now
takes `tier3State`'s own vocabulary — `active` → „védi"; `escrow_pending` → „**védené** — a távoli
mentés a helyreállítási kód létrehozásáig szünetel" + the route; no off-site and no second drive →
„nincs másolat" + both ways out. One source of state, so the row and the sentence cannot disagree.
## Red-proofs — each seen failing, then passing
| fix | break | what failed |
|---|---|---|
| R-537 label | restore the app-shaped label | „the Tier-1 label claims it holds the app's data: `Konfig + Adatok`" |
| R-538 refusal | disable the guard | „a restore that cannot return the files must refuse; got err=`<nil>`" |
| R-536 accept-time | put `NotifyAppDeployed` back beside the 202 | „the deploy handler announces an INSTALLED app at accept time" |
| R-536 completion | remove the success-path hook | „the deploy ended and nothing was told about it" |
| the bar | delete the one `addEscrowBanner` line from `executeTemplate` | „/dashboard does not tell the household the off-site copy is PAUSED" — **and the same on /launcher** |
| the sentence | compute it from the app's shape, as v0.244.0 did | „the sentence claims the files ARE protected while the copy is paused", quoting the exact v0.244.0 wording |
Each test also carries a negative control: an app whose data really is in the captured volumes keeps
its „Adatok" and is not refused.
Negative controls: an escrowed box is not nagged; an unconfigured box is not nagged; an app with no
drive-side files gets no sentence in any state. The banner fixture asserts `OffboxConfigured()`
itself, so it cannot pass by silently losing its precondition.
## Live validation (endpoint-level; no browser on DooPlex)
Both boxes ran 0.245.0. Requests ran **inside** the guests (the controller answers on the container
address with the mandatory `Host` header; neither guest is reachable from DooPlex).
- **Escrowed (9201):** bar absent on /dashboard, /launcher, /backups/apps; the row reads „védi" (×2).
- **Paused (9202):** off-site configured through `POST /backup/offbox/config` → `escrow_state=pending`
with both secret files on disk. Bar present on /dashboard, /launcher, /backups/apps and /settings.
`POST /backup/offbox/run` → „A távoli mentés a kulcs letétbe helyezésére vár.", `last_run=None`,
`snapshot_count=None` — refused by the fork-4 gate, **not** by an unreachable target. A throwaway
class-A app's row rendered „…védené … szünetel" with „védi"=0. Dismissal sets a cookie with no
Max-Age and no Expires; same visit 0 hits, next visit 1 hit.
- **Teardown, three layers:** app removed with data and backups (0 containers, 0 drive folders); the
off-site target cleared and its three secrets `shred -u`'d (verified by re-reading); temp files
gone on guest and host; the hub provisioned nothing (9202 does not report).
## Gates
`go build ./... && go vet ./... && go test ./...` — green. `controller_gates.py --fast` — all 15 OK.
`go build ./... && go vet ./... && go test ./...` — green. `controller_gates.py --fast` — 15/15 OK.
CI run 671 for the previous push (`ad398b60`) — **success**, matched by head_sha.
## Requires
Hub **v0.116.0**, which registers `app_deploy_started` / `app_deploy_failed` in `allowedEventTypes`
and `customerMessages`. Against an older hub those two POSTs 400 and the events are simply absent;
`app_deployed` keeps working. MinAgent unchanged at **0.131.0**.
Hub **v0.116.0** (unchanged from v0.244.0). MinAgent unchanged at **0.131.0**. No agent change, no
hub change, no installer change, no golden.