docs: the app page's layout, the asset tag, and a version number pushed once (controller v0.316.0)
gates / gates (push) Successful in 6m46s

02-controller-module-map.md "The app page" gains the layout rules [DESIGN] — one column of one
width, a section is a hairline not a card, a box means something (blue = the app is waiting for
you, amber = something is risky, nothing else), the hero is not a box, and a success is not a
banner. Plus the asset-tag rule: every shell's stylesheet address is derived from the BUILD, not
the version, because the origin sends no validator and a cached copy can therefore only expire or
be addressed differently.

09-update-architecture.md gains §0 [DESIGN]: a version number is pushed once. It belongs at the TOP
of that document because everything below it compares versions — the floor moves a box by comparing
a number, the button offers the highest tag, and a box re-pulls a tag only when the tag changes.
Two images under one number is therefore not untidiness; it is a box running the wrong code that
can never be told.

R-1080 filed and closed: 0.315.0 was pushed three times from three different commits. Closed rather
than left open because no box is stranded — exactly one guest ran it and it ran the registry's
current digest, and the floor was 0.314.0 so nothing else could have taken it.

audits/app-page-2026-10-15b/P0/ carries the measurement and its honest caveat: the stale Cloudflare
object is no longer reproducible, because the first probe of the session forced its revalidation.
What is shown is every precondition, the proof an aged entry existed (cf-cache-status EXPIRED), and
the elimination of the only other candidate. The six stylesheet bodies are reduced to their sizes,
sha256s and rule counts — one 99 KB file does not need six copies in git to make the point.

.claude/rules/unprompted-work.md item 6, all four versioned copies: a version number is pushed once,
stated as a FACT about what the build now enforces rather than as a new rule.

STATUS: the operator's one look, through the public address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
This commit is contained in:
2026-10-15 20:33:10 +02:00
parent 3038b23ec7
commit b6638ac4fc
14 changed files with 214 additions and 1 deletions
+6
View File
@@ -50,6 +50,12 @@ it **first** in the morning note. A decision you cannot write in that shape is o
on Tester 1, demo-hp and demo-felhom through the normal update path and the box walk's checks are green
on all three — not at night unless the night brief says so. **Agent releases still stop for the
operator's vouch.** The golden is baked weekly and vouched by the operator; CC never vouches. **No `--no-verify`.**
**A version number is pushed once** — `felhom-controller/controller/build.sh` now refuses a second
push of a tag that exists, and a registry it cannot reach is a refusal too. This is a FACT about
what the build does, not a new rule: a fix for something already pushed is the next patch version,
and a box on a bad image is moved by the normal update path, never by re-tagging. Earned on
2026-10-15, when 0.315.0 was pushed three times from three different commits and a cache kept
serving the first one (`09-update-architecture.md` §0).
7. **An enumerated gap becomes a row in the same session — or, if it is small, is fixed in it** (the size rule).
Prose is not a record.
8. **Hungarian text is searched with ASCII fragments**, with a positive and a negative control.
+2 -1
View File
@@ -2,7 +2,7 @@
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
**Updated 2026-10-15 (evening).** Agent 0.156.1 + its bundle on all three boxes; controller 0.315.0 built (one app page with four tabs); 0.314.0 is still the floor since 13:11 UTC — raised by CC with its own token after all three test boxes ran it (the first release with no golden; vouched golden 0.313.0); hub 0.153.0 (access tokens — CC uses its own token now, not your password). Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
**Updated 2026-10-15 (evening).** Agent 0.156.1 + its bundle on all three boxes; controller 0.316.0 built (the app page in one column; a version number can now only be pushed once); 0.314.0 is still the floor since 13:11 UTC — raised by CC with its own token after all three test boxes ran it (the first release with no golden; vouched golden 0.313.0); hub 0.153.0 (access tokens — CC uses its own token now, not your password). Open items: see `documentation/backlog/OPEN-ITEMS.md`. Older notes, day by day:
`documentation/backlog/STATUS-HISTORY.md`. Reports: `REPORT-night-2026-10-14.md`, `REPORT-immich-rollout-2026-10-14.md`, `REPORT-seo-felho-tarhely-2026-10-14.md`, `REPORT-immich-repair-2026-10-14.md`, `REPORT-power-cut-fixes-2026-10-14.md`, `REPORT-night-power-2026-10-13.md`, `REPORT-poweroff.md`.
## What works
@@ -48,6 +48,7 @@
| D10 | R-1079 | Storage that knows its physical disks (`architecture/16-storage-physical-identity.md` §12, six questions D1–D6): a same-disk second copy, „unknown" disks (VMs), the one-disk internal place and its reserve, old one-disk boxes, partitions, NAS films | **D1 (a) no same-disk copy · D2 (a) never count unknown · D3 (a) internal place, reserve max(16 GiB, 10 %) · D4 (a) a plain sentence · D5 (a) installer only · D6 (a) separate** | Other picks: more space spent on false copies, or films inside the whole-box copies | One-disk boxes keep refusing media apps; VM boxes keep counting same-disk copies | the build task |
## Waiting on you — do
- **Nézd meg az AdventureLog és a Plex oldalát a demo-hp-n, a rendes címén** (`felhom.enkisfelhom.hu`). Az alkalmazás oldala mostantól egy hasábban van: egy szélesség, vékony vonalak a szakaszok között, aláhúzott fül, és doboz csak ott, ahol az alkalmazás vár rád (kék) vagy ahol valami kockázatos (sárga). A „sikeres frissítés" sáv kis szürke sorrá vált a név alatt. Ha nem nézed meg: a felület él, csak senki nem látta még böngészőben.
- **Nézd meg az új alkalmazás oldalt a demo-hp-n (egy telepített és egy nem telepített app).** Egy alkalmazás mostantól egy oldal négy füllel (Beállítások · Adatok és mentés · Útmutató · Napló), az Indítás/Újraindítás/Leállítás a „Működés" kártyán van, és a „Vissza" az alkalmazáshoz visz, nem a listához. Mondd meg, jó-e így az elrendezés. Ha nem nézed meg: a felület él, csak senki nem látta még böngészőben — a robot a DooPlexen nem tud kattintani.
- **The N100 sheet (about 30 minutes):** `documentation/audits/n100-lived-in-2026-10-15/SHEET.md` — install Plex and claim it, make the admins in Prowlarr/Sonarr/Radarr, add your download sources in Prowlarr, add one show and one film. Then say „done". If not: the media apps wait, safe behind their gate.
- **Delete the test customer „Tester3” in the hub (browser only).** Its test box is already gone and switched off on purpose, so no alarm is coming. If not: a dead test customer stays in your list.
@@ -477,6 +477,35 @@ does not get to vouch for a version, so an overlay can neither set nor clear it.
**[DESIGN] Guide text is DATA.** Plain text through `html/template` — no markdown, no raw HTML — so
a step containing a tag is shown as the characters its author typed.
#### The app page's layout (v0.316.0, operator-approved mockup 2026-10-15)
**[DESIGN]** Five rules; the first three are the mockup, the last two are what they imply.
1. **One column of one width** (820px; the log tab alone is full width, because log lines are long).
Boxes of differing widths read as a page assembled by accident, which is what v0.315.0 looked like.
2. **A section is a hairline, not a card** — `padding: 28px 0` with a bottom `--line-soft`, no
background, no border, no radius. A card says „this is a thing you act on", and most of an app
page is a thing you READ; a card there is a promise the content cannot keep.
3. **A box means something, and there are exactly two.** Blue (`--blue` on `--blue-dim`) when the app
is WAITING for the household — an after-start value; amber (`--warn` on `--warn-dim`) when
something is RISKY, such as a backup sharing a disk with the data it protects. The
exception-colour principle, applied to shape as well as colour. Nothing informational is boxed.
4. **The hero is not a box**: a 64px logo tile, the name, the tagline, hairline chips (state,
lifecycle, update) and meta text (the address, the last automatic update). The tab bar is links
with a 2px `--blue-bright` underline under the open one — no pills, no fills.
5. **A success is not a banner.** The automatic-update SUCCESS line is meta text in the hero. It
asked nothing of the household, so it is a date. Everything that DOES need a decision — a failed
update, a hold, lost files, a missing drive, an interrupted install — stays a banner above the
tabs, which is R-571's placement rule and is unchanged.
**[DESIGN] The stylesheet's address changes with every build.** Every shell links
`/static/style.css?v={{.AssetTag}}`, where the tag hashes version + `GitCommit` + `BuildTime` and
stays opaque (8 hex) because the stranger-facing share pages use the same value. The origin sends no
ETag and no Last-Modified, so a cached copy cannot be revalidated — only expire, or be addressed
differently. This is the „addressed differently", and it is the fix for a measured defect: a
Cloudflare edge served 0.315.0's first stylesheet under the address its third build still asked for
(`audits/app-page-2026-10-15b/P0/VERDICT.md`). `Cache-Control` is deliberately unchanged.
### `scripts/`
| File | Class | Reason | Risk |
|---|---|---|---|
@@ -20,6 +20,29 @@ source is the truth.** Nothing here is invented: every mechanism claim is cited
---
## 0. A version number is pushed once (v0.316.0, [DESIGN])
**[DESIGN] A controller version number names exactly one image, for ever.** `build.sh` checks the
registry before building and refuses `--push` or `--multiarch` for a tag that already exists; a
registry it cannot reach is a refusal too, because proving the tag is new is the point and „I could
not ask" is not that proof. Local builds may reuse a version freely — that is iteration.
Why it has to be a mechanism and not a habit: **everything below compares VERSIONS.** The floor
moves a box by comparing its version to a number; the household's button offers the highest tag; a
box re-pulls a tag only when the tag changes. So two images sharing a number is not an untidiness,
it is a box that runs the wrong code and **can never be told** — to the box and to the floor, its
number is already current.
Measured, 2026-10-15: v0.315.0 was built and pushed three times from three different commits. A
browser that had loaded the first build kept its stylesheet, because every build asked for the same
`?v=0.315.0` and the origin sends no validator to revalidate against
(`audits/app-page-2026-10-15b/P0/VERDICT.md`). The same release's asset tag closes the cache half;
this closes the half that let it happen.
**A fix for something already pushed is the next patch version**, never a second push of the same
number. A box already on the bad image is moved by the normal update path — never by re-tagging,
which would be the same defect wearing the fix's clothes.
## 1. How an update works today, as measured
### 1.1 The button
@@ -0,0 +1,55 @@
# Part 0 — why the tab bar rendered unstyled through the public address
**Measured 2026-10-15 ~19:55 CEST from DooPlex (Hungary, so the geo rule passes). Nothing was changed.**
## Verdict: (a) a cache — and the mechanism is named
Not (b): **exactly one box runs 0.315.0 and it runs the registry's current image.**
| Box | pinned tag | status |
|---|---|---|
| demo-hp guest 9201 | `felhom-controller:0.315.0` | Up, healthy |
| demo-hp guest 9202 | `felhom-controller:0.314.0` | Up, healthy |
| felhom-pve (N100) guest 9201 | `felhom-controller:0.314.0` | Up, healthy |
| c11-appliance (Tester 1) | unreachable from DooPlex right now | — |
- demo-hp 9201's running image: `gitea.dooplex.hu/admin/felhom-controller@sha256:8887343011d64a2da9c65674a777a099708315063e3abc7c9463c03151dff041`
- The registry's current `0.315.0`: `sha256:8887343011d64a2da9c65674a777a099708315063e3abc7c9463c03151dff041` — **identical**.
- The **global floor is `0.314.0`** (read from the hub's `/configuration`), so no box could have taken
0.315.0 by itself; the one that has it was deployed by hand. Tester 1 therefore cannot be on a
stale 0.315.0 even though it is off right now.
## The four facts that make a cache the cause
1. **The stylesheet URL does not change between builds.** `layout.html` L12 asks for
`/static/style.css?v={{.Version}}` → `?v=0.315.0` for all **three different** 0.315.0 builds
(`e66d466`, `fe0228c`, `db52f5a`). The first of those had no `.app-tabs` rules at all.
2. **The origin offers no validator.** `serveCSSHandler` (`server.go:1130`) sets only
`Cache-Control: public, max-age=3600`. Measured on the box: **ETag present = 0, Last-Modified
present = 0.** With neither, no client and no edge can revalidate a copy — it can only expire.
3. **Cloudflare caches this URL and extends the browser's TTL.** Three requests in a row:
`cf-cache-status: HIT`, `age: 99 → 100`, and `cache-control: public, max-age=14400` — Cloudflare
rewrites the origin's 1 hour to **4 hours for the browser**.
4. **Cloudflare held a stale entry for that exact URL.** The very first request of the session returned
`cf-cache-status: EXPIRED` — which only happens when the edge *has* an entry that has aged past its
TTL. Having no validator, it had to refetch the whole body.
## What is true right now, and the honest caveat
Origin, edge and box agree byte-for-byte (`sha256 c1774f63daf65c56` both sides) and all contain the
`.app-tabs` rules. The page itself is never cached (`cf-cache-status: DYNAMIC`); only the stylesheet is.
**The caveat, stated rather than glossed:** the specific stale object is no longer observable — the
first probe of this session forced its revalidation, which is exactly what `EXPIRED` records. So I
cannot produce a `HIT` whose body lacks `.app-tabs`. What is shown is every precondition for it, the
proof that an aged entry existed, and the elimination of the only other candidate (b).
## Why this authorises Part 1.2
The fix is not "purge the cache" — that hides the cause and returns at the next build. The fix is that
**the stylesheet's address changes with every build**, so no cache at any layer (browser, Cloudflare,
or anything between) can serve a previous build's CSS under an address the new build still asks for.
`Cache-Control` stays as it is: the new URL is the fix, not a shorter cache.
Part 1.1 removes the precondition entirely — a version number can only be pushed once, so two
different images can never share a number again.
@@ -0,0 +1,11 @@
# The stylesheet bodies are not kept — six copies of one 99 KB file. What the evidence needs is
# their size, their sha256 and whether they carry the .app-tabs rules. Reproduce any of them with
# curl -s 'https://felhom.enkisfelhom.hu/static/style.css?v=<tag>'
FILE BYTES APP_TABS SHA256
edge1.css 98994 2 c1774f63daf65c5653520dba21fc686b
edge2.css 98994 2 c1774f63daf65c5653520dba21fc686b
edge3.css 98994 2 c1774f63daf65c5653520dba21fc686b
origin.css 98994 2 c1774f63daf65c5653520dba21fc686b
pub-bust.css 98994 2 c1774f63daf65c5653520dba21fc686b
pub.css 98994 2 c1774f63daf65c5653520dba21fc686b
@@ -0,0 +1,14 @@
HTTP/2 200
date: Thu, 15 Oct 2026 17:52:45 GMT
content-type: text/css; charset=utf-8
cache-control: public, max-age=14400
strict-transport-security: max-age=86400
age: 99
last-modified: Thu, 15 Oct 2026 17:51:05 GMT
cf-cache-status: HIT
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=HF%2FIgCiLuIzjJ5G%2BFauOO6TxF1GmCRETF6MwYBlMjEPVthuyR6t%2FfvhtFftATAPMyQJeq2XpMEjPAqZRS4losHGDEG%2FDJXgP%2FZit%2FGkpclL3hXbirbBl3wnjJRE5ZqqFYRu091UZTWE%3D"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
server: cloudflare
cf-ray: a4b0adead9c111d5-VIE
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,14 @@
HTTP/2 200
date: Thu, 15 Oct 2026 17:52:45 GMT
content-type: text/css; charset=utf-8
cache-control: public, max-age=14400
strict-transport-security: max-age=86400
age: 100
last-modified: Thu, 15 Oct 2026 17:51:05 GMT
cf-cache-status: HIT
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=Bc%2BNPGEHcXkmMFwUP2v0m0tFg%2BQTGHAbPxp8EM%2BgXoiqcVuMW2PKgQP69nygHqwLQeVsN1oQZtBZoNJcNoRb87qg323n2tkw37a8L0xXRkLEtfv3UJssz4SyEdO9nydIT4DkQ3cNYWs%3D"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
server: cloudflare
cf-ray: a4b0adec4d565a8f-VIE
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,14 @@
HTTP/2 200
date: Thu, 15 Oct 2026 17:52:45 GMT
content-type: text/css; charset=utf-8
cache-control: public, max-age=14400
strict-transport-security: max-age=86400
age: 100
last-modified: Thu, 15 Oct 2026 17:51:05 GMT
cf-cache-status: HIT
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=P6QLBAzTUXTPRekJzLrSvesHOAGvPFXgeGClsbLokMovm6BbL4eBbPfiTui2je8iLJXPxprpllXYOXF%2BZ8AnPAx0ieKT4AEYrKxGMKw1xbJK%2BgbRFidTQIusyDnyiMQuYzmBaW3XKhk%3D"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
server: cloudflare
cf-ray: a4b0adedabf49cca-VIE
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,2 @@
<a href="/login?next=%2Fapps%2Fadventurelog">Found</a>.
@@ -0,0 +1,12 @@
HTTP/2 302
date: Thu, 15 Oct 2026 17:52:47 GMT
content-type: text/html; charset=utf-8
location: /login?next=%2Fapps%2Fadventurelog
strict-transport-security: max-age=86400
cf-cache-status: DYNAMIC
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=YmplPMHLsWLWoy7RidM4MVtfIrDVPfw%2FGGlhFJwnvICgAJKbPpbyBxJPnyIG4xru8jobs1dmWqSxAKVVbzns81qeMprHAo3LUsXgoihvKztV8wp7HJnDcZju8Dm%2BBeuegOQJ61ffUGI%3D"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
server: cloudflare
cf-ray: a4b0adf5f85d3a0d-VIE
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,13 @@
HTTP/2 200
date: Thu, 15 Oct 2026 17:51:10 GMT
content-type: text/css; charset=utf-8
cache-control: public, max-age=14400
strict-transport-security: max-age=86400
last-modified: Thu, 15 Oct 2026 17:51:10 GMT
cf-cache-status: MISS
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=p%2Befhb3Eu8EOwL9ygvoAzHD9sJH4dsEYXdwNzqXOFUxPIk3yoqv5zjZtnRJwl2UUORNrcrGGfm5cKwPOARLwvpBucD8djUjF5XNb8HdsbDrgrCyTXwphFXUiLrq8jYqD6E8NHAt0hU4%3D"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
server: cloudflare
cf-ray: a4b0ab9d4bc3e9e2-VIE
alt-svc: h3=":443"; ma=86400
@@ -0,0 +1,13 @@
HTTP/2 200
date: Thu, 15 Oct 2026 17:51:05 GMT
content-type: text/css; charset=utf-8
cache-control: public, max-age=14400
strict-transport-security: max-age=86400
last-modified: Thu, 15 Oct 2026 17:51:05 GMT
cf-cache-status: EXPIRED
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=cN1LcugsETH172R4%2BgpMOJ3ssKKTZ2tsFHQEB4SvNmsXF7n%2Fdnuial3p1TwjD4pT4X8KCrgkjCJdb0wdHx7wqxeNJ4b2lYCTszizuJZiaKSGOZTLN%2BXac1Mw3no4I020eIFnBifc%2BKU%3D"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
server: cloudflare
cf-ray: a4b0ab7a4965cb7b-VIE
alt-svc: h3=":443"; ma=86400
+6
View File
@@ -26,6 +26,12 @@
---
## 2026-10-15 (evening) — controller v0.316.0, one column and one image per version
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-1080** | **v0.315.0 was built and pushed THREE times from three different commits, and nothing stopped it.** (P2) Every build asked the browser for `/static/style.css?v=0.315.0`, so a cache holding the first build's stylesheet — which had no `.app-tabs` rules at all — kept serving it under the address the third build still asked for. Worse in principle than the symptom: the floor and the household's button both compare VERSIONS, so a box that pulled between the first and the last runs different code under a number that is already current, and can never be told. | CLOSED 2026-10-15 — **controller v0.316.0**, in two halves. (1) `build.sh` refuses `--push`/`--multiarch` for a tag already in the registry, before building; a registry it cannot reach is a refusal too. Six cases in `scripts/test_build_push_once.sh` against the real script with a fake docker; red-proved twice. (2) every shell's stylesheet URL is a per-build tag (version + GitCommit + BuildTime), so two builds of one version cannot share a cache entry. **No box is left on a bad image:** exactly one guest ran 0.315.0 (demo-hp 9201) and it ran the registry's current digest `8887343011d6`; the floor was 0.314.0, so nothing else could have taken it. | `audits/app-page-2026-10-15b/P0/VERDICT.md`, `felhom-controller/REPORT.md` |
## 2026-10-15 (evening) — controller v0.315.0, the app page
| Row | What | Closed | Evidence |