Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
20 KiB
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.
-
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 everydata-*marker, and showed four bare underlined links and a logo filling a third of the screen. Fixed infe0228c: the shell reuses the existing hero classes, and the tab bar is styled to design system v2 - measured live in the browser asdisplay: flex,border-bottom 1px rgb(34,52,79)(--line), activergb(46,168,245)(--blue-bright) onrgb(15,27,46)(--bg-1), radius 2px. -
The log tab's
<script>was never closed, so the live poller was dead. The body was lifted out oflogs.htmlby line range and the range stopped one line short.html/templateparsed it, the page rendered, andTestAppPage_EachTabRendersItsOwnContentfoundsetInterval(window.fetchLogsexactly where it expected it - because the text was there. Chrome refused the block with "SyntaxError: Unexpected token '<'" and the log never refreshed. Fixed indb52f5a, withTestAppPage_ScriptTagsAreBalancedred-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
- 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. - 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
applyPlexClaimand 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. - A catalog guide rendered from a real template. No template carries
app_info.guideyet; 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 thefelhom.eusibling 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)'sdeploy_*set.)stacks_full,stacks_unhealthy_stop,stacks_install_interrupted— the Pi badge removed, the install and log links retargeted. (In §9(b)'sstacks_*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_configbecame dead the momenttier2ConfigPageHandlerwas deleted (it was the only producer). Removed from both locale files, fromscripts/i18n_go_keys.jsonand fromi18n_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.pywas file-scoped, so the sharedapp_deploy_fieldspartial 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 indeploy.htmlto an id defined only inapp_data.html, a filedeploy.htmldoes 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" and10-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.mdchanged (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
HasAppInfowas true — incidental (the FileBrowser branch setHasAppInfo = trueby 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.