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

159 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) **&lt;app&gt;** beállításai és adatbázisa visszaállítva ellenőrző mappába: **&lt;path&gt;**. 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) **&lt;app&gt;** teljes mentése visszaállítva ellenőrző mappába: **&lt;path&gt;** — 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 --fast`**all 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.