diff --git a/.claude/rules/unprompted-work.md b/.claude/rules/unprompted-work.md index 7f600645..74fab420 100644 --- a/.claude/rules/unprompted-work.md +++ b/.claude/rules/unprompted-work.md @@ -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. diff --git a/STATUS.md b/STATUS.md index 669d5807..ddd8e4ad 100644 --- a/STATUS.md +++ b/STATUS.md @@ -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. diff --git a/documentation/architecture/02-controller-module-map.md b/documentation/architecture/02-controller-module-map.md index 7f7ed0fe..2b953a7d 100644 --- a/documentation/architecture/02-controller-module-map.md +++ b/documentation/architecture/02-controller-module-map.md @@ -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 | |---|---|---|---| diff --git a/documentation/architecture/09-update-architecture.md b/documentation/architecture/09-update-architecture.md index b4a3f6be..31810d03 100644 --- a/documentation/architecture/09-update-architecture.md +++ b/documentation/architecture/09-update-architecture.md @@ -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 diff --git a/documentation/audits/app-page-2026-10-15b/P0/VERDICT.md b/documentation/audits/app-page-2026-10-15b/P0/VERDICT.md new file mode 100644 index 00000000..827ff256 --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/VERDICT.md @@ -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. diff --git a/documentation/audits/app-page-2026-10-15b/P0/css-bodies.md b/documentation/audits/app-page-2026-10-15b/P0/css-bodies.md new file mode 100644 index 00000000..863cc6a3 --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/css-bodies.md @@ -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=' + +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 diff --git a/documentation/audits/app-page-2026-10-15b/P0/edge1.txt b/documentation/audits/app-page-2026-10-15b/P0/edge1.txt new file mode 100644 index 00000000..fb71a29c --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/edge1.txt @@ -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 + diff --git a/documentation/audits/app-page-2026-10-15b/P0/edge2.txt b/documentation/audits/app-page-2026-10-15b/P0/edge2.txt new file mode 100644 index 00000000..c54b8b95 --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/edge2.txt @@ -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 + diff --git a/documentation/audits/app-page-2026-10-15b/P0/edge3.txt b/documentation/audits/app-page-2026-10-15b/P0/edge3.txt new file mode 100644 index 00000000..bdbfc044 --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/edge3.txt @@ -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 + diff --git a/documentation/audits/app-page-2026-10-15b/P0/page.html b/documentation/audits/app-page-2026-10-15b/P0/page.html new file mode 100644 index 00000000..b43fa9ec --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/page.html @@ -0,0 +1,2 @@ +Found. + diff --git a/documentation/audits/app-page-2026-10-15b/P0/pageh.txt b/documentation/audits/app-page-2026-10-15b/P0/pageh.txt new file mode 100644 index 00000000..4889f750 --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/pageh.txt @@ -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 + diff --git a/documentation/audits/app-page-2026-10-15b/P0/pub-bust-headers.txt b/documentation/audits/app-page-2026-10-15b/P0/pub-bust-headers.txt new file mode 100644 index 00000000..5baa69ce --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/pub-bust-headers.txt @@ -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 + diff --git a/documentation/audits/app-page-2026-10-15b/P0/pub-headers.txt b/documentation/audits/app-page-2026-10-15b/P0/pub-headers.txt new file mode 100644 index 00000000..653a3d29 --- /dev/null +++ b/documentation/audits/app-page-2026-10-15b/P0/pub-headers.txt @@ -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 + diff --git a/documentation/backlog/CLOSED-ITEMS.md b/documentation/backlog/CLOSED-ITEMS.md index 65387467..df5e3a51 100644 --- a/documentation/backlog/CLOSED-ITEMS.md +++ b/documentation/backlog/CLOSED-ITEMS.md @@ -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 |