v0.244.0: the backup page stops promising what it does not hold (R-537/R-538/R-536)
gates / gates (push) Successful in 17s

R-537 — the contents label is now PER TIER. One string computed from the app's
shape was rendered on all three tier rows; a Tier-1 unit has no file-copy step, so
for the four class-A apps it was claiming „Adatok" for files it does not hold.

R-538 — a unit restore REFUSES before anything is touched when the unit cannot
return the app's drive-side files, and names the route that can. It runs before the
stack is stopped because the measured harm included the app's own wastebasket going
unreachable, which still held every byte.

R-536 — „Alkalmazás telepítve" moved from the deploy's acceptance to its completion,
with app_deploy_started and app_deploy_failed as the honest pair.

Each fix red-proofed: seen failing with its own sentence, passing when restored.
Requires hub v0.116.0 for the two new event types. MinAgent unchanged (0.131.0).

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 16:55:55 +02:00
parent 383a30b3c0
commit 2f8ff2414c
16 changed files with 640 additions and 50 deletions
+51 -30
View File
@@ -1,36 +1,57 @@
# REPORT — controller v0.243.0: a real file-manager password, a truthful backup page, no stop for a missing tier (2026-09-15)
# REPORT — controller v0.244.0: the backup page stops promising what it does not hold
Task: *before the volunteer — the big night's P1 fixes*, Parts B, C, E.2 (controller half). **MinAgent: 0.131.0.**
Architecture: `07-backup-architecture.md` §6, `02-controller-module-map.md` (settings after install).
**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.
## R-513 — FileBrowser password
Measured first (B.1): the config key / env var sets the password but re-applies it on every start (a hand-set password
is overwritten); the API (`PUT /api/users?id=…` + `X-Password: <current>`) changes it once. Shipped the API path for
fresh and existing boxes: probe admin/admin → 200: generate, set, verify new=200 and admin=401, store encrypted →
`generated`; 401 → `operator`; unreachable → nothing recorded. App page shows user `admin` + reveal (R-254 rules).
**Live:** 9202 (default) → generated, reveal 200 (16 chars), revealed=200 / admin=401 / wrong=401. 9201 (hand-set) →
first probe failed on DNS before the network join (retried), then `operator` at 08:52:10Z, untouched. Public
`files.enkisfelhom.hu` admin → 401. *One invalid run first (empty credential read) — marked in the evidence.*
## What shipped
## R-517 / R-518 — backup page and tier skip
Per tier: newest success, failed attempt under it, „nincs beállítva" for absent storage; „Naprakész" and the remote tick
from successes only; a tier with absent storage is skipped before anything stops (`backup_tier_skipped`). Button copy
tells the real downtime. **Live on 9201:** local and PBS rows „✓ … Naprakész", remote tick on a real PBS success. Found
live: the top card printed „0 B" for a size read back from storage → fixed to „–" on `main` (d3eacbb), **unreleased**.
Not live-measured: the absent-storage and failed-PBS states (unit-proven; making them on a demo box means removing its
PBS storage).
**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-514 — OOM visibility
`State.OOMKilled` read for running app containers → dashboard „Memória elfogyott" + `app_oom`. **Not proven live:**
in the LXC guest Docker reported neither a restart-OOM nor a child-process OOM (R-528). The task's tag text
„… — újraindítva" was not used: the controller does not restart the app.
**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.
## Red-proofs
PUT removed → admin/admin still logs in; operator branch removed → page does not say who set it; attempt-as-success →
size lost; skip disabled → manual run starts felhom-pbs, scheduled run stops an app; OOM filter inverted → killed worker
hidden; size flag removed → „would print a size it does not know".
*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.
## Delivery
Image `felhom-controller:0.243.0` built from `843b319`. Hub floor for **demo-hp only** 0.243.0 + declared MinAgent
0.131.0 → `managed floor SERVED … from declared`; 9201 on 0.243.0 in 19 s. Scratch 9202 set by hand (its disposition).
demo-felhom not moved (its agent is 0.130.0, the floor would hold). Full suite green before each commit.
**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=`<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" |
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**.