15 KiB
REPORT — v0.154.0 (R-48 restore wizard) + Part 2 docs; Part 3 STOPPED, STOP-1 pending
Session date: 2026-07-21. Executed on DooPlex as kisfenyo.
0. Outcome at a glance
| Leg | Status |
|---|---|
| Part 1 — R-48 restore wizard (controller v0.154.0) | DONE. Committed 3a9d744, pushed, image 0.154.0 built + pushed. Deliberately NOT deployed (by design — the floor save is the deploy). |
| Part 2 — capability-map cell fix (felhom.eu) | DONE. Committed ce8c539, pushed. |
| Part 3 — agent 0.90.1 publish + deploy | STOPPED before publishing — premise does not hold. See §5. Nothing published, nothing deployed, felhom-pve untouched. |
| STOP-1 — floor save + single-fire assertion | PENDING — needs the operator. Preconditions verified green (§2). |
| Phase D — the two post-evidence doc flips | NOT DONE, correctly: both are gated on evidence that does not exist yet (§7). |
1. Baselines (re-confirmed live at session start)
| Repo | main @ start |
Clean + synced | End state |
|---|---|---|---|
| felhom-controller | b30e2e5 |
yes | 3a9d744 (v0.154.0) |
| felhom.eu | fb0b8c1 |
yes | ce8c539 |
| felhom-agent | 8c55ac7 |
yes | unchanged — no commit, no publish, no deploy |
2. Phase-0 probes
P1 — self-update enabled and hub-wired on guest 9201: PASS
From /var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml:
self_update:
auto_update: false
check_interval: 6h
enabled: true
health_timeout_seconds: 60
image: gitea.dooplex.hu/admin/felhom-controller
auto_update: false is not a blocker, and this was verified by reading the code rather than
assumed: MaybeAutoUpdate (internal/selfupdate/updater.go) never consults cfg.AutoUpdate. That
flag is the customer's opt-in to chase latest; the FLOOR path is the managed one and is independent
of it.
Stronger evidence than the config — the path has already fired on this box.
data/update-state.json:
{ "status": "success", "previous_version": "0.143.0", "target_version": "0.145.0",
"initiated_by": "auto-floor", "initiated_at": "2026-07-18T16:32:29Z",
"completed_at": "2026-07-18T16:32:34Z" }
initiated_by: "auto-floor" proves the agent swapper is wired (a nil agent short-circuits with
"no agent — auto-update unavailable") and that a floor-driven swap completes in ~5 s on this box.
Anti-flap will not block the 0.154.0 swap: the persisted target_version is 0.145.0, not
0.154.0, and the in-process lastAutoFloorAttempt is unset for it (the current-≥-floor branch
returns before setting it). Live guest state at probe time: image 0.153.0, Up 15 hours (healthy),
container StartedAt 2026-07-20 15:03:11 UTC.
Registry precondition for the floor validation also verified: 0.154.0 is the highest semver tag
in admin/felhom-controller (23 tags; top four 0.151.0, 0.152.0, 0.153.0, 0.154.0), so the
floor <= latest gate passes rather than deferring.
P2 — agent version: PASS, with a finding that changed the plan
cmd/felhom-agent/main.go:59 holds var version = "0.89.0" as a fallback only; the real version is
ldflags-injected (-X main.version) by scripts/publish-agent.sh. Built at main and verified by
self-report, not by CHANGELOG:
$ felhom-agent --version
felhom-agent 0.90.1
sha256 ba1d029602c96b1b743a4dca3ddbd9b63415fcb47021b91a21f8c32dcc534870 13 734 067 bytes
The R-39 fix commit 9596d5a is an ancestor of main and is not reverted. Agent green gate passes.
The finding is in §5 — this binary does not contain the R-39 fix, and cannot.
P3 — before-state of /backups/restore: CAPTURED
Method: endpoint-level (no browser on DooPlex). Authenticated session against the container IP
with the Host: header, from inside guest 9201. LOGIN_HTTP=302, PAGE_HTTP=200, 58 115 bytes.
Offsite section, buttons in render order — this is the R-48 defect:
immich / bookstack / calibre-web each:
- Visszaállítás ellenőrzéshez (konfiguráció + adatbázis)
- Teljes visszaállítás előkészítése
immich additionally (scratch prepared):
- Helyreállítás az élő adatok közé (csak a hiányzó fájlok) <- data CANNOT come back
- Teljes visszaállítás (fájlok + adatbázis) <- data CAN come back
Form actions on the page: 6× /backup/offbox/restore, 1× place, 1× reconstitute. The two decisive
controls are adjacent siblings differing only by label. Snapshot retained at
scratchpad/restore-before.html.
3. Part 1 — what shipped (commit 3a9d744)
Files touched: internal/web/restore_wizard.go (new), templates/backups_restore_wizard.html (new),
internal/web/restore_wizard_test.go (new), templates/backups_restore.html, offbox_handlers.go,
server.go, templates/style.css, plus CHANGELOG.md / CONTEXT.md / REUSE.md /
controller/README.md.
- One entry per app. The five inline forms per row collapse to a single „Visszaállítás…" link to
GET /backups/restore/app?name=<app>. - Three intent CARDS with consequence sentences, danger styling on card 3, the R-43 double-confirm and its pair-honesty facts carried over verbatim.
deriveWizardStepis pure over (op running, size-gate flash, scratch ready). Precedence is strict and load-bearing: a running op outranks a stale?full_prep=.- No new mutation endpoint. One GET route added; every card posts to the pre-existing
/backup/offbox/{restore,place,reconstitute}with unchanged field names and gates. - Untouched as instructed: the shares block, the local restore panel, the .fab block,
internal/backup,internal/appbackup,internal/selfupdate.
Bug found and fixed on the way (not in the spec). offboxRedirectTo hardcoded "?" when
appending its flash. Retargeting redirects at a URL that already carries ?name=<app> would have
produced ...?name=immich?flash=..., burying the flash inside the name value — the wizard would
then have refused its own app with "nincs kijelölve" after every action. The separator is now chosen.
Test results
Full suite green: go build ./... && go vet ./... && go test ./... — 0 failures.
All six controller design gates pass (template_id, emoji, mojibake, native_confirm,
offbox_rename, app_row_dedup).
| Group | Test | Result |
|---|---|---|
| B | TestDeriveWizardStep_Table (7 rows) |
PASS |
| C | TestResolveWizardApp_Refusals, TestRestoreWizardHandler_UnconfiguredRedirects |
PASS (302, no 500) |
| A | TestRestoreList_SingleEntryPerApp |
PASS |
| C | TestRestoreWizard_ThreeIntentCards, TestRestoreWizard_NoScratchLocksDataIntents |
PASS |
| E | TestRestoreWizard_OpRunningSuppressesAllMutations |
PASS |
| D | TestRestoreWizard_NoNewMutationEndpoints, TestRestoreWizard_FieldContract |
PASS |
Two pre-existing tests were coupled to the old IA and were updated, not deleted:
TestAppRow_RestoreLists and TestBackupsSplit_SectionsOnExactlyOnePage asserted
action="/backup/offbox/restore" on the list page — now inverted to assert those forms are absent
there and the single wizard entry is present.
Group-B red-proof (run, then reverted)
Replaced the body of deriveWizardStep with the trivial return restoreWizardView{Step: wizStepIntent, VerifyEnabled: true}. Result — all 7 table rows FAILED, plus the execution render
test:
--- FAIL: TestDeriveWizardStep_Table/op_running_(this_app)_→_execution;_nothing_offered
--- FAIL: TestDeriveWizardStep_Table/op_running_OUTRANKS_a_stale_full_prep_flash…
--- FAIL: TestDeriveWizardStep_Table/scratch_ready_→_intent,_and_BOTH_data-touching_intents_unlock…
--- FAIL: TestDeriveWizardStep_Table/full_prep_flash_for_THIS_app_→_prepare-confirm…
(+3 more rows)
--- FAIL: TestRestoreWizard_OpRunningSuppressesAllMutations
restore_wizard_test.go:253: the execution step must render NO mutation form
restore_wizard_test.go:256: the execution card must name what is actually running
Implementation restored; suite green; git diff clean before commit.
One assertion in TestRestoreWizard_ThreeIntentCards was itself caught being hollow during
authoring: a bare substring check for the skew/empty warnings passed on a clean pair, because the
confirmFullRestore JS repeats both sentences as string literals. Tightened to assert the rendered
banner markup and the data-restore-* attributes instead.
Image
gitea.dooplex.hu/admin/felhom-controller:0.154.0 built + pushed, 145 MB. Self-report verified:
felhom-controller 0.154.0 (built 2026-07-21T06:31:08Z, commit 3a9d744)
commit matches HEAD. Not deployed to 9201 — by design; the floor save is the deploy.
4. Part 2 — capability-map cell (ce8c539)
Row 61's status cell read **PARTIAL** — *scope corrected 2026-07-19* while its own note body already
ended with the 2026-07-20 destructive drill and the words "the row now earns PROVEN-LIVE". Cell
corrected to **PROVEN-LIVE** (2026-07-20); the full note history retained verbatim.
5. Part 3 — STOPPED. The premise is wrong; publishing would encode a false claim.
Part 3 asked me to publish + deploy v0.90.1 as "R-39's cheap half — the PBS wrapper argv fix", while Rule 12 forbids touching the wrapper. Those two cannot both be satisfied, because the fix IS the wrapper. Evidence, in order:
(a) v0.90.1 contains zero non-test Go changes. git show 9596d5a --stat:
CHANGELOG.md | 51 +
REPORT.md | 184 +-
configs/felhom-pbs-apply | 11 +- <- the wrapper: the actual fix
internal/pbsdr/manager_test.go | 48 + <- the red-proof test
Its own commit message says so outright: "Config-only (wrapper + red-proof); the Go binary is unchanged, so this ships with the next agent deploy as a config artifact." The 0.90.1 binary is functionally identical to the 0.90.0 already running — only the ldflags string differs.
(b) felhom-pve already has the fix. The wrapper was hotfixed in place on 2026-07-18:
/usr/local/sbin/felhom-pbs-apply:107: args=(--fingerprint "$fp") <- fixed (no --server)
/usr/local/sbin/felhom-pbs-apply.bak-20260718-preR39 <- rollback retained
(c) New hosts already get the fix too, independent of the binary version.
felhom-host-install.sh:1914 fetches configs/felhom-pbs-apply via fetch_raw, which pulls from
raw/branch/main — unversioned. 9596d5a is an ancestor of main, so every install since
2026-07-18 has installed the fixed wrapper.
Conclusion. R-39's cheap half is already closed in the field, through a channel that carries no version number. Publishing + deploying the 0.90.1 binary would:
- deliver no functional change to felhom-pve;
- restart the agent on a production host at a remote site for that nothing;
- and, once the operator saved Day-0 manifest Agent → 0.90.1 in the same sitting (as STOP-1
contemplates), advertise that the R-39 fix shipped as a versioned artifact — which it never did.
A fresh box gets the fixed wrapper from
mainwhether it installs binary 0.90.0 or 0.90.1, so the version number would be a false signal about fleet state.
Per §0 ("if anything requires touching agent source → STOP and report") and the Part-3 STOP rule, I
stopped before the publish. Nothing was published. Nothing was deployed. felhom-agent has no
new commit and felhom-pve is byte-for-byte as found. The built binary sits unpublished in the
session scratchpad; the registry pre-check (the diagnosed state) was confirmed and left as-is:
GET .../generic/felhom-agent/0.90.1/felhom-agent -> 404 (authed and anonymous)
GET .../generic/felhom-agent/0.90.0/felhom-agent -> 200 (control)
This needs an operator ruling — see §8.
6. STOP-1 — not yet run (needs the operator)
Preconditions are green (§2 P1) and the target image is published and verified. The assertion is ready to run the moment Viktor saves the floor; I have not observed a swap, so no R-23(a) evidence exists yet and the map row is untouched.
7. Doc flips deliberately NOT made
- R-23(a) → banked: requires STOP-1 single-fire evidence. Does not exist. Not flipped.
- "config/state change round-trips in seconds" PARTIAL → PROVEN-LIVE: same gate. Not flipped.
- R-48 → SHIPPED and the R-39 artifact note in
ROADMAP.md: not written this session. R-48's is merely deferred until the live click-through; the R-39 line is deferred because the sentence the task specified would have been false (§5).
8. Remaining — human
- Ruling on Part 3 (blocking). Options as I see them: (i) publish 0.90.1 anyway as a
version-hygiene artifact, with the CHANGELOG stating plainly that the binary is unchanged and the
R-39 fix rode the wrapper — cheap, but the Day-0 manifest still implies more than it delivers;
(ii) leave 0.90.0 as the published binary and close R-39's cheap half in the docs, recording that
the wrapper is delivered unversioned from
main; (iii) treat "a privileged host artifact tracksmainwith no version" as the real defect and spec that as TASK-B alongside R-39's primary half. Recommendation: (ii) now, (iii) as the actual fix — (iii) is what stops a root-owned wrapper from silently trackingmain, which is the same class of problem R-39 already demonstrated once. - STOP-1: floor save 0.153.0 → v0.154.0, password-gated. I observe and assert single-fire.
- Wizard click-through + one non-destructive Ellenőrzés through the new surface.
- TASK-B ruling still pending (R-39 primary half: the descriptor-hash re-apply gap).
- C6 customer-restore pass for the alpha.
9. Observations
docker_run_volume_path_gate.pyfails atHEAD, unrelated to this work.internal/appexport/estimate.go:179has an unrevieweddocker run -v. Verified pre-existing by stashing my changes and re-running (still rc=1). Left alone — out of scope — but it means that gate is currently not a usable signal.- The
?-vs-&redirect bug (§3) is the kind the wizard makes more likely, since app-scoped pages now carry query strings as a matter of course.offboxRedirectTo's REUSE.md row now carries the trap. felhom-pveis up and healthy despite the vacation note inCLAUDE.mdsaying the agent is down:systemctl is-active→active,felhom-agent 0.90.0. The F1 static-.162mitigation is holding; theCLAUDE.mdTEMPORARY block is stale on that point.- P3's accented-grep trap fired as documented. ASCII-safe substrings were required; an accented
pattern through
ssh → pct exec → bash -creturned a false 0. Used a local Python pass over the fetched bytes instead of grepping remotely.