REPORT.md is per-session and overwritten, and tonight two sessions released this repo within an hour — the second (v0.317.0, the storage work) took it. This sibling holds the app-page arc: Part 0's measurement and its caveat, what shipped, every red-proof, the three incidents, what is deployed and what must never be, the browser check, and what is still owed. The convention is felhom.eu/CLAUDE.md's, for exactly this case; this is the first use of it in this repo. Whether REPORT.md should stay a single overwritten file in a repo two sessions can release is a question for the operator, and is left as one rather than answered by a change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
14 KiB
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:
layout.htmlasked for/static/style.css?v={{.Version}}→?v=0.315.0for all three different 0.315.0 builds (e66d466,fe0228c,db52f5a); the first had no.app-tabsrules.- The origin sends no validator: on the box,
ETag present = 0,Last-Modified present = 0. A cached copy cannot be revalidated — only expire. - Cloudflare caches that URL (
cf-cache-status: HIT,age: 99 → 100over three requests) and rewrites the browser TTL from the origin's 3600 to 14400 — four hours. - 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-tabsrules. This is the fix working: before it, the same page needed a hand-forced refetch. .app-tab-activecomputesborder-bottom: 2px rgb(46,168,245)(--blue-bright); columnmax-width: 820px; heroborder-top-width: 0px.- Across settings / data / guide / logs / plex / grocy / install: 1 column each, 1
aria-currenton 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-pagepadding0px 0px 32px,.contentpadding32px, 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
- 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.)
- A blue callout in a real browser (§8).
- A catalog guide rendered from a real template — no template carries
app_info.guideyet.
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.eusibling 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.shin 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 also0.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_gateconvicted 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'sworkspace/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.mdchanged. Neither file edited. - Instruction files:
.claude/rules/unprompted-work.mditem 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; whetherREPORT.mdshould 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.