# Catalog currency — measured 2026-09-30 Catalog `app-catalog-felhom.eu` main = `64461979250b` (clean tree). Controller source read at `felhom-controller` main = `d48da6c`. Read-only. Measured from DooPlex against the public registries with anonymous tokens. No box was touched. Nothing was pulled. Nothing was committed. Method: the 2026-09-21 method (`UPDATE-ARC-STATE-2026-09-21.md` §3, `update-arc-2026-09-21/00-drift.py`), adapted in `catalog-currency-2026-09-30/00-currency.py`. What changed is written at the top of that script. ## 1. Headline numbers **53 apps · 79 `image:` lines · 67 distinct pins.** 53 lines are exact releases, 24 are floating lines (12 distinct), 1 is Felhom's own image, 1 could not be measured (plant-it: the repository is gone). | question | apps | |---|---| | **behind upstream inside the same major** (any image) | **25 of 53** | | **behind upstream across a major** (any image) | **19 of 53** | | behind in either way | 37 of 53 | | — inside the major, exact pins only | 24 | | — across a major, exact pins only | 8 (gitea, gokapi, gramps-web, homepage, jellyfin, nextcloud, paperless-ngx, sparkyfitness) | | — across a major, only because a floating engine line has a newer major | 14 (postgres → 18, redis → 8, mariadb → 13.0, postgis → 18, claper 2.5 → 3.0) | **Can update itself at night today:** | number | apps | what it counts | |---|---|---| | **(a)** a box at the pin just before the catalog's head gets a step tonight | **28**, plus **1** conditional (nextcloud) | the controller's own rule, catalog side (§4) | | **(b)** the catalog's ladder has ≥1 `proven` entry | **29** | a looser count | | never, today | **24** | no `update_ladder:` at all → skip reason `no_test_record` | (a) and (b) differ by one app only. Every entry in all 29 ladders is `proven`, and no entry has `needs_person`. The one difference is nextcloud: its newest step has `files_may_change`, so the leg takes it only when a fresh whole copy of the app is on the box. **What (a) does NOT say.** It says the catalog offers a step. It does not say how many boxes are at a pin that matches. Nobody measured the fleet's pins today. A box installed from today's catalog is at the head and has nothing to do. And a night step only goes to the catalog's head, which is itself behind upstream for 37 apps. ### 1a. After the same day's moves (added by the session, 2026-09-30 afternoon) The tables below are the MORNING measurement (catalog `6446197`). The session then moved five apps on both venues: rallly, outline and sparkyfitness to PostgreSQL 18 (their first ladder steps), bookstack 26.05.5 → 26.09.1 and kimai apache-2.57.0 → 2.67.0 (+ the Part F apps named in `pg-last-six-2026-09-30/F/`). Re-running `04-analyse.py` on the afternoon catalog with the same registry data: **night (a) 31 + nextcloud conditional; (b) 32 carry a proven step; 21 have no ladder.** The "behind upstream" counts are not re-measured; bookstack and kimai are now at their newest release inside the major. ### 1b. After the evening's moves (added 2026-09-30 evening, `more-night-apps-2026-09-30/`) **Correction first:** the numbers above were counted before the immich step of the late afternoon; that step carries `files_may_change` (R-734), so at `4c552cf` — the evening session's start — `04-analyse.py` reads **night (a) 30 + 2 conditional (nextcloud, immich); (b) 32**, not 31 + 1. The evening published first ladders for calibre-web, gitea, wger, crafty-controller, uptime-kuma and zipline, and within-major steps for emby, ghost, home-assistant, outline, rallly (and zipline's second). Re-run of `04-analyse.py` on the evening catalog, same registry data (`more-night-apps-2026-09-30/count/`): **night (a) 35 + 3 conditional (nextcloud, immich, calibre-web); (b) 38 carry a proven step; 15 have no ladder.** The "behind upstream" counts are not re-measured; each of the eleven moved apps is now at the newest release inside its major (re-checked the same evening, `more-night-apps-2026-09-30/00-upstream-recheck.txt`). ## 2. What could not be measured — stated, not guessed 1. **plant-it** (`msdeluise/plant-it:0.10.0`): Docker Hub gives 401 on the tag list, and its catalog API gives **404** for the repository. Same finding as 2026-09-21. The app is `lifecycle: abandoned`. 2. **recipe-importer**: Felhom's own image (`gitea.dooplex.hu`), not upstream. Not compared. 3. **Upload dates**: not measured for any image. ghcr's anonymous API has no timestamp. Not needed for this question, so not attempted. 4. **Whether the newest tag is a GA release.** The filter is by tag name only (rc, beta, alpha, dev, nightly, …). Two results need a person to check: `mariadb:13.0` (a two-part line tag exists; GA not checked) and `gitea/gitea:28.0.0` (pushed to Docker Hub 2026-09-30 00:15 UTC, the same day; a version renumbering, not checked). 5. **Tag shape switches.** The rule compares only tags of the same shape. A control pass (`09-shape-switch-check.txt`) looked for higher versions under ANY shape for every pin called "up to date". It found three real switches, applied by hand with their tag lists (`10-shape-switch-detail.txt`): - **gramps-web** `v25.6.0`: upstream dropped the `v`. It is at `26.9.1`. Calendar versions, so 25→26 is counted as across a major. - **jellyfin** `10.11.11`: upstream 12 uses two-part tags. It is at `12.1` (12.0 had rc1..rc7). - **kimai** `apache-2.57.0`: upstream stopped the `apache-` tags. Plain `2.67.0` exists. Whether it is the same Apache variant is NOT checked. Two more hits were noise: sonarr (`5.14-2.0.0…-ls5`, a develop-style tag) and tandoor (a `dependabot-…` branch tag). The control ran only on pins called up to date, and not on floating lines. A switch hiding behind a pin already called "behind" would change only its "newest" value. 6. **Floating pins — did the tag move?** 18 of 24 floating lines resolve to **exactly the digest the ladder recorded when the step was tested**. None has moved since its test. 6 cannot be compared, because the app has no ladder: outline (postgres 16, redis 7), rallly (postgres 16), sparkyfitness (postgres 15), zipline (postgres 16), wger (`2.6`). 7. **Majors for 0.x and calendar versions.** "Major" is the first number of the tag, as written. For `v0.x` apps a minor step can be breaking. For calendar versions (bookstack 26.05, home-assistant 2026.9, actualbudget 26.9, papra 26.6) the first number is a year. This table does not interpret it. 8. **ghcr rate limit.** immich-server and immich-machine-learning got HTTP 429 on the first run. They were measured on a slower retry (`02-retry-429-run.txt`). Nothing is left at 429. ## 3. Per image "Floating (line)" = the tag names a version LINE, not a release: fewer than three numbers in its first version run (`12.3`, `16-alpine`, `16-3.5-alpine`, immich's `16-vectorchord…`). For a floating line, "newest same major" is the newest line of the same shape in that major. "Newest at all" is the newest line of that shape. | app | service | pinned | newest same major | newest at all | behind in major | behind across major | note | |---|---|---|---|---|---|---|---| | actualbudget | actualbudget | `26.9.0` | `26.9.0` | `26.9.0` | no | no | | | adventurelog | adventurelog | `v0.13.0` | `v0.13.0` | `v0.13.0` | no | no | | | adventurelog | adventurelog-postgres | `16-3.5-alpine` | `16-3.5-alpine` | `18-3.6-alpine` | no | yes | floating (line); digest = ladder's tested digest | | adventurelog | adventurelog-frontend | `v0.13.0` | `v0.13.0` | `v0.13.0` | no | no | | | audiobookshelf | audiobookshelf | `2.36.1` | `2.37.1` | `2.37.1` | yes | no | | | bentopdf | bentopdf | `v2.8.6` | `v2.8.8` | `v2.8.8` | yes | no | | | bookstack | bookstack | `26.05.5` | `26.09.1` | `26.09.1` | yes | no | | | bookstack | bookstack-db | `12.3` | `12.3` | `13.0` | no | yes | floating (line); digest = ladder's tested digest | | calcom | calcom | `v6.2.0` | `v6.2.0` | `v6.2.0` | no | no | | | calcom | calcom-postgres | `18-alpine` | `18-alpine` | `18-alpine` | no | no | floating (line); digest = ladder's tested digest | | calibre-web | calibre-web | `v4.0.6` | `v4.0.8` | `v4.0.8` | yes | no | | | claper | claper | `2.5` | `2.5` | `3.0` | no | yes | floating (line); digest = ladder's tested digest | | claper | claper-postgres | `17-alpine` | `17-alpine` | `18-alpine` | no | yes | floating (line); digest = ladder's tested digest | | code-server | code-server | `4.129.0` | `4.139.1` | `4.139.1` | yes | no | | | crafty-controller | crafty-controller | `4.10.7` | `4.11.0` | `4.11.0` | yes | no | | | docmost | docmost | `0.96.0` | `0.96.0` | `0.96.0` | no | no | | | docmost | docmost-postgres | `18-alpine` | `18-alpine` | `18-alpine` | no | no | floating (line); digest = ladder's tested digest | | docmost | docmost-redis | `7-alpine` | `7-alpine` | `8-alpine` | no | yes | floating (line); digest = ladder's tested digest | | emby | emby | `4.11.0.3` | `4.11.0.4` | `4.11.0.4` | yes | no | | | ghost | ghost | `6.65.0-alpine` | `6.67.0-alpine` | `6.67.0-alpine` | yes | no | | | gitea | gitea | `1.27.0` | `1.27.3` | `28.0.0` | yes | yes | | | glance | glance | `v0.8.5` | `v0.8.6` | `v0.8.6` | yes | no | | | gokapi | gokapi | `v1.9.6` | `v1.9.6` | `v2.2.4` | no | yes | | | grafana | grafana | `13.2.2` | `13.2.3` | `13.2.3` | yes | no | | | gramps-web | gramps-web | `v25.6.0` | `v25.6.0` | `26.9.1` | no | yes | SHAPE SWITCH: upstream dropped the `v` prefix in 2026 (26.x.y); calendar versions, so the 25→26 step is a new year, counted as across | | home-assistant | home-assistant | `2026.9.3` | `2026.9.4` | `2026.9.4` | yes | no | | | homebox | homebox | `0.26.2` | `0.26.2` | `0.26.2` | no | no | | | homepage | homepage | `v1.13.2` | `v1.13.2` | `v2.4.0` | no | yes | | | immich | immich-server | `v3.2.2` | `v3.2.4` | `v3.2.4` | yes | no | measured on retry after ghcr 429 | | immich | immich-machine-learning | `v3.2.2` | `v3.2.4` | `v3.2.4` | yes | no | measured on retry after ghcr 429 | | immich | immich-postgres | `16-vectorchord0.4.3-pgvectors0.2.0` | `16-vectorchord0.4.3-pgvectors0.3.0` | `17-vectorchord0.4.3-pgvectors0.3.0` | yes | yes | floating (line); digest = ladder's tested digest | | immich | immich-redis | `7-alpine` | `7-alpine` | `8-alpine` | no | yes | floating (line); digest = ladder's tested digest | | jellyfin | jellyfin | `10.11.11` | `10.11.11` | `12.1` | no | yes | SHAPE SWITCH: upstream 12.x publishes two-part tags (`12.1`); 12.0 went through rc1..rc7 | | kimai | kimai | `apache-2.57.0` | `2.67.0` | `2.67.0` | yes | no | SHAPE SWITCH: upstream stopped publishing `apache-` tags after 2.57.0; plain `2.67.0` exists (whether it is the same apache variant is NOT checked) | | kimai | kimai-db | `11.8` | `11.8` | `13.0` | no | yes | floating (line); digest = ladder's tested digest | | komga | komga | `1.27.1` | `1.28.0` | `1.28.0` | yes | no | | | mealie | mealie | `v3.28.0` | `v3.28.0` | `v3.28.0` | no | no | | | n8n | n8n | `2.41.2` | `2.42.1` | `2.42.1` | yes | no | | | navidrome | navidrome | `0.64.1` | `0.64.2` | `0.64.2` | yes | no | | | nextcloud | nextcloud | `34.0.4-apache` | `34.0.4-apache` | `35.0.1-apache` | no | yes | | | nextcloud | nextcloud-db | `12.3` | `12.3` | `13.0` | no | yes | floating (line); digest = ladder's tested digest | | nextcloud | nextcloud-redis | `7-alpine` | `7-alpine` | `8-alpine` | no | yes | floating (line); digest = ladder's tested digest | | onlyoffice | onlyoffice | `9.4.0` | `9.4.0` | `9.4.0` | no | no | | | opengist | opengist | `1.15` | `1.15` | `1.15` | no | no | floating (line); digest = ladder's tested digest | | outline | outline | `1.9.1` | `1.10.1` | `1.10.1` | yes | no | | | outline | outline-postgres | `16-alpine` | `16-alpine` | `18-alpine` | no | yes | floating (line); no ladder digest to compare | | outline | outline-redis | `7-alpine` | `7-alpine` | `8-alpine` | no | yes | floating (line); no ladder digest to compare | | paperless-ngx | paperless-webserver | `2.20.15` | `2.20.15` | `3.2.1` | no | yes | | | paperless-ngx | paperless-postgres | `18-alpine` | `18-alpine` | `18-alpine` | no | no | floating (line); digest = ladder's tested digest | | paperless-ngx | paperless-redis | `7-alpine` | `7-alpine` | `8-alpine` | no | yes | floating (line); digest = ladder's tested digest | | papra | papra | `26.6.2-rootless` | `26.6.2-rootless` | `26.6.2-rootless` | no | no | | | plant-it | plant-it | `0.10.0` | ? | ? | ? | ? | NOT MEASURED: HTTPError: 401 Client Error: Unauthorized for url: https://r | | plex | plex | `1.41.4.9463-630c9f557` | `1.43.4.10903-e5521bd8c` | `1.43.4.10903-e5521bd8c` | yes | no | | | privatebin | privatebin | `2.0.6` | `2.0.6` | `2.0.6` | no | no | | | radarr | radarr | `6.4.4` | `6.4.4` | `6.4.4` | no | no | | | rallly | rallly | `4.11.1` | `4.15.3` | `4.15.3` | yes | no | | | rallly | rallly-postgres | `16-alpine` | `16-alpine` | `18-alpine` | no | yes | floating (line); no ladder digest to compare | | recipe-importer | recipe-importer | `v0.9.11` | — | — | n/a | n/a | Felhom's own image, not upstream | | romm | romm | `5.3.1` | `5.3.1` | `5.3.1` | no | no | | | romm | romm-db | `11.8` | `11.8` | `13.0` | no | yes | floating (line); digest = ladder's tested digest | | romm | romm-redis | `7-alpine` | `7-alpine` | `8-alpine` | no | yes | floating (line); digest = ladder's tested digest | | seerr | seerr | `2.7.3` | `2.7.3` | `2.7.3` | no | no | | | sonarr | sonarr | `4.0.20` | `4.0.20` | `4.0.20` | no | no | | | sparkyfitness | sparkyfitness-db | `15-alpine` | `15-alpine` | `18-alpine` | no | yes | floating (line); no ladder digest to compare | | sparkyfitness | sparkyfitness-server | `v0.17.3` | `v0.17.3` | `v1.7.3` | no | yes | | | sparkyfitness | sparkyfitness-frontend | `v0.17.3` | `v0.17.3` | `v1.7.3` | no | yes | | | tandoor | tandoor | `2.6.15` | `2.6.15` | `2.6.15` | no | no | | | tandoor | tandoor-postgres | `17-alpine` | `17-alpine` | `18-alpine` | no | yes | floating (line); digest = ladder's tested digest | | termix | termix | `2.8.0` | `2.8.0` | `2.8.0` | no | no | | | uptime-kuma | uptime-kuma | `2.4.0` | `2.5.5` | `2.5.5` | yes | no | | | vaultwarden | vaultwarden | `1.36.0-alpine` | `1.37.3-alpine` | `1.37.3-alpine` | yes | no | | | vikunja | vikunja | `2.6.0` | `2.6.0` | `2.6.0` | no | no | | | wanderer | wanderer | `v0.20.0` | `v0.21.0` | `v0.21.0` | yes | no | | | wanderer | wanderer-db | `v0.20.0` | `v0.21.0` | `v0.21.0` | yes | no | | | wanderer | wanderer-search | `v1.36.0` | `v1.54.2` | `v1.54.2` | yes | no | | | wger | wger | `2.6` | `2.7` | `2.7` | yes | no | floating (line); no ladder digest to compare | | wishlist | wishlist | `v0.67.1` | `v0.67.1` | `v0.67.1` | no | no | | | zipline | zipline | `4.6.1` | `4.8.0` | `4.8.0` | yes | no | | | zipline | zipline-postgres | `16-alpine` | `16-alpine` | `18-alpine` | no | yes | floating (line); no ladder digest to compare | ## 4. The night rule, from the code Source: `felhom-controller/controller/internal/stacks/unattended.go` at `d48da6c`, function `legCandidate` (L392–463). `RunUpdateLeg` (L221) → `runUpdateLeg` (L232–347) calls it for every deployed, non-protected app (L276–281) and presses `StartGuardedUpdate` only when it returns no reason (L311–321). The ladder reader is `ladder.go` (`LoadLadder` L83, `sameRefs` L118, `nextLadderStep` L152, which makes the same choice, L163). In order, an app is skipped when: | code line | skip | condition | |---|---|---| | L397–407 | `held` | a hold on the app | | L400 | `stopped_by_household` | the household stopped it | | L408–416 | current / `ahead` / `order_unknown` | `CatalogOrder` is not Behind | | L417–419 | `unpinned` | app.yaml has no `pinned_images` | | L422–424 | `no_test_record` | the template has no `update_ladder:` | | L426–432 | — | picks the NEWEST entry whose `from` equals the box's pin, every service, exactly | | L433–435 | `no_test_record` | no match, and the pin is the head's `to` (behind only on a digest no step records) | | L436–438 | `older_than_ladder` | no match at all: the box is older than the ladder | | L441 | `not_proven` | the entry's verdict is not `proven` | | L444 | `needs_person` | `marks.needs_person` is set and not blank | | L447 | `failed_before` | the same step was undone or held on this ladder (R-680) | | L450–458 | `files_may_change_no_whole_copy` | `marks.files_may_change` and no fresh whole copy on the box | After that, `StartGuardedUpdate` can still refuse for box reasons: `no_backup`, `engine_no_test`, `memory`, `disk`, `busy`… (`UpdatePreflight`, `update.go` L368 on; the checks at L431–470). **Definition used for (a).** For each app, take a box whose pin is the `from` of the ladder's newest entry (one step behind the head). Apply the catalog-side rows above (L422–458) to the entry the code would pick. Count "yes" when nothing skips it, and "conditional" when only `files_may_change` stands in the way. The box-side rows (hold, stop, failed_before, whole copy, refusals) are not in this count, because they depend on the box. Three things this rule means, read from the code: - **A box older than the ladder is never pressed** (L436–438). For 21 apps the ladder's first entry is a backfill of the 2026-09-21/22 move (`backfilled`). A box installed before that move matches it and can climb. A box older than that `from` cannot. - **A digest-only change is never pressed at night.** A box at the head tag with an older digest is "behind" by `digestBehind` (updateorder.go L95), but no entry has that `from`, so L433–435 skips it. - **The 11 backfilled head entries are harness v1** (no memory watch). The box does not read `harness_version`, only `verdict`, so it presses them like any other: actualbudget, audiobookshelf, bookstack, grafana, home-assistant, papra, privatebin, radarr, sonarr, termix, vikunja. ## 5. Per app — the ladder "Head = compose" is the gate's rule 3 (`check-test-record.py`, run today: 53 read, 29 with a ladder, 0 convicted, `03-check-test-record.txt`). "Box one step behind, tonight" is number (a). | app | entries | proven | of which backfilled | needs_person | files_may_change | engine conversion | head = compose | box one step behind, tonight | catalog_since | |---|---|---|---|---|---|---|---|---|---| | actualbudget | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | adventurelog | 1 | 1 | 0 | 0 | 0 | 0 | yes | yes | 2026-09-27 | | audiobookshelf | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | bentopdf | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-12 | | bookstack | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | calcom | 1 | 1 | 0 | 0 | 0 | 1 | yes | yes | 2026-09-28 | | calibre-web | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-12 | | claper | 1 | 1 | 0 | 0 | 0 | 1 | yes | yes | 2026-09-28 | | code-server | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | crafty-controller | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-06-26 | | docmost | 2 | 2 | 1 | 0 | 0 | 1 | yes | yes | 2026-09-25 | | emby | 2 | 2 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | ghost | 2 | 2 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | gitea | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | glance | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-09-21 | | gokapi | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | grafana | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | gramps-web | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | home-assistant | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | homebox | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-19 | | homepage | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | immich | 2 | 2 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | jellyfin | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | kimai | 1 | 1 | 0 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | komga | 1 | 1 | 0 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | mealie | 2 | 2 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-25 | | n8n | 3 | 3 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-25 | | navidrome | 2 | 2 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | nextcloud | 2 | 2 | 1 | 0 | 1 | 0 | yes | conditional: files_may_change (needs a fresh whole copy) | 2026-09-23 | | onlyoffice | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | opengist | 1 | 1 | 0 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | outline | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | paperless-ngx | 1 | 1 | 0 | 0 | 0 | 1 | yes | yes | 2026-09-27 | | papra | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | plant-it | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-21 | | plex | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-02-15 | | privatebin | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | radarr | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | rallly | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | recipe-importer | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-12 | | romm | 3 | 3 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | seerr | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | sonarr | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | sparkyfitness | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | tandoor | 2 | 2 | 1 | 0 | 0 | 1 | yes | yes | 2026-09-27 | | termix | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | uptime-kuma | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-09-21 | | vaultwarden | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | | vikunja | 1 | 1 | 1 | 0 | 0 | 0 | yes | yes | 2026-09-22 | | wanderer | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-21 | | wger | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-19 | | wishlist | 1 | 1 | 0 | 0 | 0 | 0 | yes | yes | 2026-09-23 | | zipline | 0 | 0 | 0 | 0 | 0 | 0 | — | no ladder: never (no_test_record) | 2026-07-18 | ## 6. Plan for R-462 — widen the upgrade harness **Order.** First: what a household loses if an update goes wrong. Then: how far behind. **T1** = a database engine service, or household files the backup marks `mandatory`. **T2** = household data in the app's own volume (SQLite or files). **T3** = little or no household data. **Cost, per app.** The only measured numbers are in R-462: a proven edge takes 6.4–305 s (median 72 s) plus the 600 s soak, a failing edge ~556 s, and a new fixture dominates everything else (the database legs were costed at 21–34 CC-hours for 15 services, `09` §6.4). So: **fixture exists** = one bench run plus one box run. **Fixture needed** = write a seed and a read-back through the app's own interface, with a negative control, first. **No route** = the harness can only answer `inconclusive`. ### 6.1 Do next — in this order 1. **immich** (T1, fixture exists). Server and ML `v3.2.2 → v3.2.4`, and a newer pgvectors line inside PostgreSQL 16. Cheapest high-value run: the fixture exists and the step is inside the major. 2. **bookstack** (T1, fixture exists). `26.05.5 → 26.09.1`. The fixture proves the database half only (R-460). 3. **kimai** (T1, fixture exists). `2.57.0 → 2.67.0`, but the tag shape changed (`apache-` is gone). A person must first confirm which plain tag is the Apache image. Then one run. 4. **calibre-web** (T1, **fixture needed**). `v4.0.6 → v4.0.8`, household books marked `mandatory`, and no ladder at all. The first new fixture to write. 5. **gitea** (T2, fixture exists but fails at the web installer, R-624). `1.27.0 → 1.27.3`. Cheap: the installer-form POST was never written. It unblocks gitea's first ladder entry. 6. **The T2 apps with a fixture and a step inside the major** — one run each: audiobookshelf, emby, ghost, grafana, home-assistant, komga, n8n, navidrome. This is the bulk and costs machine time only. 7. **T2 apps that need a new fixture and are behind inside the major:** wanderer (3 images, incl. meilisearch `v1.36 → v1.54`), uptime-kuma, crafty-controller, wger. ### 6.2 Majors — human work by the 2026-09-02 ruling, not harness bulk nextcloud 34 → 35, paperless-ngx 2 → 3, gokapi 1 → 2, homepage 1 → 2, sparkyfitness 0.17 → 1.7, gitea 1 → 28 (check first), jellyfin 10 → 12, gramps-web v25 → 26. Engine lines: redis 7 → 8 (6 apps), mariadb → 13.0 (bookstack, kimai, nextcloud, romm; GA not checked), PostgreSQL for the R-463 remainder (adventurelog/postgis, immich, outline, rallly, sparkyfitness on 15, zipline). ### 6.3 The honest ceiling — CORRECTED the same day The morning version of this section named 8 apps that can only ever be `inconclusive`. **Three of them were wrong**, measured on the bench the same afternoon: **outline** has a front-door first-run route (`POST /api/installation.create`, refused once a workspace exists), **rallly** signs up through its own API with the six-digit e-mail code read from its own table in place of a mailbox, and **zipline** 4's first-run route is `POST /api/setup` (the old fixture called two other paths). outline and rallly moved on both venues the same day; zipline's fixture now tries `/api/setup` first. **What remains in the class:** vaultwarden (closed sign-up by design, R-624) and code-server (a browser IDE); plex (a plex.tv claim token), homepage and onlyoffice (no household data) are no-route by nature. Plus plant-it (image gone) and recipe-importer (our own image; a fixture is needed). So the most the harness can prove is **47 of 53**. ### 6.4 The whole ranking | rank | app | data at risk | ladder entries | images behind in major | across major | fixture | |---|---|---|---|---|---|---| | 1 | outline | T1: DB engine: outline-postgres | 0 | 1 | 2 | ~~no route~~ **fixture written 2026-09-30** (`installation.create`); moved to PG 18 the same day | | 2 | rallly | T1: DB engine: rallly-postgres | 0 | 1 | 1 | ~~no route~~ **fixture written 2026-09-30** (sign-up + its own e-mail code); moved to PG 18 the same day | | 3 | zipline | T1: DB engine: zipline-postgres | 0 | 1 | 1 | ~~no route~~ **first-run route `POST /api/setup` measured 2026-09-30**; fixture fixed; PG stays 16 | | 4 | calibre-web | T1: household files (backup class mandatory) | 0 | 1 | 0 | fixture needed | | 5 | sparkyfitness | T1: DB engine: sparkyfitness-db | 0 | 0 | 3 | fixture needed | | 6 | immich | T1: DB engine: immich-postgres; household files (backup class mandatory) | 2 | 3 | 2 | fixture exists | | 7 | bookstack | T1: DB engine: bookstack-db | 1 | 1 | 1 | fixture exists | | 8 | kimai | T1: DB engine: kimai-db | 1 | 1 | 1 | fixture exists | | 9 | nextcloud | T1: DB engine: nextcloud-db; household files (backup class mandatory) | 2 | 0 | 3 | fixture exists | | 10 | claper | T1: DB engine: claper-postgres | 1 | 0 | 2 | fixture exists | | 11 | paperless-ngx | T1: DB engine: paperless-postgres; household files (backup class mandatory) | 1 | 0 | 2 | fixture exists | | 12 | romm | T1: DB engine: romm-db | 3 | 0 | 2 | fixture exists | | 13 | adventurelog | T1: DB engine: adventurelog-postgres | 1 | 0 | 1 | fixture exists | | 14 | docmost | T1: DB engine: docmost-postgres | 2 | 0 | 1 | fixture exists | | 15 | tandoor | T1: DB engine: tandoor-postgres | 2 | 0 | 1 | fixture exists | | 16 | calcom | T1: DB engine: calcom-postgres | 1 | 0 | 0 | fixture exists | | 17 | wanderer | T2: household data in the app's own volume (SQLite / files) | 0 | 3 | 0 | fixture needed | | 18 | gitea | T2: household data in the app's own volume (SQLite / files) | 0 | 1 | 1 | fixture exists, fails at the web installer (R-624, fixable) | | 19 | code-server | T2: household data in the app's own volume (SQLite / files) | 0 | 1 | 0 | no route: its front door is a browser IDE behind one password | | 20 | crafty-controller | T2: household data in the app's own volume (SQLite / files) | 0 | 1 | 0 | fixture needed | | 21 | plex | T2: household data in the app's own volume (SQLite / files) | 0 | 1 | 0 | no route: the first-run claim needs a token minted at plex.tv by a real Plex acc | | 22 | uptime-kuma | T2: household data in the app's own volume (SQLite / files) | 0 | 1 | 0 | fixture needed | | 23 | vaultwarden | T2: household data in the app's own volume (SQLite / files) | 0 | 1 | 0 | no route by design: closed sign-up (R-624) | | 24 | wger | T2: household data in the app's own volume (SQLite / files) | 0 | 1 | 0 | fixture needed | | 25 | gokapi | T2: household data in the app's own volume (SQLite / files) | 0 | 0 | 1 | fixture needed | | 26 | gramps-web | T2: household data in the app's own volume (SQLite / files) | 0 | 0 | 1 | fixture exists | | 27 | jellyfin | T2: household data in the app's own volume (SQLite / files) | 0 | 0 | 1 | fixture exists | | 28 | homebox | T2: household data in the app's own volume (SQLite / files) | 0 | 0 | 0 | fixture exists | | 29 | plant-it | T2: household data in the app's own volume (SQLite / files) | 0 | 0 | 0 | fixture needed | | 30 | recipe-importer | T2: household data in the app's own volume (SQLite / files) | 0 | 0 | 0 | fixture needed | | 31 | seerr | T2: household data in the app's own volume (SQLite / files) | 0 | 0 | 0 | fixture needed | | 32 | audiobookshelf | T2: household data in the app's own volume (SQLite / files) | 1 | 1 | 0 | fixture exists | | 33 | emby | T2: household data in the app's own volume (SQLite / files) | 2 | 1 | 0 | fixture exists | | 34 | ghost | T2: household data in the app's own volume (SQLite / files) | 2 | 1 | 0 | fixture exists | | 35 | grafana | T2: household data in the app's own volume (SQLite / files) | 1 | 1 | 0 | fixture exists | | 36 | home-assistant | T2: household data in the app's own volume (SQLite / files) | 1 | 1 | 0 | fixture exists | | 37 | komga | T2: household data in the app's own volume (SQLite / files) | 1 | 1 | 0 | fixture exists | | 38 | n8n | T2: household data in the app's own volume (SQLite / files) | 3 | 1 | 0 | fixture exists | | 39 | navidrome | T2: household data in the app's own volume (SQLite / files) | 2 | 1 | 0 | fixture exists | | 40 | actualbudget | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 41 | mealie | T2: household data in the app's own volume (SQLite / files) | 2 | 0 | 0 | fixture exists | | 42 | opengist | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 43 | papra | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 44 | privatebin | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 45 | radarr | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 46 | sonarr | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 47 | termix | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 48 | vikunja | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 49 | wishlist | T2: household data in the app's own volume (SQLite / files) | 1 | 0 | 0 | fixture exists | | 50 | bentopdf | T3: little or no household data (stateless or config only) | 0 | 1 | 0 | fixture needed | | 51 | glance | T3: little or no household data (stateless or config only) | 0 | 1 | 0 | fixture needed | | 52 | homepage | T3: little or no household data (stateless or config only) | 0 | 0 | 1 | no route: a dashboard rendered from config files in the template | | 53 | onlyoffice | T3: little or no household data (stateless or config only) | 0 | 0 | 0 | no route: a stateless document server: it holds no household data of its own, so | ## 7. Files In `catalog-currency-2026-09-30/`: | file | what | |---|---| | `00-currency.py` | the measurement (registries, read-only) | | `01-currency-raw.json`, `01-currency-run.txt` | its raw output | | `02-retry-429.py`, `02-retry-429-run.txt` | the slower retry for the two ghcr 429 rows | | `03-check-test-record.txt` | the catalog's own ladder gate, run today | | `04-analyse.py`, `04-analyse-run.txt` | tables and counts (offline; the night rule is in its header) | | `05-summary.json` | the counts and app lists | | `06-`, `07-`, `08-*.md` | the three tables above | | `09-shape-switch-check.txt`, `10-shape-switch-detail.txt` | the shape-switch control | **Findings that should become register rows** (this session did not edit the register — read-only brief): the three tag-shape switches (gramps-web, jellyfin, kimai), which the badge and any shape-based tool read as "up to date"; and the `gitea 28.0.0` / `mariadb 13.0` tags, which need a GA check before anyone plans on them.