diff --git a/CONTEXT.md b/CONTEXT.md index 87ca03e..29f27f8 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1776,6 +1776,35 @@ Last updated: 2026-06-13 (v0.60.0 backlog-Medium cleanup) --- +## CAMPAIGN 11 — what changed in v0.201.0 (2026-08-05) + +**The off-site key recovery is a COUPLED feature and now declares it.** It needs agent **0.125.0** +(`POST /escrow/recover-offsite-password`). `FeatureOffsiteKeyRecovery` has a `featureProbes` row, a +`featureMinAgent` row and a `Supports` gate at the unlock entry point. + +⚠ **That gate FAILS CLOSED — alone in that table.** The package default is fail-open, and that default +is what produced R-216: an agent that could not answer 404'd, the unlock was attempted anyway, and the +customer was told their correct recovery code was wrong. Anything but `SupportYes` now says *the +machine* cannot ask yet, and no attempt is made. Do not "fix" it back to the package default. + +**The box declares `needs_credential` until the TIER WORKS, not until a key exists.** The old +short-circuit on "a repository password is present" is deleted: installing one is the recovery +screen's whole job, so it made succeeding at recovery switch off the mechanism that delivers the +coordinates to use it. A disabled target still short-circuits at the first line (Scenario E). + +**The unlock finishes the job**: place the key → bring the tier up (`offsiteapply.Bridge.Reconcile`, +wired via `SetRecoveryTierUp`) → list. Without the middle step the promised listing can never render on +shape (a), because no key ⇒ no target ⇒ no inventory. + +**Four messages, not one.** Wrong code (the only one mentioning typing) · the machine cannot ask · +the store could not be read / the connection details have not arrived · the code belongs to a RETAINED +earlier package. The last one is driven by the ACK's `superseded_present`/`superseded_at` (hub +v0.97.0) and **promises nothing** — no read path for a superseded package exists. + +**Still open from the campaign:** R-214 (console pairing banner), R-220 (drives unenrollable after a +rebuild), R-221 (a rebuilt box cannot run the escrow ceremony), R-223 (the Day-0 manifest still vouches +agent 0.120.0 — operator decision). + ## R-203 (v0.197.0) — the namespace-root contract, and what `ok` now means **The contract, in one line:** `appbackup`'s path helpers (`UserdataDir`, `PrimaryBackupPath`, diff --git a/REPORT.md b/REPORT.md index 28d75c0..38527ac 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,222 +1,144 @@ -# REPORT — R-193: the recovery screen (controller v0.200.0), 2026-08-05 +# REPORT — CAMPAIGN 11 fix pass: a correct recovery code is never called wrong again -**A customer whose machine was rebuilt can now find out, and unlock their backups, without a command -line.** R-193's remaining half. No hub change; `felhom.eu` carries registers and docs only. +**2026-08-05 · controller `v0.201.0` · commit `a3499d1`** (paired with hub `v0.97.1`) -## 1. Baselines, and §3's five pieces +Campaign 11 walked the customer's recovery journey end to end for the first time. **The data came back +byte-identical; the journey did not exist.** This pass closes six of the nine findings. -`felhom-controller` at `be3c5fa7f6a1` / v0.199.0 — **exact match**, tree clean, `HEAD == origin/main`. -`felhom.eu` at hub v0.96.0, unchanged. **All five pieces of §3's table were where it said**, at the -cited landmarks: `RunRecoveryCheck` (`:55`), `RecoverAndInstall` (`:140`), `offboxResetHandler` -(`:245`) with its `confirm=1` gate and orphaned precondition, `OffboxOrphaned` (`:244`), and the -per-app snapshot lookup (`offbox_restore.go:84`). **No mechanism was invented.** +## Scenario A and its red-proof — the session's headline -**Register:** the highest ID was **R-212** as stated, and still is — re-checked at the end for the -second writer. This session mints **R-213**. +Measured live on the campaign venue 2026-08-05: agent 0.120.0 answers +`POST /escrow/recover-offsite-password` with 404, the unlock was attempted anyway, and a **correct** +recovery code came back — in **0.134 s**, far too fast for `age`'s scrypt — as: -## 2. The one thing the task under-specified, and what I did about it +> *„A megadott helyreállítási kódot nem fogadtuk el. Ellenőrizd, hogy mind a tíz szót pontosan, +> szóközökkel elválasztva írtad be…"* -§8.1 says the page appears on the same two facts the box uses to declare `needs_credential`: **a fresh -data area** (no repository password) and **a hub-held package**. Taken literally that is a window that -**closes by itself**: `WriteOffboxSecrets` AUTO-GENERATES a repository password on re-apply — which is -R-193's own orphaning mechanism — and since hub v0.96.0's credential self-heal the re-apply now happens -unaided within ~15–30 minutes. The customer who logs in the next morning would never have seen the page. +**RED-PROOF.** Deleting the `if support := s.recoverySupport(...)` block from `recoveryUnlockHandler`: -So the predicate is **the hub holds a package AND (no repository password OR the tier is orphaned)**. -Shape (b) is the state a real rebuilt box is in, and it is also the state the shipped move-aside -requires — which is what lets "I do not want the old data" reach the existing handler instead of -needing a new one. **Scenario B is unaffected:** a healthy box has its own key and is not orphaned; a -box that never had off-site backups fails the package condition. **demo-felhom is in shape (b) right -now**, which is why validation 1 needed no arrangement at all. +``` +--- FAIL: TestRecoveryGate_A_OldAgentIsNamed_NeverTheCode/agent_predates_the_route_(404) + R-216 RETURNED: an agent that cannot answer is still reported as a wrong recovery code +--- FAIL: TestRecoveryGate_A_OldAgentIsNamed_NeverTheCode/agent_cannot_be_asked_at_all + R-216 RETURNED: an agent that cannot answer is still reported as a wrong recovery code +``` -## 3. Every Hungarian string, quoted for review as copy +The accusation returns verbatim, on the assertion that names the finding. Restored; green. -**The two MANDATORY sentences of §8.2** — both verified in the live rendered page: +*(The first attempt at this red-proof failed the test for weaker reasons — the fake recoverer +succeeded regardless of the agent verdict, so the accusation could not appear. The fixture now models +the real agent: one that cannot answer the route also fails the call.)* -> **A helyreállítási kódot senki nem tudja pótolni** — sem a Felhom, sem az ügyfélszolgálat, sem az -> üzemeltető. Ez szándékos: így a mentéseidet rajtad kívül senki nem tudja megnyitni. Ha a kód -> elveszett, a korábbi mentések nem nyithatók meg többé. +## The four messages, quoted for review as copy -> Ha megadod a kódot, **feloldjuk a mentéseid zárolását és megmutatjuk, mi van bennük** — melyik -> alkalmazás, mikorról, mekkora. **Ebben a lépésben semmit nem állítunk vissza és semmi nem változik.** -> A visszaállítást utána, alkalmazásonként külön választhatod. +1. **The code did not open it** — the only one that mentions typing: + > „A megadott helyreállítási kódot nem fogadtuk el. Ellenőrizd, hogy mind a tíz szót pontosan, + > szóközökkel elválasztva írtad be — a kis- és nagybetűk nem számítanak. Semmi nem változott, + > nyugodtan próbáld újra." +2. **The machine cannot ask** (R-216): + > „Ez a gép még nem tudja megnyitni a mentéseidet — a hozzá tartozó házon belüli szolgáltatás + > régebbi, mint amit ehhez a lépéshez használunk. A kódoddal semmi baj, és nem is használtuk fel: + > tedd el biztonságos helyen. A gép magától frissül; próbáld újra később, vagy szólj a Felhom + > ügyfélszolgálatának, ha egy nap múlva sem működik." +3. **The store could not be read** (R-217) — two shapes: + > „A kulcs visszakerült, de a mentések listáját most nem sikerült beolvasni. A mentéseid nincsenek + > veszélyben — nézd meg a Biztonsági mentés oldalt néhány perc múlva." + > „A kulcs visszakerült, és biztonságban van. A gép még várja a házon kívüli tárhely kapcsolódási + > adatait — amint megvannak, a mentéseid listája megjelenik a Biztonsági mentés oldalon. Nincs + > teendőd." +4. **A retained earlier package** (R-222) — states two facts and promises nothing: + > „Ez a kód nem nyitja meg azt a csomagot, amit most őrzünk ehhez a géphez. Ha egy korábbi kódot + > adtál meg: a géped azóta új mentési kulcsot kapott, és a régebbi csomagot (…) nem töröltük — + > megőrizzük. Megnyitni viszont innen egyelőre nem lehet, ezért ha a régebbi mentéseidre van + > szükséged, keresd a Felhom ügyfélszolgálatát. A kódoddal semmi nem történt, és semmi nem + > változott." -**The situation, before any code:** +**Live proof of message 4**, on the exact box and the exact code that produced the defect in Phase 3 +step 7: rendered verbatim with the real supersession timestamp `2026-08-05 15:03:14`, and +`blames typing = False`. -> Ezt a gépet újratelepítették. A korábbi, **házon kívüli mentéseid megvannak** — a Felhom központi -> rendszere őriz hozzájuk egy lezárt csomagot, amelyet **<dátum>** zártunk le. A csomagot csak a -> **te helyreállítási kódoddal** lehet kinyitni. +## Part 1b — the shape chosen, and why nobody types a number twice -**The form:** `Helyreállítási kód (tíz szó)` · placeholder `tíz szó, szóközökkel elválasztva` · -buttons `Mentések feloldása` / `Most nem`. +`ResolveManagedFloor` compared the box's agent against `ArtifactManifest.MinAgent`, which by its own +doc comment describes **the golden's** controller, while `publish-train-rules.md` rule 3 states the +rule about **the floor's**. They are the same number only while the floor sits at or below the golden. -**What "most nem" means:** +**Chosen: a floor ABOVE the vouched golden is HELD**, with its own reason. **No new operator input at +all** — the vouched MinAgent keeps its exact meaning and the guard simply stops applying it to a +version it does not describe. The rejected alternative was a second "floor MinAgent" field, which +would have the operator type the same CHANGELOG number twice for the same release. -> A „Most nem" csak azt jelenti, hogy nem zavarunk vele többet a kezdőlapon. A mentéseid ettől -> megmaradnak, és ez az oldal a **Biztonsági mentés → Távoli mentés** oldalról bármikor újra elérhető. +Live, immediately after deploy: -**A wrong code** (quoted from the LIVE response): +``` +managed floor HELD for c11: held: floor 0.200.0 is ABOVE the vouched golden 0.192.0, so its agent +requirement is unknown — vouch a golden carrying the floor's controller (publish-train rule 1) +``` -> A megadott helyreállítási kódot nem fogadtuk el. Ellenőrizd, hogy mind a tíz szót pontosan, -> szóközökkel elválasztva írtad be — a kis- és nagybetűk nem számítanak. Semmi nem változott, nyugodtan -> próbáld újra. +**⚠ What it does NOT do:** with the guard corrected and the Day-0 agent unchanged, a new box is +**held, not served**. It stops being lied to; the feature works for it only after R-223. -**Refused (a different key is already present):** +**A slip caught by the deploy itself, and fixed in v0.97.1:** the first deployed build logged +`agent "0.125.0" < MinAgent 0.113.0` for a box held for the NEW reason — a comparison that is false. +That is `CLAUDE.md`'s corollary (a verdict that changes which field it counts from must change its +alarm text). Both surfaces now come from one `ManagedFloorDecision.HoldReason()`, pinned by a test. -> Ezen a gépen már van egy másik mentési kulcs. A régi előzmény visszanyitása felülírná azt, ezért nem -> hajtottuk végre. Vedd fel a kapcsolatot a Felhom ügyfélszolgálatával. +## R-218's live proof — stated with its limit -**After the unlock:** +**Proven by test + red-proof**, not yet by a live rebuild-shaped run. Restoring the deleted +short-circuit fails `TestOffsiteDeclare_StillDeclaresAfterARecoveredKeyIsPlaced` with *"the box went +SILENT after recovering its key while still having no off-site target"*. -> Ezek a **te** mentéseid, a lent jelzett időpontokból. Nézd át, hogy tényleg azt találod-e itt, amire -> számítasz — **semmit nem állítottunk vissza és semmi nem változott.** +**The live half is deliberately not taken**: reproducing the stranded shape means reconfiguring the +campaign venue, and §11 forbids that because Phase 2 needs it as it is. What IS verified live on the +venue is Scenario E's side — the configured tier is silent — plus the whole R-222 ACK chain end to +end (hub 0.97.1 → ACK → controller 0.201.0 → `settings.json`: `hub_escrow_superseded_present: True`, +`hub_escrow_superseded_at: 2026-08-05 15:03:14`). -> A mentéseid zárolása feloldva. Az alábbiakat találtuk a tárolóban — semmit nem állítottunk vissza. +## Tests and red-proofs -Table headings: `Alkalmazás` · `Legutóbbi mentés` · `Méret`. Next step: +| Scenario | Test | Red-proof — what was mutated | Result | +|---|---|---|---| +| A | `TestRecoveryGate_A_OldAgentIsNamed_NeverTheCode` | the capability gate deleted | **FAILED as required** | +| A | `TestRecoveryGate_A_SupportedAgentProceeds`, `_CouplingIsDeclared` | — | pass | +| B/C | `TestResolveManagedFloor_R216_FloorAboveGolden` (5 sub-cases) | the floor-above-golden branch deleted | **FAILED as required** | +| — | `TestResolveManagedFloor_HoldReasonMatchesTheCause` | — | pass | +| D | `TestOffsiteDeclare_StillDeclaresAfterARecoveredKeyIsPlaced` | the short-circuit restored | **FAILED as required** | +| E | `TestOffsiteDeclare_ConfiguredTierIsSilent`, `_DisabledTargetIsNotStranded` | — | pass | +| F | `TestRecoveryGate_F_UnlockBringsTheTierUpBeforeListing`, `_TierNotUpYetSaysPendingNotFailed` | — | pass | +| G | `TestRecoveryGate_G_UnreadableStoreNeverClaimsToHaveOpened`, `_ThreeDistinctStates` | the zero-value inventory restored | **FAILED as required** | +| H | `TestRecoveryGate_H_SupersededPackageIsNamed`, `_WithoutASuperseded…` | — | pass | +| I | `TestRecoveryGate_I_DirectGetRefusedOnABoxThatNeverHadBackups`, `_StillWorksWhenOffered` | the direct-GET gate deleted | **FAILED as required** | -> A visszaállítás alkalmazásonként történik, a **Biztonsági mentés → Visszaállítás** oldalon. Ott -> választhatod ki, melyik alkalmazás mit hozzon vissza. +Green gates: `go build ./... && go vet ./... && go test ./...` rc=0 in both repos, read separately +from every commit. `controller_gates.py --fast` and `repo_gates.py --fast` both all-OK. The `-run` +filter was proven to match (13 `=== RUN` lines) rather than trusted. -**An EMPTY store, stated plainly (§8.6):** +## Deployed -> A tároló megnyílt, de **nincs benne egyetlen mentés sem**. Ez azt jelenti, hogy a kulcs jó volt, de -> ehhez a géphez nem tartozik korábbi mentés. Ha korábban biztosan készültek mentések, keresd a Felhom -> ügyfélszolgálatát, mielőtt bármit tennél. - -**Unlocked but no coordinates yet** (the pristine rebuilt shape, added after a test found the crash): - -> A kulcs visszakerült. A gép még most kapcsolódik újra a házon kívüli tárhelyhez — a mentéseid listája -> néhány perc múlva jelenik meg a Biztonsági mentés oldalon. - -**The exceptional path — first mention, then the SECOND confirmation:** - -> Ha a helyreállítási kódod véglegesen elveszett, és tudomásul veszed, hogy a korábbi mentések így nem -> nyithatók meg többé: *nem kérem vissza a korábbi adatokat*. - -> **Biztosan nem kéred vissza a korábbi mentéseket?** Ha megerősíted: -> • a korábbi mentéseket **félretesszük — nem töröljük**; -> • a helyreállítási kód nélkül **többé nem lesznek megnyithatók**; -> • a gép **új, üres mentési tárolót kezd**, és mostantól oda ment; -> • ez az oldal **többé nem jelenik meg**. -> Ha csak most nincs kéznél a kódod, válaszd inkább a „Most nem" lehetőséget. - -Button: `Igen, félretehetitek a korábbi mentéseket` / `Mégsem`. - -**The permanent entry point in the backups area:** - -> **A korábbi, házon kívüli mentéseid visszaszerezhetők.** Ezt a gépet újratelepítették, és a Felhom -> központi rendszere őriz hozzá egy lezárt csomagot. A helyreállítási kódoddal feloldhatod a korábbi -> mentéseidet, és megnézheted, mi van bennük. → `Adatok visszaszerzése` - -## 4. §8.4 — repeated failures: LOG LOCALLY, DO NOT ALERT. The reasoning. - -**No lockout**, as instructed. On repeated failures I chose to **log locally and raise no operator -signal**, and the reasoning is: - -- The realistic generator of repeated failures is **the owner mistyping a ten-word phrase**, which is - the single most likely event on this page. An alert there is noise on every real recovery. -- The threat it would guard against — someone guessing R — is not credible: a 10-word EFF phrase is - ~129 bits, and an attacker who could reach this page already holds the household password. -- **Silence is not total:** every failure writes a `[WARN] [web] recovery: unlock failed: …` line - carrying the agent's step and no code, so it reaches the debug ring and any operator log pull. A box - being probed is visible; it just does not page anyone. - -The deliberate residual: a slow, patient prober produces only ring entries nobody reads unprompted. -That is accepted, and stated here rather than left implied. - -## 5. Files created / modified - -| File | Change | +| | | |---|---| -| `internal/backup/offbox_recovery_cli.go` | **new** `RecoverInstallCore` + its three outcomes; `RecoverAndInstall` reduced to a wrapper (exit codes and printed lines unchanged) | -| `internal/backup/offbox_inventory.go` | **new** — the read-only listing, the explicit empty case, `ErrNoOffsiteTarget`, `HumanizeBytes` | -| `internal/backup/offbox.go` | **new** `OffsiteRecoveryOffer` (the two-shape predicate) | -| `internal/settings/settings.go` | **new** `RecoveryNoticePostponed` + accessors | -| `internal/report/escrow_confirm.go` | **new** `SealedAt()` — the ACK's escrow `created_at` | -| `internal/web/recovery_handlers.go` | **new** — the page, unlock, postpone, the seams | -| `internal/web/templates/recovery.html` | **new** — the full page | -| `internal/web/{server,handlers,funcmap}.go`, `templates/backups_remote.html` | routes, the landing-page interception, the permanent entry point, `humanBytes` | -| `cmd/controller/main.go` | wires `SetEscrowSealedAt` | -| `internal/web/recovery_test.go`, `recovery_wiring_test.go` | **new** — Scenarios A–H + three AST seam tests | +| controller `0.201.0` | campaign venue (c11 guest 9201) **and** demo-felhom guest 9201 — both `healthy` | +| hub `0.97.1` | GitOps: manifest bump → ArgoCD hard-refresh + deliberate sync → `Synced Healthy`, rollout complete, image verified | +| agent | **untouched** — v0.125.0 is what R-223 asks the operator to vouch, not what this changed | -**Commits on `main`:** `636c51e` (implementation) · `62b85ec` (CHANGELOG). **Deployed:** -`gitea.dooplex.hu/admin/felhom-controller:0.200.0` on demo-felhom 9201 and demo-hp 9201. +CI: controller task **171** (`a3499d18`) success; felhom.eu tasks **172–175** success. No +`--no-verify`; every pre-push gate ran and passed. -## 6. Tests and red-proofs +## Still open, deliberately -Green gate: `go build ./... && go vet ./... && go test ./...` — **full suite rc=0**; -`controller_gates.py --fast` — **all eight OK**. +**R-214** (console pairing banner), **R-220** (drives unenrollable after a rebuild — the deploy refuses +and the wizard's list is empty), **R-221** (a rebuilt box cannot run the escrow ceremony at all). +R-221 in particular is a real blocker for a different flow. **R-213** and **R-202** untouched. -| Scenario | Test | Result | Red-proof — what was mutated | Outcome | -|---|---|---|---|---| -| A | `TestRecovery_A_PageAppearsForARebuiltBox` | PASS | — | — | -| B | `TestRecovery_B_DoesNotAppearForAnyoneElse` (3 sub-cases) | PASS | dropped the hub-package condition from `OffsiteRecoveryOffer` | **FAILED** — *"a box that never had off-site backups was offered a recovery screen for data it never had"* | -| C | `TestRecovery_C_UnlockOpensAndLists` + `…EmptyStoreSaysSo` | PASS | — | — | -| D | `TestRecovery_D_WrongCodeFailsClosedAndIsKind` | PASS | — | — | -| E | `TestRecovery_E_PostponeKeepsTheEntryPoint` | PASS | bound the backups-page entry point to `recoveryInterrupts` instead of `recoveryOffer` | **FAILED** — *"the backups page no longer offers the route… a customer who clicked past it once has lost the way to their own data"* | -| F | `TestRecovery_F_SetAsideNeedsTwoConfirmations` | PASS | removed the `ConfirmSetAside` gate in the template | **FAILED** — *"the set-aside form is on the FIRST view — one click would set the customer\'s history aside"* | -| G | `TestRecovery_G_PageAndCLIShareOneCore` + `TestRecoveryHandlerDrivesTheSharedCore` | PASS | gave the handler its own inline fetch→compare→install | **FAILED** — *"recoveryUnlockHandler does NOT call backup.RecoverInstallCore"* | -| H | `TestRecovery_H_CodeLeavesNoTrace` | PASS | (carries its own positive control) | — | -| I | `TestCLIWrapperDrivesTheSharedCore`, `TestRecoveryRoutesAreWired` | PASS | — | — | +## Observations, not acted on -**Scenario E's assertion was STRENGTHENED mid-session** because the first version asserted only -`recoveryOffer()`, which the mutation did not touch — it would have passed against the very defect it -names. It now asserts the **rendered** backups page contains `href="/recovery"`, and only then did the -mutation fail. That is the red-proof-that-passes trap, caught and closed rather than reported as green. - -**Two real defects the tests caught before shipping:** - -1. **An UNCLAIMED (legacy-open) box would have been shown the page.** `RequireAuth` passes a - password-less box through, so `ServeHTTP`'s interception fired for an unauthenticated visitor. - Fixed by an explicit `authEnabled()` check in `recoveryOffer`. -2. **The inventory nil-dereferenced when no off-site target was configured** — exactly the pristine - rebuilt shape. Now a named error with its own honest customer message. - -## 7. Live validation (endpoint-level; `claude-in-chrome` is not available on DooPlex) - -| # | What | Observable | -|---|---|---| -| 1 | **The page rendered on a genuinely stranded box — no arrangement needed** | demo-felhom 9201 is really in shape (b) (`orphaned=True, status=error, snapshots=0`). `GET /launcher` → **`302 Location: /recovery`**; `GET /recovery` → 200 with the code form; both MANDATORY sentences present in the rendered bytes (transported base64 so the ssh→pct chain could not mangle an accented assertion). | -| 2 | **A healthy box shows no page** | demo-hp 9201 (`orphaned=False, status=ok, 6 snapshots`, image `0.200.0`): `GET /launcher` → **no redirect**, and the backups page carries **0** occurrences of `href="/recovery"`. | -| 3 | **A wrong code is refused, nothing written, no lockout** | Three consecutive wrong codes → `200` each time, the form still present each time, the Hungarian above rendered, **0** occurrences of `age:` / `passphrase` / `unseal` in the page, and the `offbox/` directory listing **byte-identical before and after**. The controller log shows the failure came from the AGENT (`POST /escrow/recover-offsite-password: HTTP 400: the recovery code did not open the sealed bundle`), i.e. the whole chain — handler → agent → hub fetch → age KDF — ran live. | -| 4 | **The correct code unlocking + the listing** | **NOT RUN LIVE, and it cannot be here.** No recovery code was kept for demo-felhom's orphaned history (`RECON-offsite-dr-chain-2026-08-04.md` §12.3: *"no recovery code was kept"*), and demo-hp's drill code is operator-held out of band. It rests on `TestRecovery_C_UnlockOpensAndLists` (install + listing + "restores nothing"), `…EmptyStoreSaysSo`, and `TestRecovery_G_PageAndCLIShareOneCore` (installed → unchanged on re-run). | -| 5 | **The code persists nowhere — with the positive control** | 0 hits in every file under the data volume, 0 in the container log, 0 in the debug ring. **The positive control earned its keep:** the first sweep ran against the CONTAINER path, which does not exist in the guest, so its "0 hits" proved nothing — the planted copy was NOT found, which exposed the mis-aimed sweep. Re-run against the real path: plant found (**1**), removed (**0**), and the real sweeps stayed at 0. | - -**Teardown:** nothing was arranged, so nothing needed reversing — demo-felhom is exactly as found -(`recovery_notice_postponed` unset, offbox enabled, `repo_state=orphaned`, repository password -present). Helper scripts removed from both guests. - -## 8. Registers - -- **R-193 → CLOSED** (credential half v0.199.0/hub v0.96.0; the screen v0.200.0), with the two - operator rulings recorded and the shape-(b) reasoning stated. -- **R-213 → NEW**: putting files back in place, with the **live-versus-backup comparison** named as its - requirement. Deliberately not started. -- **R-202 stays open**, untouched. -- The register's highest ID was R-212 on arrival **and still is** — re-checked for the second writer. - -## 9. The capability-map row as it now reads - -All four drill interventions closed **and** the customer is now offered the recovery. The old -*"needs someone who knows to look"* qualifier is **gone**. What remains, stated narrowly: (1) the final -unlock has never been driven with a **correct** code through the page; (2) **putting files back is -deliberately not part of this** (→ R-213); (3) **the journey has not been re-walked end to end** since -these fixes — one more drill is what is owed. - -## 10. CI and hooks - -CI green by `head_sha` for both pushes. **`--no-verify` was NOT used**; every push ran the pre-push -gate, including the workspace-root assertion added yesterday. - -## 11. Observations — noticed, NOT acted on - -- **The postpone flag is never cleared.** If a box leaves and re-enters the stranded state, a customer - who once chose „most nem" will not be interrupted again. The entry point still shows, so the route - is intact; clearing it on a transition is a judgement call I did not make unasked. -- **`/backup/offbox/reset` redirects to `/backups/remote`, not back to the recovery page.** Correct — - the situation has ended — but the customer lands on a page whose recovery banner has just - disappeared, with no explicit "done" message. Worth a copy pass with the put-back work. -- **`RunRecoveryCheck` was left alone**, as instructed, so it still contains its own fetch+compare. It - is compare-only and writes nothing, so it cannot diverge dangerously — but it is now the ONE recovery - path not routed through `RecoverInstallCore`. +1. **`SupportsWithSource` looks up `featureProbes` FIRST and returns `SupportUnknown` on a table gap, + before the version path runs.** So `FeatureBackupAgeState`, which has a `featureMinAgent` row and no + `featureProbes` row, can never be decided by version — it is always Unknown. This fix works around it + by registering a probe that returns a sentinel; the ordering itself is untouched (out of scope). +2. The recovery screen renders `SealedAt` as a raw RFC3339 UTC string (`2026-08-05T13:11:11Z`) to a + Hungarian household; `recoverySealedAt()` is documented as "the human date". +3. The escrow wizard's form says *„tíz szó"* while the issued code is hyphen-joined, and the + wrong-code message says *szóközökkel elválasztva* (space-separated). Both forms should be accepted + or the copy aligned; not investigated.