278 lines
15 KiB
Markdown
278 lines
15 KiB
Markdown
# 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`:
|
||
|
||
```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`:
|
||
|
||
```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/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 `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-active` → `active`, `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.
|