## Changelog ### docs — the workflow moved to DooPlex-local execution (2026-07-19) **Docs only, no version bump, no code change.** Claude Code now runs on DooPlex (192.168.0.180, Debian 13, `kisfenyo`) instead of the Windows workstation, working directly in `/mnt/5_hdd/felhom.eu/git/felhom-controller`. Builds are local commands; felhom-pve is one SSH hop. - `CLAUDE.md` — environment/access table rewritten (local DooPlex + `ssh felhom-pve`, no `$SSH` variable), build/deploy commands de-SSH'd, workspace-root pointer now `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`. - **New clean-tree gate** in the build section: `git status --porcelain` empty AND `HEAD` == `origin/main` before any build — because the CC working tree IS the tree `build.sh` builds from. An unpushed change does not exist. - `claude-in-chrome` is **not available** on DooPlex — endpoint-level validation ("invoke the exact endpoint the UI invokes") is now the stated standard method; strict UI coverage is a manual pass. - Windows knowledge is preserved, not deleted: a "Legacy: Windows workstation" note in `CLAUDE.md` and `RUNBOOK-e2e-live-drive.md`, and `docs/vscode-ssh-fix.md` carries a LEGACY banner. Historical Windows references in past CHANGELOG/REPORT entries are left untouched — history is history. Platform-genuine mentions (the `_other.go` dev stubs, `\\FELHOM` shares in Windows Explorer, the Windows-grep multibyte rationale in the gate scripts) are unchanged. ### v0.147.3 — 4c follow-up 3: the run does not end with the last app (2026-07-19) Third real run, third thing only a live run could show. The per-app legs finished in ~15 seconds; the remaining **40 of the 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 frozen-looking silence 4c exists to remove, relocated to the end of the run. Progress now carries a **phase**. The post-app stages announce themselves („Megosztott mappák mentése…", „Karbantartás: régi mentések rendezése a távoli tárolón…") and the app-scoped counters are cleared when a phase starts, so the last app's finished numbers are never shown against work that is no longer about that app. Starting the next app clears the phase again. Pinned by a test. ### v0.147.2 — 4c follow-up 2: when NO counter can move, say what is being worked on (2026-07-19) The v0.147.1 file-count fallback fixed the incremental case but not the one the demo box actually hits. Watching a second real run: **bookstack** reported clean byte progress (100%, 154.0 MB, 7/7 files — the byte path works), while **immich** sat at `files_done 1 of 46`, `bytes_done 0`, for 42 seconds. restic 0.14 only counts a file into `bytes_done`/`files_done` when it **completes**, so an app dominated by a single large archive (immich's ~430MB volume tar) freezes *both* counters. No percentage can move in that window. So stop trying to. restic keeps reporting `current_files` and `seconds_elapsed` throughout; the card now shows the file being processed and the elapsed time — „Mentés: immich — 1 / 46 fájl (430.2 MB) · feldolgozás alatt: immich_upload.tar · 42 mp". „Working on this file for 42 seconds" is a completely different message from „0%", and it is the honest one. The last known `current_files` value persists across ticks that omit it (restic does not send it every time, and blanking the label every other second is its own flicker), and switching app clears it so one app's file is never shown against another. Both pinned by tests, along with the real 42-second status line shape. ### v0.147.1 — 4c follow-up: the progress bar must move on an INCREMENTAL run (2026-07-19) Found by watching the v0.147.0 card during a real manual run on the demo box, which is the only way this was ever going to surface. **The observation.** A 430MB immich push reported `0%` for 40+ seconds and then completed. The parser was not broken — restic was genuinely reporting no transferred bytes. On an incremental run where nothing changed, restic transfers nothing: `bytes_done` is `omitempty` on restic's side, so it is not even present in the JSON, and `percent_done` stays 0 for the whole run. Confirmed against the real schema by capturing `backup --dry-run --json` output from restic 0.14.0 in the controller image (the version comment in `offbox_progress.go` now quotes those captured lines verbatim). **Why it mattered.** A byte-only progress bar is indistinguishable from a hang in the COMMON case — the incremental run — which is precisely the silence 4c set out to remove. Shipping it would have replaced "no feedback" with "feedback that says 0% and looks stuck". - `files_done` / `total_files` are now parsed and published alongside the byte counters. They move on an incremental run even when bytes do not. - The card prefers bytes when bytes are moving; otherwise it drives the bar from files and says „N / M fájl ellenőrizve"; only before restic knows a total does it say „a mentendő adatok felmérése…". - `parseResticStatus` now returns a struct rather than four positional values, and a new test pins the real incremental-run line shape (bytes absent, files climbing) so a future refactor cannot quietly drop the file counters and restore the stuck bar. ### v0.147.0 — feedback slice 1: pressing a button says something (2026-07-19) Green: `go build ./... && go vet ./... && go test ./...` all pass (23 packages); `template_id_gate` + `emoji_gate` + `native_confirm_gate` + `offbox_rename_gate` + `app_row_dedup_gate` + `mojibake_gate` all PASS. (`docker_run_volume_path_gate` fails on `internal/appexport/estimate.go:179` — **pre-existing on HEAD, untouched by this release**; verified by stashing this work and re-running.) **The systemic complaint, twice in one evening: you press a button and nothing happens.** No progress, no ETA, no named result. This slice fixes the three worst offenders using the two patterns already in the codebase (the deploy 3-step panel and the storage-init status poll). It deliberately does **not** introduce a feedback framework — that is a ROADMAP item ("unified async-job feedback"), because three targeted cards are worth shipping tonight and a framework is not. - **4a — a verification restore now 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. It now carries the **full path**. The restore page gained a **„Meglévő ellenőrző másolatok"** listing (app · size · date · path) — until now nothing anywhere showed what these restores had accumulated, so they piled up and the only way to find them was SSH — each with a double-confirmed **„Másolat törlése"**. - That delete is the **only** delete this release adds, so it names a STACK, never a path: the Manager resolves the name inside a `backups/offsite-restore` root it computed itself and refuses anything landing outside. Red-proofed — neutralise the name guard and `stack: ""` resolves to the offsite-restore ROOT and 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). - `backups/offsite-restore` was open-coded in three places; it now has one home (`offsiteRestoreRootFor`), and 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. - **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 had come up. Now detached + polled, 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", which 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. - **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 currently being pushed**. Before the scan finishes it says „a mentendő adatok felmérése…" rather than pinning a bar at 0%, which reads as stuck. - **Manual only.** The nightly run stays silent and its output format is untouched — pinned by a test that fails if the scheduled path ever passes `--json` or publishes progress. - The poll 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, both confirmed: break the parser → the percent assertion fails; drop the wiring → the `--json` assertion fails. The `--json` stream is tail-bounded (40 lines) so a large backup does not buffer megabytes of status spam for error diagnosis. - **Golden/controller infra-image drift closed at the source (supports the agent-side change).** `infra.Images()` derives the list from the existing pins, `--print-infra-images` prints it, and the golden bake now asks the controller binary it is about to bake instead of carrying its own copy. The 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. A test parses the const block out of the source and fails if a pin is added without reaching `Images()`. **Live-validated** on demo guest 9201 through the real UI. **No floor change** — Viktor decides floor timing. ### v0.146.0 — nav polish: styled scrollbars + collapsible sidebar groups (2026-07-18) UI-only; no behavioural or backup/restore surface touched. Green: `go build ./... && go vet ./... && go test ./...` all pass; `template_id_gate` + `emoji_gate` + `native_confirm_gate` + `offbox_rename_gate` + `mojibake_gate` + `app_row_dedup_gate` all PASS. - **Scrollbars (`style.css`).** The platform default is a light, chunky bar that reads as a bright stripe against the navy and competes with the content it is scrolling. Now thin and hairline coloured: `scrollbar-width: thin` + `scrollbar-color` for Firefox, `::-webkit-scrollbar` (8px, thumb `--line`, hover `--text-3`, `--radius`) for WebKit/Blink — **both** declared, because neither alone covers the browsers customers actually use. The two surfaces that really scroll take their own panel background as the track (`.sidebar` → `--bg-2`, `html` → `--bg-0`) so the gutter never shows through as a lighter channel. Tokens only, no raw hexes. - **Collapsible nav groups (`layout.html` + `style.css`, vanilla JS — no framework).** Tárhely, Biztonsági mentés and Megosztás are now accordions with a chevron indicator; **exactly one is open at a time**, and clicking the open one closes it. Groups without sub-items (Vezérlőpult, Alkalmazások, Rendszermonitor, Debug) are untouched plain links. Hungarian labels unchanged. - **The header is a real `