10ca8ac884
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
171 lines
9.4 KiB
Markdown
171 lines
9.4 KiB
Markdown
# REPORT — controller v0.147.0 → v0.147.3: feedback slice 1 (2026-07-19)
|
|
|
|
**Baseline v0.146.0 → LIVE v0.147.3 on demo guest 9201.** Green at every version:
|
|
`go build ./... && go vet ./... && go test ./...` (23 packages) plus `template_id_gate`,
|
|
`emoji_gate`, `native_confirm_gate`, `offbox_rename_gate`, `app_row_dedup_gate`, `mojibake_gate`.
|
|
|
|
> `docker_run_volume_path_gate` fails on `internal/appexport/estimate.go:179`. **Pre-existing on
|
|
> HEAD and untouched by this release** — verified by stashing this work and re-running the gate.
|
|
> Not fixed here (out of scope, and it is someone's call whether that `-v :ro` du probe gets an
|
|
> allowlist entry or a rewrite).
|
|
|
|
## The problem
|
|
|
|
The systemic complaint, twice in one evening: **you press a button and nothing happens.** No
|
|
progress, no ETA, no named result. Three worst offenders fixed on the two patterns already in the
|
|
codebase — the deploy 3-step panel and the storage-init status poll. **No new framework**; that is
|
|
now ROADMAP **R-45**, because three targeted cards were worth shipping tonight and a framework was
|
|
not.
|
|
|
|
Three of the four versions below exist because the cards were watched against **real runs on the
|
|
demo box**. Each follow-up is a thing no unit test would have surfaced.
|
|
|
|
---
|
|
|
|
## 4a — a verification restore names its result
|
|
|
|
The completion flash said the app had been restored „ellenőrző mappába a meghajtón". *Which* folder,
|
|
on *which* drive, was invisible — so the customer could not go and look at the thing they had just
|
|
asked for. And nothing anywhere listed what these restores had accumulated, so copies piled up and
|
|
the only way to find them was SSH.
|
|
|
|
- The flash now carries the **full path**.
|
|
- The restore page gained **„Meglévő ellenőrző másolatok"** — app · size · date · path — each with a
|
|
double-confirmed **„Másolat törlése"** (the house inline `felhomConfirm` idiom, never native
|
|
`confirm()`).
|
|
- `backups/offsite-restore` was open-coded in three places; it now has one home
|
|
(`offsiteRestoreRootFor`). A test pins the path in the flash to the path in the listing, so the
|
|
customer can never be told about a directory the page cannot show or remove.
|
|
|
|
**The delete is the only one this release adds**, so it names a **stack, never a path**: the Manager
|
|
resolves that name inside a root it computed itself and refuses anything landing outside.
|
|
Red-proofed — neutralise the name guard and `stack: ""` resolves to the offsite-restore **root** and
|
|
`os.RemoveAll` takes every copy with it. Every refusal is asserted as a **non-effect** (the
|
|
neighbouring copy and the live data are still on disk afterwards).
|
|
|
|
Honest note: the `HasPrefix` containment check behind that name guard could **not** be red-proofed
|
|
independently — with `isSafeStackName` in front of it, no input the API accepts can reach it with an
|
|
escaping path. It is kept as defence-in-depth and labelled in the test file as unproven-by-test
|
|
rather than dressed up as a red-proof it does not have.
|
|
|
|
### Live (endpoint-exact, through the real handlers)
|
|
|
|
Real verification restore of `calibre-web`, then the guard matrix:
|
|
|
|
```
|
|
message: "A(z) calibre-web visszaállítva ellenőrző mappába:
|
|
/mnt/felhom-drives/hdd_1/backups/offsite-restore/calibre-web
|
|
(a meglévő adatok változatlanok)."
|
|
listing: calibre-web · 430.4 KB · …/offsite-restore/calibre-web · [Másolat törlése]
|
|
|
|
stack=../primary -> refused; copy AND backups/primary both still present
|
|
missing confirm -> refused; nothing changed
|
|
real delete -> copy gone; backups/primary SURVIVES; page -> „Nincs ellenőrző másolat"
|
|
```
|
|
|
|
---
|
|
|
|
## 4b — Megosztás enable shows what it is waiting for
|
|
|
|
Enabling sharing ran `ReconcileSamba()` **synchronously inside the POST handler**. On a box whose
|
|
golden had not baked `felhom-samba` that is `compose up -d` pulling ~100MB from a private registry:
|
|
minutes of an apparently-hung form post, then „Beállítás mentve." whether or not anything came up.
|
|
|
|
Detached + polled now, with a card that distinguishes **„képfájl letöltése"** (image genuinely
|
|
absent — the multi-minute case) from **„indítás"** (already baked — seconds). The distinction is
|
|
decided *before* the work starts, because afterwards the image is always present and the card could
|
|
never truthfully say „letöltés".
|
|
|
|
- Success is **probed, not inferred** — `compose up -d` exits 0 on a crash-loop, so the terminal
|
|
state is container liveness.
|
|
- `nil` from reconcile also covers "deliberately deployed nothing because there is no household
|
|
password yet"; that now gets its own message instead of a card spinning forever.
|
|
- The **password** form starts the same job — with `UserSet` false reconcile deploys nothing, so on a
|
|
fresh box *that*, not the enable toggle, is where the pull actually happens.
|
|
|
|
**Test scope, stated honestly:** the unit tests cover the single-flight slot, snapshot isolation and
|
|
the phase vocabulary. They do **not** drive `runSambaEnsureJob` end-to-end — the Server field is the
|
|
concrete `*stacks.Manager`, not an interface, and introducing one purely for this card was more churn
|
|
than the slice warranted. Pulling-vs-starting and the probed terminal state are covered by live
|
|
validation instead. That seam belongs to R-45.
|
|
|
|
Live: `/sharing/status` returns `{"ok":true,"data":{"phase":"running","running":true,"error":""}}`;
|
|
the page carries the card and the poll.
|
|
|
|
---
|
|
|
|
## 4c — „Távoli mentés most" streams real progress
|
|
|
|
restic was already reporting bytes and percentages; the runner seam used `CombinedOutput()` and threw
|
|
them away. The manual run now passes `--json`, scans stdout line-by-line, and the page shows total
|
|
bytes, percent and the app being pushed. **Manual only** — the nightly stays silent, pinned by a test
|
|
that fails if the scheduled path ever passes `--json` or publishes progress.
|
|
|
|
The poll also now **arms unconditionally**. It used to start only if the page already rendered
|
|
„Fut…", which loses a race the manual trigger always runs: the POST redirects and the page renders
|
|
before the detached goroutine writes `LastStatus=running`, so the poll never armed and the customer
|
|
watched a static page during the very run they had just started.
|
|
|
|
Red-proofed twice: break the parser -> the percent assertion fails; drop the wiring -> the `--json`
|
|
assertion fails.
|
|
|
|
### What three live runs actually taught (v0.147.1 -> .3)
|
|
|
|
**Run 1 — immich reported `0%` for 40+ seconds.** The parser was not broken. On an incremental run
|
|
where nothing changed restic transfers nothing: `bytes_done` is `omitempty`, so it is not even in the
|
|
JSON, and `percent_done` stays 0. Confirmed by capturing the real schema from
|
|
`backup --dry-run --json` on restic 0.14.0 in the controller image rather than guessing; those lines
|
|
are now quoted verbatim in the type's doc comment. **A byte-only bar is indistinguishable from a hang
|
|
in the common case** — shipping it would have traded "no feedback" for "feedback that says 0% and
|
|
looks stuck". -> **v0.147.1** parses and publishes `files_done`/`total_files`.
|
|
|
|
**Run 2 — bookstack showed clean byte progress (100%, 154.0 MB, 7/7), immich sat at `1 of 46 files,
|
|
0 bytes` for 42 seconds.** restic 0.14 only counts a file when it **completes**, so an app dominated
|
|
by one large archive (immich's ~430MB volume tar) freezes *both* counters. No percentage can move.
|
|
-> **v0.147.2** shows what restic *does* keep reporting: the file being processed and elapsed
|
|
seconds. „1 / 46 fájl (430.2 MB) · feldolgozás alatt: immich_upload.tar · 42 mp" is a different
|
|
message from „0%".
|
|
|
|
**Run 3 — the per-app legs finished in ~15s; the remaining 40 of a 57-second run was the shares leg
|
|
and `forget --prune`,** during which the card sat frozen on „calibre-web — 8 / 8 fájl". The same
|
|
silence, relocated to the end of the run. -> **v0.147.3** adds a run **phase**; the post-app stages
|
|
announce themselves and app-scoped counters clear when a phase starts.
|
|
|
|
Final live trace — the whole 57-second run is narrated, no frozen state:
|
|
|
|
```
|
|
t+6s app=immich
|
|
t+9s app=bookstack
|
|
t+12s app=calibre-web
|
|
t+15s phase=shares
|
|
t+18s ... t+54s phase=retention
|
|
t+57s done
|
|
```
|
|
|
|
---
|
|
|
|
## Also: golden/controller infra-image drift closed at the source
|
|
|
|
`infra.Images()` derives the list from the existing pins; `--print-infra-images` prints it; the golden
|
|
bake now asks the controller binary it is about to bake instead of carrying its own copy. That copy
|
|
had already drifted — `felhom-samba` was never added to it, so the golden baked 3 of 4, **which is
|
|
why enabling Megosztás pulled at runtime in the first place**. A test parses the const block out of
|
|
the source with `go/ast` and fails if a pin is added without reaching `Images()`; red-proofed.
|
|
|
|
Verified against the real published image on the live box — all four returned.
|
|
|
|
## Validation method
|
|
|
|
Live validation was **endpoint-exact** (the exact URLs the UI posts to, through the real handlers and
|
|
the real restic/samba paths), not browser click-through: the controller UI is password-gated and I do
|
|
not enter credentials into web forms. No server logic was skipped — only rendering. The rendered
|
|
markup was checked by fetching the pages themselves and asserting on the emitted HTML.
|
|
|
|
## Not done
|
|
|
|
- **Floor not raised** — Viktor decides floor timing.
|
|
- No golden rebuild, no manifest saves, nothing on Peti's arc.
|
|
- No SMB server-side changes — the `\\FELHOM` access issue stays client-side diagnosis first.
|
|
- ROADMAP: **R-45** (unified async-job feedback) and **R-46** (verification-copy browse + expiry)
|
|
registered in `felhom.eu/documentation/backlog/ROADMAP.md`.
|