v0.204.0 — the restore list is keyed on the store (R-237); the size gate stops refusing in silence (R-238)
gates / gates (push) Successful in 26s
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.
This commit is contained in:
@@ -1,101 +1,91 @@
|
||||
# REPORT — felhom-controller v0.203.0 (+ felhom-agent v0.127.0)
|
||||
# REPORT — v0.204.0: the restore list is keyed on the store (R-237), and the size gate stops refusing in silence (R-238)
|
||||
|
||||
**The two dead ends that kept the recovery journey failing are closed in code.** R-218's consume half
|
||||
and R-220 (both halves: the host-side detection and the customer-facing refusal).
|
||||
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.
|
||||
|
||||
## 1. R-218 — which retry shape, and why (§7.1)
|
||||
## R-238 — classified: a HARNESS ARTIFACT, with a real residue that is fixed
|
||||
|
||||
**Poll**, on a 5-minute scheduler tick, driven from the box's own published declaration
|
||||
(`OffboxReportStatus().State`) — **the very statement the hub acts on**, so the two cannot disagree
|
||||
about whether a retry is wanted.
|
||||
The two runs diverge at one parameter, and the state at that point is quoted rather than inferred:
|
||||
|
||||
**Decided on Scenario B, measured against the actual promises:**
|
||||
- `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.
|
||||
|
||||
| surface | what it promises |
|
||||
**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 |
|
||||
|---|---|
|
||||
| the no-target unlock message | „…**amint megvannak**, a mentéseid listája megjelenik…" — **no deadline** |
|
||||
| the backups card | „…**Ha egy napon belül nem áll be**, jelezd az üzemeltetőnek." — **within a day** |
|
||||
| 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. |
|
||||
|
||||
A 5-minute tick is inside both by a wide margin, and it needs **no hub change** (which §0 expected).
|
||||
**No text needed changing.** If either promise ever tightens to minutes, the ACK-driven shape is the
|
||||
one to reach for.
|
||||
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.
|
||||
|
||||
**It stops by construction** (Scenario C) and **the settle gate is kept** (Scenario D) — the retry goes
|
||||
through `ReconcileWhenSettled`, unchanged.
|
||||
`felhom-offbox` and `_shares` are excluded from the app list.
|
||||
|
||||
## 2. The marker question (§7.2) — answered, and left alone
|
||||
## Tests
|
||||
|
||||
`applied_marker` lives at `<DataDir>/offbox/applied_marker` — **inside the guest's data dir, which a
|
||||
rebuild destroys.** It therefore cannot suppress a legitimate post-rebuild re-run, and it is not part
|
||||
of this defect. Untouched.
|
||||
`go build` · `go vet` · `go test ./...` → **28 packages ok**. `controller_gates.py --fast` → **9/9 OK**.
|
||||
|
||||
## 3. R-220's shape (§7.3) — corroboration, not a wider prefix
|
||||
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).
|
||||
|
||||
**Candidate (b)**, the claimed check distinguishing our own mount from a foreign one — because the host
|
||||
and its knowledge survive the rebuild while the guest's registry does not. Candidate (a) (a cleanup
|
||||
that clears the raw mounts) would have made correctness depend on a step running, and a step that does
|
||||
not run leaves exactly today's defect.
|
||||
## Red-proofs — each mutation asserted to have applied before the result was trusted
|
||||
|
||||
**The discriminator is corroborated:** a non-managed mountpoint is forgiven **only when the same device
|
||||
is ALSO mounted under `/mnt/felhom-drives`** — a pairing only our enrolment produces.
|
||||
**`/proc/mounts`, not `lsblk MOUNTPOINTS`**, because the lsblk invocation is pinned verbatim in the
|
||||
sudoers file and changing it would have coupled this fix to a config rollout.
|
||||
| # | 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" |
|
||||
|
||||
**The fence holds and has its own test** (Scenario F): `/srv/data`, `/media/photos`,
|
||||
`/mnt/someone-elses-disk`, `/var/lib/other` are all still refused. **Fail-safe:** an unreadable mount
|
||||
table corroborates nothing.
|
||||
All three restored; suite green again afterwards.
|
||||
|
||||
## 4. Every changed Hungarian string
|
||||
## Files
|
||||
|
||||
**The deploy refusal (R-220, Scenario G).** Was:
|
||||
- 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`
|
||||
|
||||
> „A megadott tárhely nem azonosítható regisztrált meghajtóként, ezért alkalmazás adatkönyvtáraként nem
|
||||
> használható. **Válasszon a listából csatlakoztatott meghajtót.**"
|
||||
## Not done here, deliberately
|
||||
|
||||
Now:
|
||||
|
||||
> „A megadott tárhely nem azonosítható regisztrált meghajtóként, ezért alkalmazás adatkönyvtáraként nem
|
||||
> használható. **Ha a gépet nemrég telepítettük újra, a meghajtóid megvannak, de még nincsenek újra
|
||||
> csatlakoztatva ehhez a géphez — a Tárhely → Meghajtók oldalon csatlakoztathatod őket, és utána
|
||||
> indítsd újra a telepítést. Ha ott sem látszanak, keresd a Felhom ügyfélszolgálatát.**"
|
||||
|
||||
**No other customer-facing string changed.** The NAS refusal is a different situation and is untouched.
|
||||
|
||||
## 5. Tests and red-proofs — every one demonstrated failing, then restored
|
||||
|
||||
| Scenario | Test | Red-proof mutation | Result |
|
||||
|---|---|---|---|
|
||||
| **A** | a credential staged after start-up is collected | make `RetryIfDeclared` return `(false, nil)` — the pre-v0.203.0 world | **FAILS**: "the box declared a need and no reconcile was attempted" |
|
||||
| **C** | a healthy box does nothing, and logs nothing | drop the `!declared()` stop condition | **FAILS**: a healthy box reconciles |
|
||||
| **D** | the settle gate holds on the retry path | call `Reconcile` instead of `ReconcileWhenSettled` | **FAILS**: "SETTLE GATE BYPASSED" |
|
||||
| **E** | the customer's own drive is offered after a rebuild | remove the `felhomOwnedMounts` exemption | **FAILS**: "device is mounted at /mnt/adatok (sdb)" |
|
||||
| **F** | a genuinely foreign mount is still refused | over-widen to any `/mnt/*` path | **FAILS**: "/mnt/someone-elses-disk was offered for formatting" |
|
||||
| **G** | an empty list explains itself | restore the old sentence | **FAILS**: "the refusal tells the customer to choose from a list that may be empty" |
|
||||
|
||||
Plus: a nil bridge is a silent no-op; the corroboration requires both mounts of the **same** device,
|
||||
and another device's managed mount does not vouch for a foreign one.
|
||||
|
||||
**Green:** controller **28 packages ok**, agent **29 packages ok**, both `go vet` clean, all gates OK
|
||||
in both repos.
|
||||
|
||||
## 6. Live checks (§7.4)
|
||||
|
||||
**Deployed to demo-hp** from the **published** bytes (agent sha verified against the artifact before
|
||||
installing): agent **0.127.0**, controller **0.203.0**, healthy.
|
||||
|
||||
**Scenario C — PROVEN LIVE, with a positive control.** The retry job is registered
|
||||
(`Registered periodic job: offsite-credential-retry (every 5m0s)`), **ran once**
|
||||
(`Running job: offsite-credential-retry` = 1) and produced **zero** log lines on a box whose tier is
|
||||
configured. The positive control matters: without it, silence could have meant the job never fired.
|
||||
*(A first reading counted 2 lines and looked like noise; they were the START-UP reconcile's settle-gate
|
||||
lines, not the retry job — the instrument was corrected before the conclusion.)*
|
||||
|
||||
**Scenarios A and E are NOT live-proven yet, and deliberately so.** Both need a *rebuilt* box: A needs
|
||||
one declaring `needs_credential` (no target), E needs one whose registry lost its drives while the raw
|
||||
mounts survived. Contriving either on a demo box means dismantling its real off-site state; the state
|
||||
arises **naturally in Part 4**, which is where they belong and where the last re-walk measured them.
|
||||
|
||||
## 7. Not changed
|
||||
|
||||
No hub change. No sudoers, no allowlisted command, no PVE surface. The settle gate, the marker, every
|
||||
other claim signal, and the NAS refusal are untouched.
|
||||
- **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.**
|
||||
|
||||
Reference in New Issue
Block a user