53e9bf0224
gates / gates (push) Successful in 26s
R-237: /backups/restore listed apps that are CURRENTLY DEPLOYED and CURRENTLY TOGGLED ON for future off-site backups. A rebuilt box has neither, so a household that had just lost everything was shown nothing to restore while the repository held their snapshots — measured live on the R-201 re-walk. To restore an app you had to select it, to select it you had to have installed it, and to know what to install you had to see the backup you could not see. The store is now the source of the list (offsite_restore_list.go), built on the existing R-193 OffsiteInventoryList. Installed-ness became a property OF a row, never a filter on it. Every case is answered rather than hidden: a snapshot for an app that is not installed is offered and says it will reinstall first; an installed app with no snapshot is shown as having nothing; an unreadable store renders as UNKNOWN (R-225's rule, one screen over) AND keeps the action, because "we could not look" is not "there is nothing"; no-target is its own state. The felhom-offbox and _shares marker tags are excluded from the app list. R-238 classified as a HARNESS ARTIFACT: mode=full without confirm=1 is step 1 of a deliberate two-step — it starts no job by design and redirects carrying &full_prep=<app>, which deriveWizardStep requires to reveal the commit. A driver that did not carry it forward landed back on the intent step. The operator's browser run completed the same restore. The wizard's precedence rules were NOT re-keyed: a stale ?full_prep= must never resurrect a commit button mid-restore. The residue WAS real and is fixed: neither branch of that step wrote anything to the log, so a refusal — including by the headroom gate — left no trace on the box. Both branches now log, and so does the concurrent-op refusal. resolveWizardApp is removed: it was dead once the gate moved, and its test pinned the defect's behaviour (an untoggled app refused), which would have read as policy. 28 packages ok, 9/9 gates OK. Three red-proofs, each asserted to have applied.
92 lines
5.6 KiB
Markdown
92 lines
5.6 KiB
Markdown
# REPORT — v0.204.0: the restore list is keyed on the store (R-237), and the size gate stops refusing in silence (R-238)
|
|
|
|
2026-08-06. Controller **v0.203.0 → v0.204.0**. MinAgent unchanged (**0.127.0**) — nothing here needs
|
|
a new agent capability. `felhom-agent` untouched.
|
|
|
|
## R-238 — classified: a HARNESS ARTIFACT, with a real residue that is fixed
|
|
|
|
The two runs diverge at one parameter, and the state at that point is quoted rather than inferred:
|
|
|
|
- `POST /backup/offbox/restore` with `mode=full` and **no** `confirm=1` is **step 1 of a deliberate
|
|
two-step** (`offbox_handlers.go:314`). It computes size + headroom via `OffboxRestorePrepareFull`,
|
|
**starts no job**, and redirects to
|
|
`restoreWizardPath(app) + "&full_prep=<app>&full_size=<size>"`.
|
|
- `deriveWizardStep` (`restore_wizard.go`) reveals the commit **only** when
|
|
`in.FullPrepApp == in.App`, sourced from `?full_prep=`.
|
|
- The endpoint-level driver posted step 1 and then re-fetched the wizard **without** that parameter.
|
|
The pure function therefore returned the **intent** step — correctly. `restore-status.last == null`
|
|
is likewise **correct**: step 1 starts no job by design.
|
|
- The operator's browser run followed the redirect, saw the confirm, pressed „Igen" twice, and the
|
|
restore completed.
|
|
|
|
**So the button is not dead, and the wizard was not re-keyed.** The precedence rules exist so a stale
|
|
`?full_prep=` can never resurrect a commit button mid-restore, and they were left alone.
|
|
|
|
**The residue, which is real whoever triggers it:** neither branch of step 1 wrote anything to the
|
|
log. `offboxRedirectTo` only flashes to the page. A customer refused a disaster restore — **including
|
|
a refusal by the headroom gate** — left **no trace on the box at all**. Fixed: the refusal logs
|
|
`[WARN] … full-restore preparation REFUSED for <app> (no job started): <err>`, the success logs
|
|
`[INFO] … full-restore prepared for <app> (size N) — awaiting the customer's confirm; no restore has
|
|
started`, and the concurrent-op refusal logs too.
|
|
|
|
## R-237 — the restore list, rebuilt on the store
|
|
|
|
`buildOffsiteRestoreRows` (new, pure) merges `OffsiteInventoryList` (the repository's own snapshot
|
|
tags — the existing R-193 reader) with the installed set. `resolveOffsiteRestoreApp` replaces
|
|
`resolveWizardApp`'s toggle requirement.
|
|
|
|
Every §7 case, and the Hungarian as rendered:
|
|
|
|
| case | rendered |
|
|
|---|---|
|
|
| snapshot present, app not installed | restore offered + „Nincs telepítve — a visszaállítás előbb újratelepíti." |
|
|
| installed, no snapshot | „Nincs mentése a távoli tárolóban — nincs mit visszaállítani." |
|
|
| store unreadable | „Nem tudjuk elolvasni a távoli tárolót, ezért **nem tudjuk, mi van benne**. Ez nem azt jelenti, hogy üres — próbáld újra később, vagy jelezd az üzemeltetőnek." + „Nem tudjuk, van-e mentése — a tárolót nem sikerült elolvasni." **and the action stays offered** |
|
|
| no target yet | „A távoli tároló kapcsolódási adatai még nem érkeztek meg ehhez a géphez, ezért még nem tudjuk megmutatni, mi van benne. Ez magától rendeződik." |
|
|
| store empty | „A távoli tároló üres — nincs mit visszaállítani." |
|
|
| app under a different name | **not guessed** — it lists under the tag the store holds, and if nothing is installed under that name the row says so. No fuzzy matching. |
|
|
|
|
Wizard refusals also changed: „Ehhez az alkalmazáshoz nincs mentés a távoli tárolóban." and, when the
|
|
store could not be read, „Nem tudjuk elolvasni a távoli tárolót, ezért nem tudjuk, van-e benne mentés
|
|
ehhez az alkalmazáshoz." Both log an INFO naming the app and the store state.
|
|
|
|
`felhom-offbox` and `_shares` are excluded from the app list.
|
|
|
|
## Tests
|
|
|
|
`go build` · `go vet` · `go test ./...` → **28 packages ok**. `controller_gates.py --fast` → **9/9 OK**.
|
|
|
|
New: `offsite_restore_list_test.go` (7 tests — the truth table, the rebuilt box, unreadable-is-unknown,
|
|
no-target, empty, and two rendered-page tests) and `offbox_restore_silence_test.go` (handler-level,
|
|
because the silence was in the handler).
|
|
|
|
## Red-proofs — each mutation asserted to have applied before the result was trusted
|
|
|
|
| # | mutation | result |
|
|
|---|---|---|
|
|
| RP-1 | store rows dropped from the builder — the list keyed back on installed apps (the original defect) | `TruthTable` + `RebuiltBox_SeesItsSnapshots` **FAIL** |
|
|
| RP-2 | `state` forced to `known` **and** `StoreUnknown` forced false (both guards) | `UnreadableStoreIsUnknownNotEmpty` + `NoTargetIsItsOwnState` **FAIL** |
|
|
| RP-3 | both size-gate log lines removed | `FullPrepareRefusal_IsNotSilent` **FAIL** — „wrote NOTHING to the log" |
|
|
|
|
All three restored; suite green again afterwards.
|
|
|
|
## Files
|
|
|
|
- new `internal/web/offsite_restore_list.go`, `internal/web/offsite_restore_list_test.go`,
|
|
`internal/web/offbox_restore_silence_test.go`
|
|
- `internal/web/restore_wizard.go` (gate on the store, not the toggle)
|
|
- `internal/web/offbox_handlers.go` (three log lines)
|
|
- `internal/web/handlers.go` (wire the rows)
|
|
- `internal/web/templates/backups_restore.html` (the list + the state wording)
|
|
- `internal/web/backups_split_test.go` (fixture: the new data contract)
|
|
- `internal/backup/offbox_inventory.go` (`ErrNoOffsiteTargetSentinel`, so the no-target case is
|
|
constructible from another package's table test)
|
|
- `CHANGELOG.md`, `controller/README.md`
|
|
|
|
## Not done here, deliberately
|
|
|
|
- **R-236 is diagnosed, not fixed** — the fix's shape depends on the diagnosis, and this arc has twice
|
|
shipped a fix aimed at the wrong half of a defect.
|
|
- The R-193 unlock listing still shows the `felhom-offbox` marker as if it were an app. Noticed while
|
|
reusing its reader; **out of scope and not touched.**
|