Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
19 KiB
NEW-APP CHECKLIST — what a new catalog app must have measured before it reaches the live catalog
Operator request 2026-10-01: before new apps are added, a checklist every new app passes. Reviewer's draft the same day, reviewed and piloted on wger by CC (
felhom.eu/documentation/audits/new-app-checklist-2026-10-01/). The gate:scripts/check-onboarding.py(incatalog_gates.py --fast, so the pre-push hook and CI run it).
What this is. Every check below exists because something went wrong on a real app — the why column names it.
The filled-in copy per app is the app's onboarding record, onboarding/<app>.md, started by copying
onboarding/_TEMPLATE.md. A new template directory without a complete record is refused by the gate.
How an item is answered. One line per id in the record: <id> | done | <evidence path>, <id> | n/a | <reason>
or <id> | open | <what is missing>.
- done names evidence that exists: a non-empty file, or a directory holding one. Paths are written from the
workspace root —
app-catalog-felhom.eu/…orfelhom.eu/documentation/audits/…. Several paths: separate them with;. A note may follow the path(s) after—. - n/a carries a reason of at least four words.
- open blocks publishing.
Measured, not read. "Measured" means on the bench (LXC 9401 on demo-hp) or on scratch guest 9202 (drill catalog
only, 09 §6.5) through the product. Upstream reading may PLAN a test; it never CLOSES an item. Where the how says
"read", reading is the test (a licence, a tag list).
The since column. The date an id joined the list. A record carries every id whose since is on or before the
record's opened: date; an id added later binds only apps opened after it. A record may answer a newer id anyway.
The 53 apps in the catalog on 2026-10-01 are exempt (operator default, may be reversed); what the catalog
already shows for them is onboarding/EXISTING-APPS-GAPS.md. An exempt app's record, if one exists, must still be
well-formed, and may say open.
0. Fit — should we offer it at all?
| id | since | Check | How | Why (what went wrong before) |
|---|---|---|---|---|
| 0.1 | 2026-10-01 | Open-source licence; we pull the upstream image, never redistribute it | read the repo's licence file | the business sells installation, not software |
| 0.2 | 2026-10-01 | Upstream is alive: a release in the last 6 months, issues answered | read the repo | plant-it is lifecycle: abandoned (templates/plant-it/.felhom.yml) |
| 0.3 | 2026-10-01 | An official image with version tags (not latest only), amd64 (+ arm64 if pi_compatible) |
docker manifest inspect <image>:<tag> — the tag list can be stale (REUSE.md §2 "Image pinning") |
pins, digests and the ladder need real tags |
| 0.4 | 2026-10-01 | Telemetry / phone-home — on by default? a switch to turn it off? Start-time downloads (exercise sync, model fetch)? | read the entrypoint (1.7) for start-time network jobs; on 9202, the app's log of its first start | data sovereignty is the pitch |
| 0.5 | 2026-10-01 | Needs the internet at runtime (claim tokens, first-start downloads)? | docs + the first start on 9202 | plex (PLEX_CLAIM), adventurelog's world data (09 decision 41), immich's geodata import (R-732) |
| 0.6 | 2026-10-01 | Needs ports other than HTTP(S)? The tunnel carries HTTP only — say what the household loses | compose + docs | gitea SSH, DLNA, game servers |
| 0.7 | 2026-10-01 | Phone / desktop apps: which login route do they call, and does it work through the tunnel and the setup gate? | docs for the route; then that route with curl through traefik on 9202 (3.9) — a real client only if one is at hand |
wger's phone-app login route answered 500 while the web login worked (R-737); a gated app is unreachable for a phone app (09 decision 46) |
| 0.8 | 2026-10-01 | What a household gets from it, in one sentence (Hungarian) | — | the catalog card and use_cases |
| 0.9 | 2026-10-01 | The app does not call ITSELF at its public name (server-side), or the bench override is recorded | the env/config the server reads for its own URL; then the bench start | wanderer cannot run on the bench at all (R-739) — no bench, no update proof |
1. Images, the start command and the database
| id | since | Check | How | Why |
|---|---|---|---|---|
| 1.1 | 2026-10-01 | Every image pinned to a concrete tag that resolves | gates image-pins + image-resolvable (catalog_gates.py <app>) |
:latest breaks restore fidelity |
| 1.2 | 2026-10-01 | Database engine and major = what the app's own upstream compose runs | upstream compose at the pinned tag | 09 decisions 37/42 (one conversion, not two) |
| 1.3 | 2026-10-01 | MariaDB sidecar: MARIADB_AUTO_UPGRADE=1; PostgreSQL 18: mount at /var/lib/postgresql |
compose (no gate checks either today) | R-459; PG 18 refuses /var/lib/postgresql/data (CLAUDE.md engine rule) |
| 1.4 | 2026-10-01 | The app migrates its own database at start, or the switch that makes it do so is set | 1.7's read names the switch; then prove it: install the PREVIOUS upstream release on the bench, seed, move to the pin (upgrade-test.py --move), read back through the login |
wger ran no migration without DJANGO_PERFORM_MIGRATIONS; the update said done, the login answered 500 (R-738). A new app at its newest tag has no "next" update to try — the step INTO the pin is the test |
| 1.5 | 2026-10-01 | The app runs its production server, not a development server | process list in the running container (ps / /proc/*/cmdline) |
wger ran manage.py runserver (R-755) |
| 1.6 | 2026-10-01 | Every key and secret the app READS is set or generated — none left at an image default or empty | list the env names the app's settings read (grep its settings source for env reads of *KEY*, *SECRET*, *TOKEN*, *PEM*); each one set in the compose; then 3.9's logins on every route |
wger read JWT_PRIVATE_KEY, the template set none: the API login answered 500 on the right password while the web login worked (R-737); grafana falls back to admin on an empty field (R-708) |
| 1.7 | 2026-10-01 | Every start-time switch in the image's entrypoint is read and decided (migrations, production server, debug, start-time downloads, static files) | read the entrypoint inside the image (docker run --rm --entrypoint cat <image> <entrypoint>); list each if $VAR and the value the template sets |
one read of wger's entrypoint.sh shows DJANGO_PERFORM_MIGRATIONS AND WGER_USE_GUNICORN — R-738 and R-755 in one file (felhom.eu/documentation/audits/more-night-apps-2026-09-30/box/wger/entrypoint-read.txt) |
| 1.8 | 2026-10-01 | Debug / development mode is OFF on the public origin | 1.7's read + the running container's env; an error page through traefik shows no stack trace | adventurelog ran Django with DEBUG=True on the public origin (R-482) |
| 1.9 | 2026-10-01 | A secret whose loss destroys data or locks people out (it encrypts stored data, or signs 2FA secrets / long-lived tokens) carries data_key: true and a comment saying why it must never be regenerated; a key that only signs sessions does not |
what each generated secret is used for (the app's settings source) | a restore must RECOVER that key; regenerating it destroys data or locks out 2FA (felhom-app-catalog skill §4) |
2. Storage and backup
| id | since | Check | How | Why |
|---|---|---|---|---|
| 2.1 | 2026-10-01 | Every path the app writes is mounted — measured | gate volume-persistence (check-volume-persistence.py <app>, scratch host) |
papra backed up an empty folder (R-156) |
| 2.2 | 2026-10-01 | Where each path lives: named volume (NVMe) / ${HDD_PATH}/appdata / ${USERDATA_PATH} |
compose; REUSE.md §2 "Compose file skeleton" | the household can browse userdata, not appdata |
| 2.3 | 2026-10-01 | Backup class for every HDD path (backup: in .felhom.yml) and what the tier-1 unit holds |
.felhom.yml backup:; the app's backup page on 9202 |
DB rows point at files (07); the tier-1 unit holds NO drive-side data (R-537, R-538) |
| 2.4 | 2026-10-01 | File owner / uid fits the box (PUID/PGID where the image wants them) | first start on 9202 | linuxserver images chown their tree |
| 2.5 | 2026-10-01 | A backup and a restore through the product, data read back | 9202: seed → backup → remove → restore → read back | the promise is the restore, not the backup |
| 2.6 | 2026-10-01 | "Remove with data" and "remove, keep data" both do what they say | 9202, then list what is left on the drive | "remove with data" was inert (R-442); refused with the folder present (R-756, cause not yet known) |
| 2.7 | 2026-10-01 | Off-site size for a typical household | measured size after the seed + one sentence | 09 decision 50 (the page names the largest apps) |
| 2.8 | 2026-10-01 | A file the household uploads opens again through the front door | 9202: upload through traefik, open it the way the page does | adventurelog's photos uploaded and rendered broken (R-483) |
3. Accounts and strangers
| id | since | Check | How | Why |
|---|---|---|---|---|
| 3.1 | 2026-10-01 | First-admin class (FIRST-ADMIN.md 1–6), measured, and the household can make its first account on a fresh install | fresh install on 9202, through traefik | 09 decision 45; wishlist could not be signed up to while the deploy said success (R-612) |
| 3.2 | 2026-10-01 | Known default login → after_install with a generated password (and a generated name, if the name is public and a lock targets it) |
9202: default fails, generated works, wrong fails | bookstack, claper (R-702), calibre-web (09 decisions 45, 61) |
| 3.3 | 2026-10-01 | Open first-run screen → setup_gate (+ a probe that flips, measured before and after) |
9202 as a stranger; gate probe-measured refuses a probe with no measurement above it |
33 apps gated today (09 decision 46); a probe that never flips blocks the household (R-715) |
| 3.4 | 2026-10-01 | Open sign-up after the setup → signup_block / after_setup; the block case-insensitive, the API sign-up blocked too |
9202 as a stranger, after the setup | R-711, R-512; 09 decisions 47–49 |
| 3.5 | 2026-10-01 | The install window: a stranger reaches nothing before the password is replaced | poll the default login once a second from the install press | R-741 (fixed box-wide in controller v0.284.x — a new after_install app still proves it) |
| 3.6 | 2026-10-01 | Lock-out: N wrong passwords by a stranger for the public name — who is locked (name / address / everyone), for how long | 9202 through traefik; then the household's right password, and a SECOND member's | mealie 24 h (R-747); behind the tunnel every visitor has ONE address, so a per-address lock locks everyone (R-753) — wger (R-752) |
| 3.7 | 2026-10-01 | The household can change its password and add family members; the page says how | 9202 | add_people copy |
| 3.8 | 2026-10-01 | Secrets pass to commands as arguments, never inside program code | review after_install |
claper pasted the password into Elixir code (R-713); security review 2026-09-29 |
| 3.9 | 2026-10-01 | Sign in the way each client does, through traefik over https: the browser form (with its https Origin and CSRF cookie) AND every API login route a phone/desktop app uses — right password works, wrong refused |
9202, curl with the headers a browser sends; the API route from 0.7 |
wger refused every browser sign-in behind traefik (CSRF, R-712); wger's API login 500 (R-737) |
| 3.10 | 2026-10-02 | The visitor's address: how the app reads it (from the RIGHT with a trusted-proxy list / a fixed count / the LEFTMOST X-Forwarded-For / not at all) and what it decides with it (log, lock, "local network" rights). A leftmost reader whose address decides anything carries the router reset <router>-xff; a right-walking reader with a setting gets 172.16.0.0/12 trusted |
read the app's client-address function at the pinned tag; then on 9202 a forged leftmost X-Forwarded-For through the SIMULATED tunnel (a container at 172.16.253.2, Part A's method) must not become the address the app logs or locks |
since controller v0.286.0 traefik keeps the tunnel's chain, whose leftmost entry a stranger writes (R-753); 19 catalog apps read the leftmost (felhom.eu/documentation/audits/visitors-2026-10-01/A/sweep/) |
4. Health
| id | since | Check | How | Why |
|---|---|---|---|---|
| 4.1 | 2026-10-01 | Compose healthcheck of the family the IMAGE has (inspected, one tool per run), dialling 127.0.0.1 not localhost |
the inspection loop in the felhom-app-catalog skill §2; REUSE.md §2 (no gate checks localhost; 0 templates use it today) |
rallly's guessed wget (ENOENT); vaultwarden's IPv6 trap |
| 4.2 | 2026-10-01 | The controller probe dials what the compose healthcheck dials; a real health path where one exists | gate probe-matches-compose |
a wrong probe stops a working app after an update (R-618) |
| 4.3 | 2026-10-01 | Healthy within start_period on a cold first start (incl. one-time imports), with no restarts |
9202, timed, RestartCount read |
glance crash-looped on every fresh install (R-473); immich's first start restarted 12× (R-676) — 09 decision 28 stops ≥ 6 in 10 min |
| 4.4 | 2026-10-01 | Negative control: a broken app reads unhealthy | stop the DB (or break the app's data dir), read the status | a probe that is always green proves nothing; uptime-kuma parked on its wizard read healthy (R-613) |
| 4.5 | 2026-10-01 | The probe-named container is the stack name; sidecars <app>-db … |
compose (REUSE.md §2 "Probe-container naming") | findProbeContainer falls back to the DB; paperless's probe never ran (R-630) |
5. Resources
| id | since | Check | How | Why |
|---|---|---|---|---|
| 5.1 | 2026-10-01 | First start from birth, swap OFF: peak anon, oom_kill = 0 |
bench, the container's own cgroup sampled every 2 s from creation (memory.stat anon, memory.events oom_kill — never Docker's OOMKilled, R-528) |
immich's DB killed at 512 MiB, hidden by swap on 9202 (R-732, R-733); calcom could not start at its limit (R-703) |
| 5.2 | 2026-10-01 | 10-minute soak under the household's heaviest ordinary act: peak anon < 80 % of the limit, 0 kills, 0 restarts |
harness memory watch (upgrade-test.py) + one burst of that act |
romm OOM-looped for six hours after an update called success (R-635, 09 decision 22); paperless lost 11 of 20 uploads at once (R-514) |
| 5.3 | 2026-10-01 | mem_limit in .felhom.yml = the sum of the compose limits, and the header comment says the same |
arithmetic by hand — no gate checks it; 8 templates differ today (R-758) | immich's mem_limit was 128 MB under its sum, its header named a 256M database running at 512M (fixed 56c4888) |
| 5.4 | 2026-10-01 | Node/Java apps: does the heap size itself from the limit? | two watches at two limits | R-693 (docmost) |
| 5.5 | 2026-10-01 | Pi-compatible (yes/no) and the disk the images pull | registry (image size) | pi_compatible is a card field; a box's Docker disk refuses installs when full (R-736) |
6. Updates
| id | since | Check | How | Why |
|---|---|---|---|---|
| 6.1 | 2026-10-01 | An upgrade fixture: seed + read-back through the app's own front door, negative control — or the reason none can exist | upgrade_fixtures*.py |
the box's health check sees only the front page; only the read-back saw wger broken (R-738); three apps cannot be seeded headless (R-624) |
| 6.2 | 2026-10-01 | A first ladder step proven on bench + 9202 (--write-ladder), or "manual only" with the reason |
harness; gates test-record + test-record-move check the entry once written |
38 of 53 apps carry a ladder today; zipline needed two steps where one failed (R-742) |
| 6.3 | 2026-10-01 | The undo works on a real failure (one forced-fail case) | 9202, the drill catalog's image store (09 §6.5) |
zipline's real failed jump, undone in 20 s (R-742) |
| 6.4 | 2026-10-01 | files_may_change understood (which files change at start) |
bench (files_changed_detail) |
immich's six marker files (R-734) |
| 6.5 | 2026-10-01 | Tag shape is stable upstream (no v dropped, no flavour prefix) and whether the publisher re-pushes tags |
tag list over a year; linuxserver rebuilds weekly | gramps-web, jellyfin, kimai (R-731); same-tag re-pushes (R-743, 09 decisions 52/55) |
7. Mail
| id | since | Check | How | Why |
|---|---|---|---|---|
| 7.1 | 2026-10-01 | Sends mail? → smtp_mapping + ${VAR:-} compose lines; a fresh install with mail OFF boots |
9202 | vaultwarden's empty-vars trap (REUSE.md §2 "App-email") |
8. Household-facing text and listing
| id | since | Check | How | Why |
|---|---|---|---|---|
| 8.1 | 2026-10-01 | Hungarian + English, informal „te", no „kérjük"; parity green; freeze updated | gate copy-i18n (check-copy-i18n.py --capture-freeze after a copy change) |
operator rule; R-560 |
| 8.2 | 2026-10-01 | app_info: tagline, use_cases, first_steps, default_creds (if any), add_people — and each one TRUE on 9202 |
read on 9202's app page and follow the first steps | paperless's page named a login that did not exist (R-515); first steps named a literal wiki.DOMAIN (R-498) |
| 8.3 | 2026-10-01 | Logo + screenshots on felhom.eu by slug |
https://felhom.eu/assets/<slug>-logo.svg (or .png, the controller's fallback) and <slug>-screenshot-<n>.webp answer 200 — NOT -logo.webp (the canonical template's comment said so until 2026-10-05, R-761) |
the card is blank otherwise |
| 8.4 | 2026-10-01 | README tables, FIRST-ADMIN row, category, catalog_since |
review | REUSE.md §5 |
| 8.5 | 2026-10-01 | The website's app count still holds | `ls templates | wc -lagainstfelhom.eu/website` claims; record a drift |
9. Sign-off
| id | since | Check | How | Why |
|---|---|---|---|---|
| 9.1 | 2026-10-01 | python3 scripts/catalog_gates.py <app> — all green (exit 0) |
bench / scratch host (it runs the runtime gate) | the one entry point (R-161) |
| 9.2 | 2026-10-01 | A fresh install on 9202 from the drill catalog, as a household and as a stranger, start to finish | 9202 | the walk finds what single checks miss (R-482..R-488 in one evening) |
| 9.3 | 2026-10-01 | Every item above done or n/a-with-reason; every finding not fixed is a register row | the record | prose is not a record |
| 9.4 | 2026-10-01 | Published to the live catalog in ONE commit with its record | gate onboarding |
a template and its proof travel together |
Row count
| group | rows |
|---|---|
| 0 Fit | 9 |
| 1 Images, start command, database | 9 |
| 2 Storage and backup | 8 |
| 3 Accounts and strangers | 10 |
| 4 Health | 5 |
| 5 Resources | 5 |
| 6 Updates | 5 |
| 7 Mail | 1 |
| 8 Text and listing | 5 |
| 9 Sign-off | 4 |
| total | 61 |