83392064ae
gates / gates (push) Failing after 46s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
281 lines
20 KiB
Markdown
281 lines
20 KiB
Markdown
# REPORT — v0.315.0: one app page with four tabs (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 | — |
|
||
|
||
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`.
|
||
|
||
**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.
|
||
|
||
## 2. Files created / modified
|
||
|
||
**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`
|
||
|
||
**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.
|
||
|
||
**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`.
|
||
|
||
## 3. 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**) |
|
||
|
||
Baseline `85f65c7` -> `db52f5a`. felhom.eu `06e7cf8c` -> `5d4538cc` (docs).
|
||
|
||
## 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.**
|
||
|
||
| # | 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 |
|
||
|
||
**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.
|
||
|
||
**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.
|
||
|
||
**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.
|
||
|
||
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.
|
||
|
||
**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.
|
||
|
||
## 5. Test count
|
||
|
||
`internal/web` + `internal/stacks` test functions: **+22** (13 new functions). Parity CASES: 145 -> 148.
|
||
|
||
## 6. Deployed versions
|
||
|
||
| 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 |
|
||
|
||
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.
|
||
|
||
**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.
|
||
|
||
## 7. NOT yet live-validated
|
||
|
||
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.
|
||
|
||
## 8. Evidence
|
||
|
||
`/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).
|
||
|
||
## 9. Live validation - demo-hp guest 9201, 0.315.0
|
||
|
||
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.
|
||
|
||
## 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.
|