docs: the app page (controller v0.315.0) — design, the guide in the copy model, R-1066/R-1067
gates / gates (push) Successful in 6m35s
gates / gates (push) Successful in 6m35s
02-controller-module-map.md gains "The app page" under web/: the URL map, why the default tab is
RENDERED at the bare URL and not redirected to, why the tabs are links and not panels, the
302-never-301 rule for the old /stacks/{name}/... GETs and the untouched POSTs, the after-start
registry (and why its actions are Go, never catalog data), and the guide's checked_version gate.
Each marked [DESIGN]. There was no document covering the app page at all, which is why the six-page
sprawl and the "Vissza goes to the list" behaviour were never called wrong — R-1066, filed and
closed by the same change.
10-localisation.md §7 gains the guide block, including the one exception it has to state out loud:
guide sections and FAQ entries are matched BY INDEX, the only lists in the copy model that are,
because a section's title is the thing being translated and so there is no key to match on. Safe
only because the copy gate requires equal counts AND the fallback stays field by field.
R-1067 (P4, BLOCKED): the catalog still carries pi_compatible: and nothing reads it. Left on
purpose — a box on 0.314.0 would show "csak x86" on every app — and gated on the fleet floor
reaching 0.315.0.
STATUS.md: one line asking the operator to look at the new app page on demo-hp, and the controller
version line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117kRTSgs3rJWUFGxyoWN6Y
This commit is contained in:
@@ -397,6 +397,86 @@ by its words — the earlier Hungarian-substring test would have moved the warni
|
||||
page the day the sentence was translated (R-553, pinned by `TestR553_DiskWarningPlacementSurvivesWordingChange`).
|
||||
A new inline warning needs its own kind, not a text match.
|
||||
|
||||
#### The app page — one page, four tabs (v0.315.0)
|
||||
|
||||
**[DESIGN]** One app is ONE page. Until v0.314.0 it was six — details, install/settings, logs,
|
||||
export, the per-app backup panel, and a details card for moving data — and every „Vissza" on them
|
||||
went to the app LIST, so there was no way back to the app you were looking at.
|
||||
|
||||
| URL | Tab | Holds |
|
||||
|---|---|---|
|
||||
| `/apps/{slug}` | — | the default tab |
|
||||
| `/apps/{slug}/settings` | „Beállítások" | „Működés" (Start / Restart / Stop), the cards that want something from the household, the install-time and optional settings |
|
||||
| `/apps/{slug}/data` | „Adatok és mentés" | the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export |
|
||||
| `/apps/{slug}/guide` | „Útmutató" | tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs link |
|
||||
| `/apps/{slug}/logs` | „Napló" | the log |
|
||||
| `/apps/{slug}/install` | — | the install wizard — a page, not a tab |
|
||||
|
||||
**[DESIGN] The default tab is RENDERED at the bare URL, never redirected to.** `/apps/{slug}` serves
|
||||
the settings tab's content for an installed app and the guide for a not-installed one, with the URL
|
||||
unchanged, so the address an alert, an e-mail, a bookmark or the app list carries stays the app's one
|
||||
address. A redirect would make every such link bounce and would put a tab name into addresses nobody
|
||||
chose.
|
||||
|
||||
**[DESIGN] The tabs are links, not panels.** A link in an alert or an e-mail has to land ON a tab,
|
||||
and the log tab's poller must not run while the household reads the settings; one page with hidden
|
||||
panels gives neither. Each tab is an `<a>` with `aria-current`, and the log poller is inside the log
|
||||
tab's template.
|
||||
|
||||
**[DESIGN] A not-installed app has one view and one URL.** No tab bar — a bar of one tab is
|
||||
furniture — and every tab suffix answers 302 to the base.
|
||||
|
||||
**[DESIGN] The old URLs are a bridge, and only for reads.** `/stacks/{name}/{deploy,logs,export,backup}`
|
||||
GET answer **302** to the tab that now holds them, with the query string carried through;
|
||||
`/deploy` goes to the settings tab for an installed app and to the install wizard for one that is
|
||||
not. **302 and never 301:** a 301 lives in the browser's cache beyond anyone's reach, and these
|
||||
paths are a bridge for one release, not an address. `name → slug` is resolved through the stack
|
||||
list, never assumed equal — the two are equal for every catalog app today, which is exactly why
|
||||
assuming it would break on the first app where they differ, on a page nobody tests.
|
||||
|
||||
**Every POST on those paths is untouched.** The backup form, the app-email toggle and the
|
||||
`auto-field/reveal` endpoint keep their URLs and their handlers; the redirect cases are matched on
|
||||
`GET`. Redirecting a POST would turn a save into a page fetch and lose the household's input.
|
||||
|
||||
#### After-start fields — the catalog asks, Go answers (v0.315.0, D2)
|
||||
|
||||
**[DESIGN]** Some values can only be given once the app runs: Plex's claim code is minted on plex.tv
|
||||
and handed to a server that is already up (R-1028/R-1060). A catalog field marked `after_start: true`
|
||||
is skipped by the install wizard and gets a card on the settings tab.
|
||||
|
||||
**[DESIGN] The catalog declares THAT a value is asked, and what to call it. It never declares what
|
||||
happens to the value.** The action is Go, in a registry keyed by `slug + "/" + ENV_VAR`
|
||||
(`web/after_start.go`), with `Needed(stack)` and `Apply(…, value)`. The household's text is therefore
|
||||
an argument to a function in the controller's own source, and never a fragment of a command, a URL
|
||||
or a container name that a catalog file chose. Adding a template cannot add an action.
|
||||
|
||||
**[DESIGN] Both halves are required, and each is checked on its own.** A field the catalog declares
|
||||
with no registered action renders no card, answers its POST with 404, and logs one WARN naming the
|
||||
env var KEY — a field that silently does nothing is the kind of absence nobody notices. An action
|
||||
registered for a field **this box's catalog does not declare** is equally refused: the registry alone
|
||||
is not an invitation. The second check looks redundant and is not — it was deletable with the whole
|
||||
suite green until a test was written for it, and without it a POST for an undeclared field reached
|
||||
Plex and opened the setup gate.
|
||||
|
||||
The value is a credential and is never logged. `/apps/plex/plex-claim` remains and runs the same
|
||||
registry entry, so a page open across the update still works.
|
||||
|
||||
#### The catalog's user guide (v0.315.0)
|
||||
|
||||
**[DESIGN]** `app_info.guide` carries `sections` (title, optional `intro`, `steps`) and `faq`
|
||||
(`q`, `a`), rendered on the guide tab. The copy model is `10-localisation.md` §7; the guide block is
|
||||
described there.
|
||||
|
||||
**[DESIGN] `checked_version` is the render gate, and it is not copy.** It names the image tag the
|
||||
guide's author actually clicked through. A guide is rendered only when it is set; otherwise the tab
|
||||
shows its other content and says a guide is still to come. A guide walking a household through a
|
||||
screen two releases old is worse than no guide, and „someone wrote this against a version" is the
|
||||
only cheap evidence that it was ever true. Being outside the copy model is deliberate: a translator
|
||||
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.
|
||||
|
||||
### `scripts/`
|
||||
| File | Class | Reason | Risk |
|
||||
|---|---|---|---|
|
||||
|
||||
@@ -248,6 +248,45 @@ reached only through `LocalizeStacks` / `LocalizeStackPtr` / `Stack.MetaFor`.
|
||||
still looks right.
|
||||
- **`For` never writes through.** The metadata lives in the stack manager's cache and is shared by
|
||||
concurrent requests; an in-place merge would put one household's language on another's page.
|
||||
|
||||
**[DESIGN] The guide block (v0.315.0).** `app_info.guide` joins the copy model with one exception it
|
||||
has to state out loud:
|
||||
|
||||
```yaml
|
||||
app_info:
|
||||
guide:
|
||||
checked_version: "1.2.3" # NOT copy — the render gate; see below
|
||||
sections:
|
||||
- title: "Első bekapcsolás"
|
||||
intro: "…" # optional
|
||||
steps: ["…", "…"]
|
||||
faq:
|
||||
- q: "…"
|
||||
a: "…"
|
||||
i18n:
|
||||
en:
|
||||
app_info:
|
||||
guide:
|
||||
sections: [...] # same shape; checked_version is NOT overlaid
|
||||
faq: [...]
|
||||
```
|
||||
|
||||
- **`checked_version` is not copy and is not overlaid.** It names the image tag the guide's author
|
||||
clicked through, and the guide renders only when it is set (`02-controller-module-map.md`, „The
|
||||
catalog's user guide"). A translator does not vouch for a version, so an overlay can neither set
|
||||
nor clear it.
|
||||
- **Sections and FAQ entries are matched BY INDEX — the only lists here that are.** The rule above
|
||||
says every list is matched by its own key, and a guide section has no key to match on: its title
|
||||
is the very thing being translated, so a `match_title` would have to be kept in step with the
|
||||
Hungarian by hand, which is the drift the key rule exists to prevent. Index matching is safe here
|
||||
for two reasons that must both hold: the catalog's copy gate requires a translated guide to carry
|
||||
the same number of entries in the same running order, and **the fallback is still field by
|
||||
field**, so a misaligned entry shows the Hungarian one and never a sentence nobody wrote.
|
||||
- **`steps` replaces whole**, like the other prose lists, and for the same reason.
|
||||
- An English list **longer** than the Hungarian one is ignored past the Hungarian count, with one
|
||||
WARN naming the app and the counts (`Metadata.GuideOverlayExtras`, logged by the page that renders
|
||||
the guide — `For` runs per request and holds no logger). A silently dropped translation is exactly
|
||||
the kind of absence nobody notices.
|
||||
- **Two copy producers have no request and therefore no language** — the integration rows
|
||||
(`internal/integrations`) and the initial-credentials note (`internal/stacks/initialcreds.go`).
|
||||
Both are re-taken from the localised metadata in the handler.
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-10-15 (evening) — controller v0.315.0, the app page
|
||||
|
||||
| Row | What | Closed | Evidence |
|
||||
|---|---|---|---|
|
||||
| **R-1066** | **No architecture document covered the app page.** (P3) `02-controller-module-map.md` classified `templates/` as PORT and said nothing about what an app's page IS, so the six-page sprawl, the „Vissza goes to the list" behaviour and the Plex-only claim box were all undocumented — there was nothing a reviewer could have read to call any of them wrong. Found while specifying the rebuild. | CLOSED 2026-10-15 — **controller v0.315.0**: `02-controller-module-map.md` gains „The app page" under `web/` (the URL map, the rendered-not-redirected default tab, the 302-never-301 redirect rule, the after-start registry and why its actions are Go, the guide's `checked_version` gate), and `10-localisation.md` §7 gains the guide block with its index-matching exception. Each marked `[DESIGN]`. | `felhom-controller/REPORT.md`, `architecture/02-controller-module-map.md` „The app page" |
|
||||
|
||||
## 2026-10-15 (evening) — controller v0.314.0, the first release under decision 250 (the floor raised by CC's token)
|
||||
|
||||
| Row | What | Closed | Evidence |
|
||||
|
||||
@@ -124,7 +124,7 @@ stopping line that lies.
|
||||
| **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC |
|
||||
| **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator |
|
||||
|
||||
## Apps & catalog — 14 rows (P2 1, P3 8, P4 5)
|
||||
## Apps & catalog — 15 rows (P2 1, P3 8, P4 6)
|
||||
|
||||
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
@@ -141,6 +141,7 @@ stopping line that lies.
|
||||
| **R-707** | Apps & catalog | P4 | **[P2] 37 apps still start with a login a stranger can take (`09` §3 decision 45).** Audit of all 53 apps: `app-catalog-felhom.eu/FIRST-ADMIN.md` (class, fix route, status, measured or read). Open: **3 hard-coded defaults** — calibre-web (`admin / admin123`, measured working on demo-hp and 9202; its own `cps.py -s` route needs a generated password WITH a special character — our generator is letters+digits, a controller change), mealie (`changeme@example.com / MyPassword`), wger (`admin / adminadmin`); **34 open first-run screens** (the first visitor creates the admin: actualbudget, adventurelog, audiobookshelf, calcom, docmost, emby, ghost, gitea, gramps-web, home-assistant, homebox, immich, jellyfin, komga, n8n, navidrome, opengist, outline, papra, plant-it, radarr, rallly, recipe-importer, romm, seerr, sonarr, sparkyfitness, tandoor, termix, uptime-kuma, vikunja, wanderer, wishlist, zipline). **Stale notes:** romm's `default_creds` `admin / admin` answers 401 on demo-hp (like a wrong password) — the page now warns with a login that does not exist; zipline's looks stale too. **Measured on demo-hp 2026-09-28 (read-only):** bookstack's default still logs in on the INSTALLED app (the fix is for new installs; the page now warns). Each fix: route (a) env or (b) the app's own CLI/API via `after_install:`, proven on 9202 with the default failing and the generated password working; route (c) a page sentence. Several sessions (operator, 2026-09-28). **2026-09-29 (controller v0.280.0, catalog `d0e7e2e`):** every class-3 app fixed — mealie, wger, calibre-web by `after_install` (calibre-web with the new `password:24:special`), proven on 9202 fresh installs (`audits/login-gate-2026-09-29/D/`); the setup gate (decision 46, spike PASSED) built and live on immich, n8n, audiobookshelf (probes measured) and uptime-kuma (button) (`…/C/`); romm's and zipline's stale notes removed. **Left: 30 class-4 apps** — gate each (probe measured on 9202 where one exists — 11 upstream candidates listed in `…/B/B-VERDICT.md` §3; the button otherwise). **2026-09-29 afternoon (controller v0.281.0, catalog `6faf432`):** 28 more class-4 apps gated — 32 of 34 — each proven on 9202 (`audits/gate-rollout-2026-09-29/`B): stranger → gate page / 401, household reached the first-setup screen, the gate opened (9 by a measured probe, the rest by the press), the app answered after. seerr, outline, rallly: gated, their opening needs a media server / e-mail (not proven). **Left:** wanderer (R-714); plant-it is not installable. | **NARROWED** (2026-10-03 triage: the row's verdict was finished, but it names open work no other row carries — seerr, outline and rallly are gated, but the gate OPENING is not proven (needs a media server / e-mail)) — **CLOSED — 2026-09-29 (the rest → R-714)** | — | — | CC |
|
||||
| **R-905** | Apps & catalog | P4 | **wger's collected style files (283 MB) go into every wger backup, though the app rebuilds them at every start.** Since R-762's fix (`DJANGO_DEBUG=False` + `collectstatic`, catalog `cf1ed43`) the static files sit in a named volume that the backup legs copy like data (measured 283 MB, `audits/design-build-2026-10-06/`); `collectstatic` regenerates them, so they are rebuildable bytes in every Tier-1 unit, Tier-2 copy and off-site snapshot. wger is `lifecycle: hidden` and no box runs it (hub read 2026-10-08). Options to weigh: a non-backed-up volume class for regenerable data, or an anonymous volume. Found by the R-762 helpers 2026-10-08. | **OPEN — needs a design before wger is shown** | — | Design the „regenerable volume" exclusion (catalog + controller backup legs) | CC |
|
||||
| **R-995** | Apps & catalog | P4 | **Removing an app through the product can leave unnamed Docker volumes and an empty import folder behind.** MEASURED 2026-10-13 on demo-felhom after 61 removals (`documentation/audits/sizing-2026-10-13/METHOD.md` „Teardown”, `evidence/leftover-volumes-after-bench.txt`): 8 unnamed volumes (4–72 KB; immich 1, romm 2, privatebin 3, onlyoffice 2 — volumes the images declare themselves) and `userdata/import/{paperless,calibre,grimmory}` (empty, created by the deploy). The removal answered 200 and its own leftover check was clean, because it looks only at labelled volumes. No data risk; disk and tidiness only. Removed by name on the bench. | **READY** — filed 2026-10-13 | — | Make the removal account for the containers' unnamed volumes (and an empty import folder it created), with a test that deploys an image that declares a VOLUME | CC |
|
||||
| **R-1067** | Apps & catalog | P4 | **The catalog still carries `pi_compatible:` on every template, and nothing reads it any more.** controller v0.315.0 removed the „Pi kompatibilis" badge and the „csak x86" tag from every page (Raspberry Pi is no longer supported, so one was noise and the other was a warning on almost every app). The `ResourceHints.PiCompatible` struct field and the catalog key were deliberately LEFT: a box still on 0.314.0 reads the key, and removing it would make that box show „csak x86" on every app. **Gated on the floor:** once the fleet floor is at or above 0.315.0, drop `pi_compatible:` from the templates and the field from `stacks.ResourceHints` in the same pair of commits. Harmless until then — an unread key costs nothing. | **BLOCKED** — filed 2026-10-15 | the fleet floor reaching 0.315.0 | Drop the catalog key and the struct field; grep the hub and the website for Pi wording at the same time (none found 2026-10-15) | CC |
|
||||
| **R-1043** | Apps & catalog | P4 | **Sonarr, Radarr and Prowlarr build their login redirect as an absolute `http://` URL.** They trust forwarded headers only from their „Trusted networks” (the household set `192.168.0.0/24`; traefik is `172.18.x`), so they ignore `X-Forwarded-Proto: https` and answer `Location: http://<name>/login` to an https visitor. Since controller v0.312.0 that hop is sent back to https by traefik (a GET, no password), so it is cosmetic; a template could seed traefik's network as trusted. (`audits/https-only-2026-10-14/``1A-3-4.txt`) | **OPEN** — filed 2026-10-14 | — | Seed the docker network as a trusted proxy in the *arr templates (with the R-1030 seed), measure the redirect | CC |
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user