Files
app-catalog-felhom.eu/NEW-APP-CHECKLIST.md
T
2026-10-05 16:55:06 +02:00

160 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` (in `catalog_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/…` or `felhom.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 -l` against `felhom.eu/website` claims; record a drift | felhom-app-catalog skill §6 |
## 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** |