# REPORT — controller v0.244.0: the backup page stops promising what it does not hold **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. ## 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. **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. *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). ## 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=``" | | 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" | Each test also carries a negative control: an app whose data really is in the captured volumes keeps its „Adatok" and is not refused. ## Gates `go build ./... && go vet ./... && go test ./...` — green. `controller_gates.py --fast` — all 15 OK. ## 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**.