REPORT + CONTEXT for v0.316.x: the diagnosis, the two mechanisms, and the bad push I caused
gates / gates (push) Failing after 48s

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:56:58 +02:00
parent 8c7aa10a6e
commit ec438479e2
2 changed files with 157 additions and 241 deletions
+20 -1
View File
@@ -7,7 +7,26 @@
>
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
Last updated: 2026-10-15 (v0.315.0 — one app page with four tabs, the catalog guide, no Pi badge)
Last updated: 2026-10-15 (v0.316.x — the app page in one column; a version number is pushed once)
> **2026-10-15 (late evening) — v0.316.0/.1/.2 built and on demo-hp; .3 committed, NOT built.**
> **Measured before fixing:** the tabs rendered unstyled through the public address because of a
> CACHE, not the box — the stylesheet URL was `?v={{.Version}}`, identical across three different
> 0.315.0 builds; the origin sends NO ETag and NO Last-Modified, so a copy can only expire; and
> Cloudflare caches it while rewriting the browser TTL to four hours. Evidence and its caveat:
> `audits/app-page-2026-10-15b/P0/VERDICT.md`.
> **Two mechanisms now:** `build.sh` refuses a second push of a version (a registry it cannot reach
> is a refusal too), and every shell's stylesheet address is a per-build tag.
> **The app page is the approved one-column mockup**: one width, hairline sections, underlined tabs,
> and a box only for „the app is waiting for you" (blue) or „this is risky" (amber). The
> auto-update SUCCESS banner is now meta text.
> **THE LESSON OF THE EVENING, and it cost a bad push:** I put the guard in `controller/build.sh`,
> but the release runbook runs `build/felhom-controller/build.sh` — a SEPARATE COPY from
> 2026-07-18. Proving the refusal against a real tag therefore re-pushed 0.315.0 a fourth time
> (R-1081). The build directory's script is a symlink now. A guard in a script nobody runs is not a
> guard. The same shape bit twice more in one evening: the asset tag shipped dead because only one
> of TWO render paths set it, and both times a BROWSER found what the suite could not.
> Not yet validated: the operator's own look; 0.316.3's two cosmetic fixes.
> **2026-10-15 (evening) — v0.315.0, ONE APP PAGE WITH FOUR TABS.** An app was six pages and every „Vissza" went to the
> app LIST; it is now `/apps/{slug}/{settings,data,guide,logs}` plus `/apps/{slug}/install`, one shared header and one
+137 -240
View File
@@ -1,280 +1,177 @@
# REPORT — v0.315.0: one app page with four tabs (2026-10-15)
# REPORT — v0.316.x: the app page as one column; one image per version number (2026-10-15)
## 1. Baselines
| Repo | `main` at start | Version | → |
|---|---|---|---|
| felhom-controller | `85f65c7` | v0.314.0 | **v0.315.0** |
| felhom.eu | `06e7cf8c` (the task said `3263bb2`; it had moved — two other sessions are pushing) | docs only | — |
| app-catalog-felhom.eu | `e67aad4` | read only | — |
| felhom-controller | `8339206` | v0.315.0 | **v0.316.0 → .1 → .2 built; .3 committed, NOT built (§6)** |
| felhom.eu | `340804f9` (docs only) | — | — |
Architecture read before any claim: `02-controller-module-map.md` (`web/`, the R-571 alert-placement
rule), `10-localisation.md` §7, `15-media-library.md` (the Plex claim path), and the controller's
`CLAUDE.md`, `REUSE.md` and `.claude/rules/{ui-hungarian,gates,unprompted-work}.md`.
Architecture read first: `02-controller-module-map.md` „The app page", `09-update-architecture.md`,
the `felhom-ui-design` skill, and the controller's `CLAUDE.md` + `.claude/rules/*`.
**Where the work ran.** Edited on the Windows workstation; **built and tested on DooPlex** in an
isolated scratch clone (`/mnt/5_hdd/felhom.eu/scratch/cc-apptabs`) so the shared DooPlex checkout —
which two other sessions are using tonight — was never dirtied. The sync normalises line endings to
LF and restores any file it did not mean to touch, because this checkout has `core.autocrlf=true`
while six files (`.gitignore`, `Dockerfile`, `Makefile`, the three infra templates) are stored in git
WITH CRLF; a blanket normalisation rewrote all six on the first attempt.
Built and tested on DooPlex in an isolated scratch clone; edited on the Windows workstation. The
browser work is on the workstation, which is the reason CC runs here.
## 2. Files created / modified
## 2. PART 0 — the diagnosis, before any fix
**Created (11)**
`controller/internal/web/app_page.go` · `after_start.go` · `app_page_test.go` · `guide_render_test.go` ·
`controller/internal/stacks/guide_test.go` ·
`templates/app_shell.html` · `app_settings.html` · `app_data.html` · `app_guide.html` · `app_logs.html` · `app_fields.html`
**Verdict: (a) a cache.** Full evidence: `felhom.eu/documentation/audits/app-page-2026-10-15b/P0/VERDICT.md`.
**Deleted (4 templates)** `templates/app_info.html` · `logs.html` · `app_export.html` · `tier2_config.html`
— with the three page handlers that rendered them (`logsHandler`, `exportPageHandler`,
`tier2ConfigPageHandler`). The POST handlers beside them (`tier2ConfigSaveHandler`,
`appEmailToggleHandler`) stay, with their URLs.
**(b) is ruled out.** Exactly one guest ran 0.315.0 — demo-hp 9201 — on digest
`sha256:8887343011d6…`, identical to the registry's 0.315.0 at the time of measuring. The **global
floor is 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 are both on 0.314.0; Tester 1 (`c11-appliance`) was
unreachable and, by the floor, cannot be on 0.315.0.
**Modified (main ones)** `web/server.go` (routes + the GET-only redirects) · `web/handlers.go`
(`appDetailHandler` → `appCommonData`; `deployHandler` → `deployPageData` + a thin renderer) ·
`web/media_events.go` (`applyPlexClaim` extracted) · `web/tier2_config_handler.go` (redirect targets) ·
`templates/deploy.html` (install-only; 1197 → 544 lines) · `stacks.html`, `dashboard.html`,
`backups_apps.html`, `app_import.html` (links) · `stacks/metadata.go`, `metadata_i18n.go`,
`appinfo_address.go`, `manager.go` (the guide) · both locale files · `scripts/template_id_gate.py`,
`scripts/test_gate_decoys.py`, `scripts/i18n_go_keys.json`.
**The mechanism, in four measured facts:**
## 3. Commits on `main`
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**: measured 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 `max-age=3600` to `max-age=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.
**What is true now, and the caveat stated rather than glossed:** origin, edge and box agree
byte-for-byte (`sha256 c1774f63…` both sides) and all carry the rules; the page itself is never
cached (`DYNAMIC`). The specific stale object is **no longer observable** — the first probe forced
its revalidation, which is what `EXPIRED` records — so I cannot show a `HIT` whose body lacks
`.app-tabs`. 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. Boxes on 0.315.0, with digests
| Box | tag | digest | now |
|---|---|---|---|
| demo-hp 9201 | 0.315.0 | `8887343011d6` | **0.316.2** |
| demo-hp 9202 | 0.314.0 | — | unchanged |
| felhom-pve (N100) 9201 | 0.314.0 | — | unchanged |
| Tester 1 (`c11-appliance`) | unreachable | — | cannot be on 0.315.0 (floor 0.314.0) |
## 4. Files changed, commits on `main`
| Commit | What |
|---|---|
| `e66d466` | v0.315.0 — the slice: tabs, routes, the after-start registry, the guide, the Pi removal, the fixtures. One commit, because the template split, the routes and the fixtures cannot be green apart |
| `fe0228c` | style the tab bar; reuse the hero classes that already have rules (**found in a browser**) |
| `db52f5a` | close the log tab's `<script>`; `TestAppPage_ScriptTagsAreBalanced` (**found in a browser**) |
| `9cc7ffc` | build.sh refuses a repeated push; `assetTag` per build; every shell uses it; harness normalises it |
| `7f3e58f` | the one-column layout (the approved mockup) |
| `48d8674` | v0.316.0 CHANGELOG + the push-once fact in the rule file |
| `006ceab` | **v0.316.1** — the asset tag reaches the dashboard pages |
| `f908eee` | the settings tab loses the install page's boxes |
| `9e68e52` | two more data-tab leftovers |
| `8c7aa10` | **v0.316.3** CHANGELOG |
| felhom.eu `b6638ac4`, `22cd7c51` | the design docs, the audit, R-1080 closed, R-1081 filed |
Baseline `85f65c7` -> `db52f5a`. felhom.eu `06e7cf8c` -> `5d4538cc` (docs).
Main files: `controller/build.sh`, `scripts/test_build_push_once.sh`, `internal/web/server.go`,
`asset_tag_test.go`, `app_layout_test.go`, `templates/{style.css,app_shell,app_settings,app_data,app_guide,app_logs,app_fields,deploy}.html`,
both locale files, `i18n_parity_test.go`, `i18n_wiring_test.go`.
## 4. Tests and red-proofs
**New test groups (9 functions across 3 files), all through the production router where routing is
the subject.**
| Test | Asserts |
|---|---|
| `TestAppPage_DeployedDefaultsToSettings` | `/apps/{slug}` renders the settings tab (`data-tab="settings"` + `aria-selected="true"`), all four tabs present, Mukodes offers restart+stop and not start, **the header contains no `stackAction(`**, no Pi/x86 text |
| `TestAppPage_NotDeployedShowsGuideOnly` | no tab bar, `data-install-cta`, every tab suffix → 302 `/apps/grocy`; unknown app 404; unknown suffix 404 |
| `TestAppPage_OldURLsRedirectGETOnly` | the five old GETs → 302 with the right Location, query carried, `?raw=1` still text/plain, unknown name 404, **POST to `/backup` and `/app-email` reaches its handler** |
| `TestAfterStart_PlexThroughRouter` | POST `/apps/plex/after-start/PLEX_CLAIM` → the real registry → a fake Plex, by ADDRESS; the gate moves; the card disappears |
| `TestAfterStart_NoActionNoCard` | an unregistered field: no card, POST 404, **zero calls to the fake** |
| `TestAfterStart_RegistryAloneIsNotAnInvitation` | a REGISTERED action whose field the catalog does not declare: 404, zero calls, the gate untouched |
| `TestAfterStart_CardHiddenWhenNotNeeded` | registered + `Needed()==false` → no card |
| `TestAfterStart_PlexClaimOldRouteStillWorks` | `/apps/plex/plex-claim` runs the same entry |
| `TestInstallPage_BackGoesToApp` | Vissza and Mégsem href `= /apps/grocy`; the post-install links point at the tabs; unknown app 404 |
| `TestAppPage_EachTabRendersItsOwnContent` | one marker per tab, and **the log poller on the log tab only** |
| `TestGuide_RenderRules` | checked renders / empty does not / absent says so / `<script>` escaped / DOMAIN filled |
| `TestGuide_EnglishFallsBackFieldByField` | English FAQ + Hungarian sections; a blank English intro falls back; `checked_version` survives; the Hungarian page is unchanged |
| `TestGuide_*` (stacks, 8 funcs) | parse, the render gate, the overlay field by field, extras dropped+counted, `WithAddress` over every string, `Clone` not shared, and the catalog snapshot carries no guide yet |
**Red-proofs — each break seen failing, then reverted.**
## 5. Tests and red-proofs
| # | Break | Result |
|---|---|---|
| A | default tab → guide for a deployed app | `TestAppPage_DeployedDefaultsToSettings` FAILS |
| B | drop the not-deployed tab redirect | `TestAppPage_NotDeployedShowsGuideOnly` FAILS |
| C | drop `&& r.Method == http.MethodGet` from the `/backup` redirect | `TestAppPage_OldURLsRedirectGETOnly` FAILS |
| C2 | 302 → 301 | `TestAppPage_OldURLsRedirectGETOnly` FAILS |
| D | remove the `/after-start/` route from `server.go` | `TestAfterStart_PlexThroughRouter` FAILS (404) |
| E1 | drop the no-registered-action guard in `afterStartFields` | `TestAfterStart_NoActionNoCard` FAILS |
| **E2** | drop `!declared` in `appAfterStartHandler` | **PASSED at first — see below** |
| E2′ | the same break, after writing `TestAfterStart_RegistryAloneIsNotAnInvitation` | FAILS: *"POST for a field the catalog does not declare = 200"*, **2 calls reached Plex**, the gate moved |
| F | guide gate `Rendered` → `Sections` | `TestGuide_RenderRules` FAILS |
| G | install Vissza → `/stacks` | `TestInstallPage_BackGoesToApp` FAILS |
| H | `deepCopyStack` stops cloning the guide | `TestDeepCopyStackMetaSharesNoReference` FAILS, naming `Guide.Sections` and `Guide.FAQ` |
| I | remove `</script>` from `app_logs.html` again | `TestAppPage_ScriptTagsAreBalanced` FAILS: "logs: 3 `<script>` opened, 2 closed" |
| 1–3 | `Rendered()` without `checked_version`; `overlayGuide` replacing the FAQ whole; `WithAddress` skipping the guide | the three matching `TestGuide_*` FAIL |
| B1 | the whole 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 |
| 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` fails: empty buster on four pages |
| D1 | the auto-update 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 |
**E2 is the finding of this session.** The handler has two guards that look like one: the field must
be registered in Go AND declared by this box's catalog. Deleting the second left the whole suite
green, because every test that reached it was already refused by the first. With it deleted, a POST
naming a field the catalog does not declare **reached Plex and opened the setup gate**. The missing
test was written, the break repeated, and it now convicts. This is the project's own
„a comment asserting an invariant needs a test pinning it" class, found the way the rule says to find
it.
New: `TestBuildScript_RefusesExistingTag` (six cases, real script, fake docker),
`TestAssetTag_{ChangesPerBuild,EveryShellUsesIt,ReachesTheRenderedPage}`,
`TestAppPage_{OneColumnLayout,AutoUpdateSuccessIsMetaNotBanner,CalloutsOnlyForAskAndWarn}`.
**Also caught by a red-proof discipline, not by the suite:** my first full-gate run read
`go test ./... | grep -v ok | head -25`, whose exit code is `head`'s. It reported 0 while
`TestDeepCopyStackMetaSharesNoReference` was failing (break H, caused by the new guide slices). Re-run
capturing `go test`'s own `$?`: **`GO_TEST_RC=0`, 37 packages `ok`, zero `FAIL` lines.** The
„exit codes that lie" trap, hit and corrected in-session.
Full gate: `go build ./... && go vet ./... && go test ./...` → **exit 0, 37 packages ok, zero
failures**, measured with `go test`'s own exit code (not a pipeline's). All controller gates OK; 53
decoys behaved.
**TWO DEFECTS THE SUITE COULD NOT SEE, both found in a browser.** This is the case for Claude in
Chrome running on the workstation, and it is worth stating plainly: every endpoint-level marker
passed on both.
## 6. Deployed — and what is NOT
1. **The tab bar had no CSS and the logo was unsized.** The shell was written with new class names -
`app-hero`, `app-hero-logo`, `app-tabs` - and the first two duplicate names the design system
already has rules for (`app-info-hero`, `app-info-logo`), while the third had none at all. The
page answered 200, carried every `data-*` marker, and showed four bare underlined links and a
logo filling a third of the screen. Fixed in `fe0228c`: the shell reuses the existing hero
classes, and the tab bar is styled to design system v2 - measured live in the browser as
`display: flex`, `border-bottom 1px rgb(34,52,79)` (`--line`), active `rgb(46,168,245)`
(`--blue-bright`) on `rgb(15,27,46)` (`--bg-1`), radius 2px.
2. **The log tab's `<script>` was never closed, so the live poller was dead.** The body was lifted
out of `logs.html` by line range and the range stopped one line short. `html/template` parsed it,
the page rendered, and `TestAppPage_EachTabRendersItsOwnContent` found
`setInterval(window.fetchLogs` exactly where it expected it - **because the text was there.**
Chrome refused the block with "SyntaxError: Unexpected token '<'" and the log never refreshed.
Fixed in `db52f5a`, with `TestAppPage_ScriptTagsAreBalanced` red-proved.
**demo-hp guest 9201 runs `0.316.2`, healthy.** Tester 1 and demo-felhom untouched. The **floor stays
0.314.0** — raising it needs all three test boxes and a green box walk.
**The lesson, in the project's own terms:** a marker proves a string is present; it does not
prove the browser will execute it. The positive observable is
`typeof window.fetchLogs === "function"`, which is what the live check now reads.
**v0.316.3 is committed and pushed to git but NOT built.** Between the 0.316.2 build and this one,
another session's work in progress appeared in the shared DooPlex build clone (15 modified files,
the physical-disk storage work, R-1074..R-1079). The clean-tree gate forbids building over it and I
will not stash or revert another session's work. Every build I did make printed `porcelain=0` first.
## 5. Test count
What 0.316.3 adds, and therefore what the operator will NOT see tonight: on the data tab,
„Biztonsági mentés" is still drawn as a card, and the 2nd-copy heading still reads „2. mentés
beállítása — AdventureLog". Both are cosmetic; everything else is live.
`internal/web` + `internal/stacks` test functions: **+22** (13 new functions). Parity CASES: 145 -> 148.
## 7. The browser check, through the PUBLIC address
## 6. Deployed versions
Chrome on the workstation, `https://felhom.enkisfelhom.hu`, **normal loads — no hard refresh**:
| Box | Before | Now |
|---|---|---|
| demo-hp guest 9201 | 0.314.0 | **0.315.0**, `Up ... (healthy)` |
| Tester 1, demo-felhom | 0.314.0 | unchanged - not touched |
- The stylesheet URL is `?v=20540d81` (an 8-hex per-build tag, not a version, not empty), 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`; the column is
`max-width: 820px`; the hero has `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: **0 blue,
0 amber** — correct, nothing is asked and nothing is risky.
- **Console clean on a fresh load.**
Image `gitea.dooplex.hu/admin/felhom-controller:0.315.0`, built three times from a clean tree
(`git status --porcelain` empty and HEAD == origin/main each time) as the two browser findings were
fixed. **The floor is NOT raised**: per `.claude/rules/unprompted-work.md` item 6 that waits until it
has run on Tester 1, demo-hp and demo-felhom through the normal update path with a green box walk on
all three. demo-hp is one of the three, not the gate.
Screenshots: `felhom.eu/documentation/audits/app-page-2026-10-15b/browser/`.
**For anyone rebuilding the same version:** the stylesheet's cache key is `?v={{.Version}}` with
`max-age=3600`, so a second build of 0.315.0 serves new CSS under a key the browser already holds. It
cost twenty minutes here. It cannot reach a household - a released version is built once - so it is
recorded, not filed.
**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.
## 7. NOT yet live-validated
## 8. Fixtures replaced, by name
1. **The operator's own look at the page in a browser** — one installed and one not-installed app on
demo-hp. Asked for in `STATUS.md`. Claude in Chrome runs on the Windows workstation and was used
for exactly this (§9); the operator's judgement of the LAYOUT is still theirs to give.
2. **A real Plex claim through the new card.** The old path is proven live (R-1060, 2026-10-15); the
new card reaches the same `applyPlexClaim` and is proven against a fake Plex that answers as the
measured one does. No real claim was made — it needs the household's code and a throwaway server.
3. **A catalog guide rendered from a real template.** No template carries `app_info.guide` yet; the
render is proven against fixtures and a unit test.
Recaptured (the pages this task restyles, per the standing exception): `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 so against 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.
## 8. Evidence
## 9. NOT yet validated
`/mnt/5_hdd/felhom.eu/evidence/app-page-2026-10-15/` on DooPlex, copied off the boxes at the end of
each phase and before the scratch clone was deleted: `gates/` (the runner all green, 53 decoys,
`go test ./...` with its own exit code), `live/` (every page fetched in the endpoint walk, cookie
jars stripped) and `browser/` (four screenshots for the operator).
1. **The operator's look** at AdventureLog and Plex on demo-hp through the public address — asked
for in STATUS. The layout judgement is theirs.
2. **v0.316.3's two cosmetic fixes** (§6) — committed, unbuilt, so unseen.
3. **A blue callout in a real browser** (§7).
## 9. Live validation - demo-hp guest 9201, 0.315.0
## 10. Teardown — three layers
The OMV work another session is running tonight is on demo-hp's guests **9310/9311**; 9201 was free.
N100 was left alone (the media-stack session has it).
**Endpoint-level, authenticated, through the endpoints the UI invokes.** Every assertion is on an
ASCII `data-*` marker with a positive and a negative control - an accented Hungarian pattern through
`ssh -> pct exec -> bash` returns a false `0` that reads exactly like the markup being gone.
| Checked | Result |
|---|---|
| all five tab URLs on an installed app | 200 each |
| content per tab | run card / data tab / guide / poller each on exactly one tab; the bar on all four |
| the selected tab per URL | `/apps/x`->settings, `/settings`->settings, `/data`->data, `/guide`->guide, `/logs`->logs; `aria-current="page"` once |
| the header | **0** `stackAction(` before the hero; the Mukodes card offers restart+stop for a running app |
| a not-installed app | no tab bar, no run card, one Telepites; all four tab suffixes **302 -> `/apps/grocy`** |
| the five old URLs | 302 each, to `/logs`, `/data`, `/data`, `/settings`, `/install`; **0 of them 301**; `?raw=1` carried through; the tab answers `text/plain` |
| POST `/stacks/adventurelog/backup` | **303 -> `/apps/adventurelog/data?flash=flash.tier2.saved`** - the save ran and came back to the tab holding the form |
| after-start POSTs (with a CSRF token) | unknown field 404, unknown app 404, **a registered env var on the wrong app 404**; without a token 403, which is what proves the 404s are the handler's |
| Pi / x86 badges | 0 across every page fetched; `meta-badge` (22) and `Alkalmaz` (42) as positive controls |
| the guide | `data-guide-missing` on the guide tab, no rendered guide - no catalog template carries one yet |
| the install wizard | Vissza and Megsem both `href="/apps/grocy"` |
| negative controls | unknown app, unknown tab suffix and unknown stack name all 404 |
**Plex's card is absent, and the reason was read rather than assumed:** its template declares
`after_start` and `PLEX_CLAIM`, and its gate is `state: open` since 2026-10-15T08:45:53Z - the
operator's real claim (R-1060). `Needed()` is therefore false, which is the
`TestAfterStart_CardHiddenWhenNotNeeded` branch. A positive control would mean un-claiming the
operator's real Plex server; not done.
**The data tab's Tier-2 section, with both controls:** `adventurelog` and `bookstack` keep their data
on the system disk and get the honest `data-system-backup="same_disk"` sentence and no target picker
(the `app_data_not_hdd_same_disk` branch); `paperless-ngx`, `plex` and `calibre-web` are HDD apps and
each gets the real save form. A missing form on one app was a branch, not a defect - checked before
it was reported.
**In a real browser** (Claude in Chrome; four screenshots in `evidence/.../browser/`): the settings,
data and logs tabs and the not-installed view all render correctly; the tab bar's computed style
matches the design tokens; `window.fetchLogs` is a function on the log tab and refreshing the log
works; **the console is clean on a fresh load**. "Elso lepesek" on Grocy's page shows the household's
own address (`kamra.enkisfelhom.hu`), so R-498 travels through the new page.
## 9b. Teardown - three layers, stated
- **Machine:** nothing provisioned. The probe scripts, the fetched pages and the password file are
removed from guest 9201 and from demo-hp (0 leftovers, verified). The password went file->file, was
never printed, and no copy reached Windows or git.
- **Host:** the DooPlex scratch clone (`scratch/cc-apptabs`) and the `felhom.eu` sibling symlink the
shared gates need are deleted; the shared checkout two other sessions are using was never dirtied.
Evidence was copied off first.
- **Hub:** nothing created. No customer, no floor change.
## 10. Parity fixtures (the §9 exception), by name
**Removed (33).** `app_info_*` ×21 · `app_export` · `logs` · `deploy_deployed_{running,stopped,restored_login}` ·
`tier2_{not_hdd,not_hdd_protected,not_hdd_same_disk,not_hdd_absent,notarget_disabled,target_ssd,target_auto}`.
**Added (36).** `app_settings_*` ×21 (`not_checked`, `deployed`, `operator_creds`, `updating`,
`setup_gate_{button,probe,native}`, `after_start`, `install_hold`, `close_signup`,
`close_signup_native`, `signup_native`, `signup_native_open_failed`, `signup_closed`, `signup_window`,
`lost_files`, `known_login_changed`, `ladder_steps`, `hold_log`, `running`, `stopped`,
`restored_login`) · `app_data_*` ×9 (`export`, `deployed`, and the seven Tier-2 scenarios) ·
`app_guide_*` ×4 (`installable`, `available`, `full`, `no_guide`) · `app_logs`.
**Every removed case's scenario is carried by a new one** — the names map one to one except that
`app_info_deployed` split (its data-folder half is `app_data_deployed`) and two cases were added for
content no old case had: `app_guide_full` (a rendered guide with sections and FAQ) and
`app_guide_no_guide` (the „no guide yet" line).
**Changed, and NOT covered by the task's exception list (14).** The task's §9(b) names the pages it
expected to change. These fourteen also changed, and each is required by the task's own §2.6 and
Part 3 — a link change and a badge removal ARE byte changes:
- `deploy_new`, `deploy_new_blocked`, `deploy_new_memory_blocked` — the Pi badge removed (Part 3), the
Vissza/Mégsem retargeted (§2.5). *(In §9(b)'s `deploy_*` set.)*
- `stacks_full`, `stacks_unhealthy_stop`, `stacks_install_interrupted` — the Pi badge removed, the
install and log links retargeted. *(In §9(b)'s `stacks_*` set.)*
- `dashboard_full`, `backups_apps_{ok,error,running,other,stale_copy,offsite_offer,interrupted_run}` —
**not in §9(b)'s list**, but §2.6 requires exactly these links to be updated. Stated here rather
than quietly regenerated.
**Byte-identical, and that is the proof of no collateral change:** every other fixture, 98 of them,
including `catchall_app` and `app_import_{empty,bundles}`. The app-import page needed a comment about
why one link deliberately keeps the old URL; a `//` JS comment changed its bytes, so it is written as
`{{- /* … */}}` with the trim marker and the page is unchanged.
- **Machine:** nothing provisioned, no app installed, no throwaway Plex. Probe scripts and fetched
pages removed from DooPlex `/tmp`.
- **Host:** the scratch clone and the `felhom.eu` sibling symlink are deleted. The shared DooPlex
checkout was never written to by me — which is how I could tell the other session's work from
mine. `build.sh` in the build directory is now a symlink to the repo's (deliberate, §11), with the
stale copy kept beside it.
- **Hub:** nothing created; no floor change.
## 11. Observations
- **NOT-A-FINDING:** `page.title.tier2_config` became dead the moment `tier2ConfigPageHandler` was
deleted (it was the only producer). Removed from both locale files, from
`scripts/i18n_go_keys.json` and from `i18n_wiring_test.go`'s two lists in the same commit. Caused by
this change, so cleaning it is part of it, not scope creep.
- **NOT-A-FINDING (fixed without a row, the size rule):** `scripts/template_id_gate.py` was
file-scoped, so the shared `app_deploy_fields` partial made it convict ids that are demonstrably on
the page. It now follows `{{template}}` includes transitively. The hazard that buys is a VACUOUS
check, so it ships with a scope decoy — a reference planted in `deploy.html` to an id defined only
in `app_data.html`, a file `deploy.html` does not include — which the gate must still convict. It
does. 53 decoys green.
- **NOT-A-FINDING:** the i18n gate caught „Please" in the new English string (the copy style forbids
pleading); reworded before the commit.
- **FILED: R-1067** — the catalog still carries `pi_compatible:` and nothing reads it. Deliberately
left: a box on 0.314.0 would show „csak x86" on every app. Gated on the fleet floor reaching
0.315.0. (A row and not a session fix: it is another repo and it is blocked on the floor.)
- **FILED + CLOSED: R-1066** — no architecture document covered the app page. Closed by this task's
`02-controller-module-map.md` „The app page" and `10-localisation.md` §7 additions.
- **Capability map:** no row covers „the app page", so none moved. Said rather than assumed.
- **Register size: 156 before → 157 after; 1 opened (R-1067), 1 opened-and-closed (R-1066).**
- **System poster:** no fact in `felhom-system-poster.facts.md` changed (no machine, role, traffic
path, backup tier, time, retention, key or known gap). Neither file edited; nothing added to STATUS.
- **Behaviour change worth naming:** the initial-credentials and default-login cards used to render
only when `HasAppInfo` was true — incidental (the FileBrowser branch set `HasAppInfo = true` by hand
just to make its credentials show). On the settings tab they are no longer gated on catalog copy
existing. A login is not catalog prose.
- **FILED: R-1081** — the registry's `0.315.0` no longer holds the 0.315.0 release. **I did this**,
and the cause is worth stating plainly: I put the guard in `controller/build.sh` in the repo, but
the documented release command runs `$FELHOM_ROOT/build/felhom-controller/build.sh`, which was a
**separate copy taken 2026-07-18**. The very 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 a fourth
time, with v0.316.0's code under it. A guard added to a script the runbook does not execute is the
seam-built-but-never-wired class, and I shipped one while fixing another. Root cause fixed in the
session (symlink), then the refusal proven from the documented path. No box is affected.
- **NOT-A-FINDING (fixed in session):** the asset tag shipped dead in 0.316.0 — every shell asked for
it, the template test passed, and the dashboard path never set it, so the first live load rendered
`?v=` (empty, i.e. a constant). Found in a browser; `TestAssetTag_ReachesTheRenderedPage` now
asserts the rendered value, not the markup.
- **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:** `page.title.tier2_config`-style leftovers — the 2nd-copy heading's locale key
carries „— {{.DisplayName}}" because it was a page title. New key `app_page.second_backup`.
- **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;
nothing added to STATUS about it.
- **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.