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
3.4 KiB
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
- The stylesheet URL does not change between builds.
layout.htmlL12 asks for/static/style.css?v={{.Version}}→?v=0.315.0for all three different 0.315.0 builds (e66d466,fe0228c,db52f5a). The first of those had no.app-tabsrules at all. - The origin offers no validator.
serveCSSHandler(server.go:1130) sets onlyCache-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. - Cloudflare caches this URL and extends the browser's TTL. Three requests in a row:
cf-cache-status: HIT,age: 99 → 100, andcache-control: public, max-age=14400— Cloudflare rewrites the origin's 1 hour to 4 hours for the browser. - 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.