Files
felhom-controller/REPORT.md
T
2026-08-05 07:37:25 +02:00

12 KiB
Raw Blame History

REPORT — R-204 items 1 & 3 (controller v0.198.0), 2026-08-05

Two of the four steps a customer would have hit alone. The 2026-08-04 R-201 drill passed and only passed because a person was there. Items 1 and 3 are this repo's; item 2 is the hub's (felhom.eu v0.95.0); item 4 remains open (R-193).

1. Baselines, re-read on arrival

Repo Expected Found
felhom-controller f4796e0d00e9 / v0.197.0 exact match, tree clean, HEAD == origin/main

§3.1's landmarks held: claim.go:46 effectiveClaimCode, and the --print-reset-code block in cmd/controller/main.go loading its OWN settings.Load from cfg.Paths.DataDir+"/settings.json" — the separate-process seam the whole item turns on. §3.3's held: mode defaulted to "unit", and the full path was two-step and size-gated.

One drift, in the sibling repo: §3.2 described offsite.go:222-231 carrying a known-consequence comment. That comment had already been rewritten (the R-196 comment-correction commit) and now gave a non-false ground for the mark — the guest-rebuild shape. That made Scenario D the crux rather than a formality; see the hub's REPORT.

2. Per-item outcome

Item Outcome
1 — the reset code needs a restart CLOSED. effectiveClaimCode reads through to the persisted claim state (settings.ReloadClaimCode) before applying the precedence rule, which is unchanged. Read-through only, on the claim path only; fails closed.
3 — the restore's default returns the wrong thing silently CLOSED. restoreScratchOutcomeMsg names what was restored, what was not, and the next step; the wizard card states its scope before the choice. Size gate untouched.
2 — a re-issue marks a healthy escrow stale The hub's half — shipped in felhom.eu v0.95.0.
4 — a rebuilt box cannot obtain an off-site credential unaided REMAINS OPEN. Dependency: the one-shot credential design decision. It needs an operator ruling and belongs to R-193. Not begun.

3. The exact Hungarian strings added (for review as copy)

Unit-restore outcome — with the scratch path resolved (the fallback drops : <path>):

A(z) <app> beállításai és adatbázisa visszaállítva ellenőrző mappába: <path>. A saját fájljaid (dokumentumok, képek, feltöltések) NEM kerültek vissza — ez az ellenőrző visszaállítás csak az alkalmazás beállításait és adatbázisát hozza vissza. Ha a fájljaidra van szükséged, indítsd el a „Teljes visszaállítás előkészítése” lépést ezen az oldalon. A meglévő adatok változatlanok.

Full-restore outcome:

A(z) <app> teljes mentése visszaállítva ellenőrző mappába: <path> — a saját fájljaiddal együtt. A meglévő adatok változatlanok.

Wizard intent card 1 (heading + body, stated BEFORE the choice):

1. Ellenőrzés külön mappába (beállítások és adatbázis) Az alkalmazás beállításait és adatbázisát hozza vissza egy külön ellenőrző mappába. A saját fájljaidat (dokumentumok, képek, feltöltések) nem hozza vissza — azokhoz a 3. pont teljes visszaállítása kell. Az élő adataid nem változnak.

Claim fail-closed message (new — shown on the page and on a refused submission):

A beállító állapot most nem olvasható — próbáld újra néhány perc múlva.

4. Was the restore default changed? NO — and why

The default stays mode=unit. All three wizard forms set mode explicitly, so the mode == "" fallback in offboxRestoreHandler is reachable only by a hand-crafted POST. Changing it would alter nothing a customer sees while silently changing that POST's behaviour — and it would push a mode-less request into the size-gated two-step path. The defect was silence, not the choice of default, and silence is what was fixed: a mode-less POST now gets the same scoped wording, pinned by TestOffboxRestore_DefaultModeGetsTheScopedOutcome. The size gate stays in every case.

5. Files created / modified

File Change
controller/internal/settings/settings.go new ReloadClaimCode() — read-through of the persisted claim fields only
controller/internal/web/claim.go effectiveClaimCode reads through + returns an error; claimGateActive, claimLegacyOpen, handleClaimPage, handleClaimSubmit fail closed
controller/internal/web/claim_readthrough_test.go new — Scenarios A, B and the fail-closed case
controller/internal/web/offbox_handlers.go new restoreScratchOutcomeMsg; the handler calls it with full
controller/internal/web/offbox_restore_scope_test.go new — Scenarios E and F through the real handler
controller/internal/web/restore_wizard_test.go intent-card copy assertion updated to the new scope wording
controller/internal/web/templates/backups_restore_wizard.html intent card 1 states its scope
CHANGELOG.md, CONTEXT.md, controller/README.md v0.198.0 entry; session block; claim-hatch + restore-scope architecture notes

Commits on main: 73b6dbc (item 1) · 2e936f4 (item 3) · 33fcc50 (CHANGELOG). Deployed: gitea.dooplex.hu/admin/felhom-controller:0.198.0 on demo-felhom 9201 and demo-hp 9201.

6. Tests and red-proofs

Green gate after each phase: go build ./... && go vet ./... && go test ./...full suite rc=0; python3 controller/scripts/controller_gates.py --fastall eight gates OK.

Test Result Red-proof — what was mutated Outcome
TestClaimCode_FreshlyMintedByHatch_AcceptedWithoutRestart (A) PASS deleted the ReloadClaimCode() call in effectiveClaimCode FAILED"freshly minted code refused… the running server did not see the persisted code"
TestClaimCode_SupersededByASecondMint_RefusedImmediately (B) PASS substituted a per-instance 30 s TTL cache for the read-through FAILED"the SUPERSEDED code was accepted — a stale/TTL read left an old code alive"
TestClaimCode_UnreadablePersistedState_FailsClosed PASS both mutations above FAILED both times ("the gate OPENED on an unreadable claim state")
TestOffboxRestore_UnitOutcomeNamesWhatItDidNotRestore (E) PASS reverted restoreScratchOutcomeMsg to the single pre-R-204 sentence FAILED on all four assertions
TestOffboxRestore_DefaultModeGetsTheScopedOutcome PASS same FAILED
TestOffboxRestore_FullPathUnchanged (F) PASS same FAILED on the full-outcome assertion; the size-gate half stayed green, correctly
TestOffboxRestore_UnitRestrictsToTheUnitPath PASS — (the mechanism half of E)

Each mutation removed every guard its test covers, except deliberately in the last row: reverting the message does not touch the size gate, and Scenario F's gate assertions correctly stayed green while its wording assertion failed. That split is the evidence the two halves are independent.

A red-proof that first failed for the WRONG reason, corrected and recorded (task §10's exact trap). Scenario B's first TTL mutation used a package-level variable, so test A's earlier read had already opened the window and B failed with "the gate is not active after the first mint" — no code at all, rather than a superseded code surviving. Two fixes: the mutation was made per-Settings (what a real TTL fix would be), and the test was restructured so the server has already seen the first code — through claimGateActive(), the production path a page load takes — before the second is minted. Only then does the TTL demonstrably keep the old code alive.

Seams named. The claim tests stub nothing: a real settings.Load on a real file, the real PrintLocalResetCode against a second *Settings loaded from the same path (the docker exec shape), and submission through fullMux() (RequireAuth + CsrfProtect + ServeHTTP). The restore tests stub only the restic exec (SetOffboxRunner) and drive offboxRestoreHandler itself, asserting the flash on RestoreStatus().Last.Message — the field the wizard renders — and waiting on a real completion marker (!Running && Last.FinishedAt != 0), never a fixed sleep.

7. Live validation (endpoint-level; claude-in-chrome is not available on DooPlex)

# What Observable
1 A freshly minted code is accepted with nothing restarted demo-felhom 9201. Two codes minted through the real hatch (docker exec … --print-reset-code, generations 3 then 4). Container StartedAt 05:21:28Z, RestartCount=0, both mints after it. The current code reached "A jelszónak legalább 12 karakter hosszúnak kell lennie" — a positive observable reachable only after the hash, generation and expiry checks all pass. The same code was then submitted with the real password: 302 → /, accepted end to end.
2 The previously valid code is refused The generation-3 code, superseded seconds earlier, returned „Hibás vagy lejárt kód". No window.
3 A unit restore names what it did not restore demo-hp 9201, app privatebin, via POST /backup/offbox/restore (mode=unit), read back from /api/backup/restore-status: „A(z) privatebin beállításai és adatbázisa visszaállítva ellenőrző mappába: /mnt/sys_drive/felhom-data/backups/offsite-restore/privatebin. A saját fájljaid (dokumentumok, képek, feltöltések) NEM kerültek vissza — ez az ellenőrző visszaállítás csak az alkalmazás beállításait és adatbázisát hozza vissza. Ha a fájljaidra van szükséged, indítsd el a „Teljes visszaállítás előkészítése” lépést ezen az oldalon. A meglévő adatok változatlanok."
4 The full-restore size gate still computes and reveals Same box/app, mode=full without confirm: 302 → /backups/restore/app?name=privatebin&full_prep=privatebin&full_size=6.8+KB, and no restore ran.

Method: authenticated curl against the container IP with the mandatory Host header, run inside the guest; Hungarian strings transported base64 so the ssh → pct exec → bash chain could not mangle an accented assertion.

demo-hp fences respected: no wipe, no ceremony, no re-issue. privatebin was chosen precisely so the drill's calibre-web scratch stayed untouched — confirmed after teardown, it is the only directory left under backups/offsite-restore/.

Why 9201 on demo-felhom could not serve items 34: its off-site repository is orphaned with 0 snapshots (status: error, the known R-193 guest-rebuild shape), so no restore can run there.

8. Teardown

Nothing was provisioned. The outstanding generation-4 reset code on 9201 was consumed through the real claim flow using the box's existing password, so the box is left claimed, healthy and with an unchanged password and no live reset code; re-submitting it now returns „Hibás vagy lejárt kód". The privatebin restore scratch created on demo-hp was removed. All credential-bearing helper scripts were deleted from /tmp on both Proxmox hosts and both guests. No secret appears in any committed file — the minted codes are one-time, now consumed or superseded, and are referenced here only by generation number.

CI: run 153 for 33fcc50 — success. --no-verify was not used; the pre-push gate ran and passed on every push.

9. Observations — noticed, NOT acted on

  • --print-reset-code rewrites the WHOLE settings file from its own settings.Load. If the running server persists a setting between that load and the hatch's save, the hatch's write wins and the server's change is lost. Not triggered here (the hatch is short-lived and operator-driven) and out of scope — but it is a genuine lost-update window on a shared file.
  • The hub's allowedEventTypes still lists escrow_stale, which now has no producer in either repo. Harmless, but inert; removing an allowlist entry is a behaviour change and was left alone.
  • / on DooPlex is at 86% used. Under the 90% abort line, but worth watching before large builds.