Files
felhom-controller/REPORT.md
T

15 KiB
Raw Blame History

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.
  • deriveWizardStep is 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/mainunversioned. 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 main whether 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

  1. 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 tracks main with 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 tracking main, which is the same class of problem R-39 already demonstrated once.
  2. STOP-1: floor save 0.153.0 → v0.154.0, password-gated. I observe and assert single-fire.
  3. Wizard click-through + one non-destructive Ellenőrzés through the new surface.
  4. TASK-B ruling still pending (R-39 primary half: the descriptor-hash re-apply gap).
  5. C6 customer-restore pass for the alpha.

9. Observations

  • docker_run_volume_path_gate.py fails at HEAD, unrelated to this work. internal/appexport/estimate.go:179 has an unreviewed docker 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-pve is up and healthy despite the vacation note in CLAUDE.md saying the agent is down: systemctl is-activeactive, felhom-agent 0.90.0. The F1 static-.162 mitigation is holding; the CLAUDE.md TEMPORARY 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 -c returned a false 0. Used a local Python pass over the fetched bytes instead of grepping remotely.