# 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`.