v0.315.0: one app page with four tabs; the guide the catalog can carry; no Raspberry Pi badge
gates / gates (push) Failing after 47s

An app was six pages — 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. It is now ONE page with four tab URLs under /apps/{slug}/,
one shared header and one set of banners.

  settings  Mukodes (Start/Restart/Stop, Stop confirms) + the cards that want something from the
            household + the install-time and optional settings. The default for an installed app,
            RENDERED at the bare URL and never redirected to.
  data      the data folders, move to another drive, the nightly copy, the 2nd-copy setting, export
  guide     tagline, screenshots, use cases, first steps, the catalog's guide, the FAQ, the docs
            link — and the WHOLE page for an app that is not installed
  logs      the log, and the only tab whose poller runs

The tabs are links, not panels: a link in an alert has to land ON a tab, and the log poller must not
run on the other three. Start/Restart/Stop left the page header, where they read as menu buttons.

The old /stacks/{name}/{deploy,logs,export,backup} GETs answer 302 — never 301, which a browser
caches beyond anyone's reach — to the tab that now holds them, query string carried through, with
name->slug resolved through the stack list rather than assumed equal. Every POST on those paths is
untouched: the backup form, the app-email toggle and the reveal endpoints keep their URLs.

A value asked AFTER the app starts is now generic (D2). The catalog marks a field `after_start: true`
and supplies its label; the ACTION is Go, in a registry keyed by slug + env var. The catalog can
never declare what runs — a declared field with no registered action renders nothing, POSTs 404 and
logs one WARN with the env var KEY, never the value. A red-proof found the second half of that guard
unpinned: deleting the "the catalog must have DECLARED this field" check left the suite green, and a
POST for an undeclared field then reached Plex and opened the setup gate.
TestAfterStart_RegistryAloneIsNotAnInvitation pins it.

New catalog block app_info.guide (sections + faq + checked_version), parsed, localised field by
field and rendered with the household's own address filled in. It renders only when checked_version
names the image tag its author clicked through. No template carries one yet.

The Pi and "csak x86" badges are gone; PiCompatible and the catalog key stay this release, or a box
on 0.314.0 would read "csak x86" on every app (R-1067).

template_id_gate.py now follows {{template}} includes, with a scope decoy proving the walk did not
become vacuous.

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 18:37:50 +02:00
parent 85f65c70d8
commit e66d4669d6
120 changed files with 16239 additions and 9866 deletions
+62 -3
View File
@@ -336,6 +336,65 @@ in `internal/agentapi/diskverdict.go` together with the provenance of 64 / 55 /
The controller manages Docker Compose stacks through a complete lifecycle: catalog sync, first-time deployment, runtime operations, and deletion.
#### The app page — one page, four tabs (v0.315.0)
One app is ONE page. Until v0.314.0 it was six (details, install/settings, logs, export, per-app backup, and a details
card for moving data), and every „Vissza" went to the app LIST, so there was no way back to the app.
| URL | Tab | What is on it |
|---|---|---|
| `/apps/{slug}` | — | the default: the settings tab when installed, the guide when not. **Rendered, not redirected** — the app's address stays its address |
| `/apps/{slug}/settings` | „Beállítások" | „Működés" (Start / Restart / Stop, Stop confirms), the cards that want something from the household (setup gate, after-start fields, sign-up window, first login), then the generated values, the install-time values read-only, the optional settings, e-mail, integrations, geo |
| `/apps/{slug}/data` | „Adatok és mentés" | „Hova tegyem a fájlokat?", move to another drive, the nightly copy, the 2nd-copy setting, export |
| `/apps/{slug}/guide` | „Útmutató" | tagline, screenshots, what it is for, first steps, the catalog's guide, the FAQ, the docs link. For a NOT-installed app this is the whole page, with „Telepítés" |
| `/apps/{slug}/logs` | „Napló" | the log, and the only tab whose poller runs |
| `/apps/{slug}/install` | — | the install wizard (a page, not a tab). Vissza and Mégsem go to the APP |
**The tabs are links, not panels.** A link in an alert or an e-mail has to land ON a tab, and the log poller must not
run on the other three; one page with hidden panels gives neither.
**A not-installed app has one view and one URL** — no tab bar, and every tab suffix answers 302 to the base.
**The old URLs are a bridge, reads only.** `/stacks/{name}/{deploy,logs,export,backup}` GET answer **302** (never 301:
a cached 301 cannot be withdrawn) to the tab that holds them, with the query string carried through; `name→slug` is
resolved through the stack list, never assumed equal. Every **POST** on those paths is untouched — the backup form, the
app-email toggle and the reveal endpoints keep their URLs and their handlers.
Code: `internal/web/app_page.go` (routing, the tabs, the redirects), `templates/app_shell.html` (the one header, the
banners, the tab bar), `app_settings.html` / `app_data.html` / `app_guide.html` / `app_logs.html`, and
`app_fields.html` — the configuration fields, shared byte-for-byte by the install wizard and the settings tab so the
two cannot drift.
#### „Még egy lépés" — a value asked AFTER the app starts (v0.315.0)
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. The catalog marks such a field `after_start: true`; the wizard skips it and the settings tab grows a card.
**The catalog declares THAT a value is asked, its label and its placeholder. It never declares what happens to the
value.** The action is Go, in a registry keyed by `slug + "/" + ENV_VAR` (`internal/web/after_start.go`), so the
household's text is an argument to a function in this repository and never a fragment of a command, a URL or a
container name a template chose. A declared field with **no registered action renders no card, answers its POST with a
404 and logs one WARN naming the env var KEY** — the value is a credential and is never logged.
`POST /apps/{slug}/after-start/{ENV}` (CSRF, JSON `{value}`). `/apps/plex/plex-claim` remains and runs the same entry,
so a page open across the update still works. Registered today: `plex/PLEX_CLAIM`.
#### The catalog's user guide (v0.315.0)
`app_info.guide` carries `sections` (title, optional intro, steps) and `faq` (q, a), rendered on the guide tab with the
household's own address filled into every string (R-498).
**It renders only when `checked_version` names the image tag its author actually clicked through.** A guide walking the
household through a screen two releases old is worse than no guide, so an unchecked one is not shown and the page says
one is still to come. `checked_version` is not copy: a translation can neither set nor clear it.
Everything is plain text through `html/template` — no markdown, no raw HTML; a step containing a tag is shown as the
characters the author typed. English overlays field by field; sections and FAQ entries are matched BY INDEX, the only
lists in the copy model that are (a section's title is the thing being translated, so there is no key to match on), and
an English list longer than the Hungarian one is dropped past the Hungarian count with one WARN.
No catalog template carries a guide yet — the schema and the rendering are here, the content is a separate task.
#### Git Sync (`internal/sync/`)
The app catalog lives in a separate Git repository. The controller:
@@ -456,7 +515,7 @@ visibility is parked under the SSO arc.
Running apps on the Vezérlőpult now show a "Megnyitás ↗" button that opens the app's subdomain in a new tab. The `Subdomains` map is built in `dashboardHandler` from `app.yaml` env or metadata fallback.
The optional `.felhom.yml` **`open_path`** field (`Metadata.OpenPath`) is appended to that URL for apps whose UI isn't at `/` — e.g. `gokapi` → `/admin`, `ghost` → `/ghost/`. Empty = bare root. Rendered via `.Meta.OpenPath` in `dashboard.html`, `deploy.html`, and `app_info.html` (the same field flows through all three open-link sites; no handler change needed). Must start with `/`; cosmetic only (does not affect routing).
The optional `.felhom.yml` **`open_path`** field (`Metadata.OpenPath`) is appended to that URL for apps whose UI isn't at `/` — e.g. `gokapi` → `/admin`, `ghost` → `/ghost/`. Empty = bare root. Rendered via `.Meta.OpenPath` in `dashboard.html` and `app_shell.html` (the app page's shared header, v0.315.0 — it replaced `app_info.html`, and `deploy.html` no longer carries an open link because the install page is never an installed app). The same field flows through both open-link sites; no handler change needed. Must start with `/`; cosmetic only (does not affect routing).
#### App lifecycle — withdrawing an app without orphaning anyone (v0.158.0)
@@ -795,7 +854,7 @@ state the containers are in at that moment; whether the app works is the health
#### App Info Pages
Each app can define rich metadata in `.felhom.yml`:
- `app_info`: tagline, use_cases, first_steps, prerequisites, default_creds, docs_url
- `app_info`: tagline, use_cases, first_steps, prerequisites, default_creds, docs_url, add_people, `guide` (v0.315.0 — `checked_version`, `sections[]`, `faq[]`; see „The catalog's user guide")
- `optional_config`: groups of post-deploy configurable env vars (e.g., API keys for metadata providers)
- `resources`: mem_request, mem_limit, pi_compatible, needs_hdd, hungarian_ui
- `initial_credentials` (v0.84.0): for apps that **auto-generate a first-login password into a file** at
@@ -1124,7 +1183,7 @@ Per-app export creates a self-contained `.fab` file (tar.gz, optionally encrypte
**Browser upload (v0.128.0 — the download's return leg):** the `/import` page uploads a `.fab` straight from the browser. Chunked because the Cloudflare tunnel caps request bodies at ~100 MB (probed live 2026-07-13: 120 MiB → edge 413, 80 MiB → origin): JS slices the file into 64 MiB chunks (strictly sequential offsets; one retry per chunk re-synced from the 409 `received_bytes` echo), the server streams each to a `.part-<random>` file in the DEFAULT drive's exports dir (`io.Copy`, 96 MiB per-request cap, free-space gate = size + 1 GiB), finalize checks the exact declared size, fsyncs and atomically renames (collision → lowest-free `"name (N).fab"`). Single-flight; no client-side hash (the .fab format self-validates at import); in-memory state — startup GC sweeps `*.part-*`, 15-min idle timeout aborts server-side. The scan + validation + import pipeline are untouched. `handler_export_upload.go`.
**UI:** Export button on app info page, the "Hordozható mentéscsomag (.fab)" section on `/backups/restore` (per-app download + batch), standalone import page at `/import` (upload zone + drive-scanned bundle list).
**UI:** the export form is the last section of the app page's „Adatok és mentés" tab (v0.315.0; it was a page of its own at `/stacks/{name}/export`, whose GET now 302s there), the "Hordozható mentéscsomag (.fab)" section on `/backups/restore` (per-app download + batch), standalone import page at `/import` (upload zone + drive-scanned bundle list).
---