# REPORT — the app page in one column, and one image per version number (2026-10-15) **A non-overwritten sibling of `REPORT.md`.** Two sessions released this repo on the same evening; `REPORT.md` is per-session and overwritten, and it now holds v0.317.0's (the storage work). This file holds the app-page arc so it stops being clobberable — the convention `felhom.eu/CLAUDE.md` prescribes for parallel sessions, applied here. ## 1. Baselines | Repo | `main` at start | Version at start | |---|---|---| | felhom-controller | `8339206` | v0.315.0 | | felhom.eu | `340804f9` | docs only | Architecture read before any claim: `02-controller-module-map.md` „The app page", `09-update-architecture.md`, the `felhom-ui-design` skill, `CLAUDE.md` + `.claude/rules/*`. Edited on the Windows workstation; built and tested on DooPlex in an isolated clone; the browser work is on the workstation, which is why CC runs here. ## 2. PART 0 — the diagnosis, before any fix **Verdict: (a) a cache.** Evidence: `felhom.eu/documentation/audits/app-page-2026-10-15b/P0/VERDICT.md`. **(b) is ruled out.** Exactly one guest ran 0.315.0 — demo-hp 9201 — on digest `8887343011d6`, identical to the registry's 0.315.0 at the time of measuring. The **global floor was 0.314.0** (read from the hub's `/configuration`), so no box could have taken 0.315.0 by itself. demo-hp 9202 and felhom-pve (N100) 9201 were both on 0.314.0; Tester 1 (`c11-appliance`) was unreachable and, by the floor, cannot have had it. **The mechanism, in four measured facts:** 1. `layout.html` asked for `/static/style.css?v={{.Version}}` → `?v=0.315.0` for **all three** different 0.315.0 builds (`e66d466`, `fe0228c`, `db52f5a`); the first had no `.app-tabs` rules. 2. The origin sends **no validator**: on the box, `ETag present = 0`, `Last-Modified present = 0`. A cached copy cannot be revalidated — only expire. 3. Cloudflare caches that URL (`cf-cache-status: HIT`, `age: 99 → 100` over three requests) and **rewrites the browser TTL from the origin's 3600 to 14400** — four hours. 4. The session's first request returned **`cf-cache-status: EXPIRED`**, which only happens when the edge holds an entry that has aged past its TTL. **The caveat, stated rather than glossed:** origin, edge and box now agree byte-for-byte (`sha256 c1774f63…`) and all carry the rules. The stale object itself is **no longer observable** — that first probe forced its revalidation, which is what `EXPIRED` records — so no `HIT` with a bad body can be produced. What is shown is every precondition, the proof an aged entry existed, and the elimination of the only other candidate. **No cache was purged by hand.** ## 3. What shipped | Area | What | |---|---| | Push-once | `build.sh` refuses `--push`/`--multiarch` for a tag already in the registry, before building. A registry it cannot reach is a refusal too. Local builds untouched. | | Clean tree | `build.sh` also refuses a tree that is dirty, not at `origin/main`, or not a git checkout. Added after it was needed — see §6. | | Asset tag | `assetTag()` hashes version + `GitCommit` + `BuildTime`; **all nine shells** use it (seven asked for `?v={{.Version}}`). Opaque 8 hex, because the stranger-facing share pages use the same value. `Cache-Control` unchanged — the new address is the fix. | | Layout | The approved one-column mockup: 820px column, hairline sections, underlined tabs, no hero box, 200px label grid. Two boxes only: blue „the app is waiting for you", amber „this is risky". | | Banner | The auto-update SUCCESS banner is meta text in the hero. Every banner that needs a decision is untouched above the tabs (R-571). | | Leftovers | „2. mentés beállítása — " and „ — Exportálás" removed; „Cím és kulcsok" with one address row replaces the duplicate disabled subdomain field; the „already installed, read-only" banner is now the hint under the fields; the install wizard gets the same back link, hero and column. | ## 4. Commits on `main` `9cc7ffc` push-once + per-build asset tag · `7f3e58f` the one-column layout · `48d8674` v0.316.0 CHANGELOG + the rule-file fact · `006ceab` v0.316.1 (the tag reaches the dashboard) · `f908eee` settings tab de-boxed · `9e68e52` two data-tab leftovers · `8c7aa10` v0.316.3 CHANGELOG · `f9151f0` **the padding fix** · `4261a38` `FELHOM_REPO_DIR` + the build names its tree · `c6a5671` v0.316.4 the clean-tree gate · `e7b64af` `workspace/` gitignored. felhom.eu: `b6638ac4` (design docs + the audit), `22cd7c51` (R-1080 closed, R-1081 filed). ## 5. Tests and red-proofs — every break seen failing, then reverted | # | Break | Result | |---|---|---| | B1 | the registry check deleted | 5 of 6 cases fail; a present tag builds AND pushes | | B2 | an unreachable registry treated as absent | the two unanswered-registry cases fail | | B3 | the clean-tree gate deleted | all four of its cases fail | | C1 | `assetTag` from the version only | `TestAssetTag_ChangesPerBuild` fails on both same-version cases | | C2 | one shell back on `?v={{.Version}}` | `TestAssetTag_EveryShellUsesIt` fails naming layout.html | | C3 | `addShellAssetData` removed from `executeTemplate` | `TestAssetTag_ReachesTheRenderedPage`: empty buster on four pages | | D1 | the success banner restored | `TestAppPage_AutoUpdateSuccessIsMetaNotBanner` fails | | D2 | one tab loses its column | `TestAppPage_OneColumnLayout` fails naming the tab | | D3 | a section drawn as a card again | same test fails on two tabs | | E | an informational section drawn as a blue callout | `TestAppPage_CalloutsOnlyForAskAndWarn` fails on both counts | New: `scripts/test_build_push_once.sh` (**ten** cases, driving the real `build.sh` with a fake `docker` on PATH — nothing is ever pushed), `TestAssetTag_{ChangesPerBuild,EveryShellUsesIt,ReachesTheRenderedPage}`, `TestAppPage_{OneColumnLayout,AutoUpdateSuccessIsMetaNotBanner,CalloutsOnlyForAskAndWarn}`. Full gate: `go build ./... && go vet ./... && go test ./...` → **exit 0, 37 packages ok, zero failures**, read from `go test`'s own exit code, not a pipeline's. All controller gates OK; 53 decoys behaved. ## 6. THREE INCIDENTS, one shape All three are the same defect: **a guard that is not on the path the work actually takes.** Two of them I caused while fixing the first. **(i) I re-pushed 0.315.0 a fourth time.** The push-once guard went into `controller/build.sh` in the repo; the documented release command runs `$FELHOM_ROOT/build/felhom-controller/build.sh`, which was a **separate copy taken 2026-07-18** with no guard. The first thing I did with the new refusal — run it against a real existing tag to prove it fires — therefore rebuilt and re-pushed 0.315.0 with v0.316.0's code under it (`8887343011d6` → `b809201841f7`). Root cause fixed: the build directory's script is a symlink to the repo's, the stale copy kept as `build.sh.stale-copy-2026-07-18`, and the refusal then proven from the documented path. **R-1081.** **(ii) The asset tag shipped dead in 0.316.0.** Every shell asked for `?v={{.AssetTag}}` and the template test passed, but `addShellAssetData` was only ever called by `executeTemplateLang` — the stranger-facing pages. The dashboard path had never needed it, because `layout.html` used to ask for `{{.Version}}`, which `baseData` supplies. The first live load rendered `/static/style.css?v=` — **empty, which is a constant, and a constant busts nothing.** Found in a browser. Fixed in 0.316.1; `TestAssetTag_ReachesTheRenderedPage` now asserts the rendered value, not the markup. **(iii) Building 0.316.3 published another session's unreleased work.** The symlink from (i) points into the shared clone's **working copy**, which predated the `FELHOM_REPO_DIR` override — so the override was silently ignored and the build copied the shared tree, which held 31 uncommitted files of the physical-disk storage work. The banner said `Commit: 0225255` with no `Repo:` line; the evidence was on screen and the image was already pushed. **Nobody deployed it — that was luck, not a control.** The clean-tree rule had existed only as a sentence in a runbook. It is now two questions the build asks and refuses on, plus a refusal for a source that is not a git checkout at all; it immediately caught its own next build (the scratch `workspace/` beside the script, now gitignored). **`0.316.3` must never be deployed.** It is neither re-pushed nor deleted: a version number is pushed once, and that applies to a number I spoiled. ## 7. Deployed, and what happened to 0.316.4 **demo-hp guest 9201 runs `0.317.0`, healthy.** That is the other session's release, built from `main` at a commit that contains all of this work — verified: `f9151f0` (the padding fix) and `c6a5671` (the clean-tree gate) are both ancestors of it. **`0.316.4` was built but never published, and was then dropped.** Its first build attempt was refused by its own new clean-tree gate (the `workspace/` directory); by the time that was fixed, `0.317.0` was already in the registry. Publishing 0.316.4 would have put a *lower* number in the registry carrying nothing 0.317.0 does not already have. The household's button offers the highest tag, so it would have been invisible anyway. **The floor stays 0.314.0** — raising it needs all three test boxes and a green box walk, and this evening two sessions released the same component. | tag | state | |---|---| | 0.316.0 / .1 / .2 | published, superseded | | **0.316.3** | published, **CONTAMINATED — never deploy** | | 0.316.4 | never published (superseded) | | **0.317.0** | published, deployed on demo-hp, carries this work | ## 8. The browser check, through the PUBLIC address Chrome on the workstation, `https://felhom.enkisfelhom.hu`, **normal loads — no hard refresh**: - The stylesheet URL is a real 8-hex per-build tag and the loaded sheet carries the `.app-tabs` rules. **This is the fix working:** before it, the same page needed a hand-forced refetch. - `.app-tab-active` computes `border-bottom: 2px rgb(46,168,245)` (`--blue-bright`); column `max-width: 820px`; hero `border-top-width: 0px`. - Across settings / data / guide / logs / plex / grocy / install: **1 column each, 1 `aria-current` on an installed app and 0 on a not-installed one, 0 `.app-info-card`, 0 success banners.** - Data tab: exactly **1 amber callout** (the same-disk warning). Settings, guide, grocy: none — correct, nothing is asked and nothing is risky. - **After the operator's report:** `.app-page` padding `0px 0px 32px`, `.content` padding `32px`, measured gap **32px from the sidebar** (was 80) and **36px from the top** (was 64+). - **Console clean on a fresh load**, on 0.316.2 and again on 0.317.0. Screenshots: `felhom.eu/documentation/audits/app-page-2026-10-15b/browser/` — `01-settings-boxes-still-there`, `02-settings-one-column-0.316.2`, `03-data-tab-amber-callout`, `04-guide-tab-padding-fixed-0.317.0`. **Scenario E's blue callout was NOT exercised live.** Plex's setup gate is open (the operator's real claim, R-1060), so `Needed()` is false and the card is correctly hidden. Showing it live would mean un-claiming the operator's real Plex server; not done. The branch is covered by `TestAppPage_CalloutsOnlyForAskAndWarn` against a harness Plex with a closed gate. ## 9. Parity fixtures Recaptured (the pages this task restyles): `app_settings_*` ×21, `app_data_*` ×9, `app_guide_*` ×4, `app_logs`, `deploy_new{,_blocked,_memory_blocked}`. **All other fixtures are byte-identical** — and now permanently independent of build identity: the harness normalises the asset tag on **both sides** of the comparison, as it already did for the per-session CSRF token, so a build can no longer churn 148 files. The same normalisation was applied at the second comparison site in `i18n_wiring_test.go`, which had the same assumption. ## 10. NOT yet validated 1. **The operator's own look** at AdventureLog and Plex on demo-hp through the public address. The layout judgement is theirs. (The padding complaint from the first look is fixed and live.) 2. **A blue callout in a real browser** (§8). 3. **A catalog guide rendered from a real template** — no template carries `app_info.guide` yet. ## 11. Teardown — three layers - **Machine:** nothing provisioned, no app installed, no throwaway Plex. Probe scripts, fetched pages and the password file removed from guest 9201, demo-hp and DooPlex `/tmp`; the dashboard password went file→file, was never printed, and no copy reached Windows or git. - **Host:** both scratch clones and the `felhom.eu` sibling symlink deleted. The shared DooPlex checkout was never written to by me — which is how the other session's work was distinguishable from mine. `build.sh` in the build directory is now a symlink to the repo's (deliberate), with the stale July copy kept beside it. - **Hub:** nothing created; no floor change. ## 12. Observations - **FILED: R-1081** — the registry's `0.315.0`, and now also `0.316.3`, hold content that is not what their numbers say. Both caused by this session; both fixed at the root; no box affected. - **NOT-A-FINDING (fixed in session):** the asset tag shipped dead — incident (ii). - **NOT-A-FINDING (fixed in session):** the clean-tree rule had no mechanism — incident (iii). - **NOT-A-FINDING (fixed in session):** `app_row_dedup_gate` convicted my `.app-row`, correctly — that class already means „a row in the app list". Renamed `.app-field-row`; an over-broad rename briefly took the list's own rule with it and was restored. - **NOT-A-FINDING (fixed in session):** the one-column padding doubled against `.content`'s own 2rem. The mockup's numbers were the total the page should show, not an amount to add. - **NOT-A-FINDING:** `build.sh`'s `workspace/` dirtied any clone it was run from; gitignored. - **Register: 169 before → 170 after; 1 opened (R-1081), 1 opened-and-closed (R-1080).** - **System poster:** no fact in `felhom-system-poster.facts.md` changed. Neither file edited. - **Instruction files:** `.claude/rules/unprompted-work.md` item 6 in **all four** versioned copies gains „a version number is pushed once", stated as a fact about what the build now enforces, not as a new rule. The workspace-root copy is unversioned and not on this machine. - **Worth the operator's attention:** two sessions released this repo within an hour, and the second overwrote the first's `REPORT.md`. This file is the workaround; whether `REPORT.md` should stay a single overwritten file in a repo two sessions can release is a question for the operator, not a change I should make unasked.