- Twelve steps published on both venues (catalog): first ladders for calibre-web, gitea, wger, crafty-controller, uptime-kuma, zipline (two steps); within-major emby, ghost, home-assistant, outline, rallly. immich's step v3.0.3 -> v3.2.2 re-proven at 768M (Part D). - Part C: the night leg skips a digest-only change AND the catalog never records a same-tag re-test, so a same-name upstream fix reaches no box. Row R-740; the decision in STATUS; `09` decision 30 carries a dated note (the decision itself unchanged). - Rows: 369 -> 377. Opened R-735..R-742; closed R-735, R-738, R-742; narrowed R-462, R-624, R-446, R-440, R-734, R-732. The currency audit gains §1b (and corrects its 31+1 to 30+2). Evidence: documentation/audits/more-night-apps-2026-09-30/. Report: REPORT-more-night-apps-2026-09-30.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
32 KiB
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
- 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 islifecycle: abandoned. - recipe-importer: Felhom's own image (
gitea.dooplex.hu), not upstream. Not compared. - Upload dates: not measured for any image. ghcr's anonymous API has no timestamp. Not needed for this question, so not attempted.
- 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) andgitea/gitea:28.0.0(pushed to Docker Hub 2026-09-30 00:15 UTC, the same day; a version renumbering, not checked). - 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 thev. It is at26.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 at12.1(12.0 had rc1..rc7). - kimai
apache-2.57.0: upstream stopped theapache-tags. Plain2.67.0exists. 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 (adependabot-…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.
- gramps-web
- 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). - Majors for 0.x and calendar versions. "Major" is the first number of the tag, as written. For
v0.xapps 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. - 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 thatfromcannot. - 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 thatfrom, so L433–435 skips it. - The 11 backfilled head entries are harness v1 (no memory watch). The box does not read
harness_version, onlyverdict, 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
- 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. - bookstack (T1, fixture exists).
26.05.5 → 26.09.1. The fixture proves the database half only (R-460). - 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. - calibre-web (T1, fixture needed).
v4.0.6 → v4.0.8, household books markedmandatory, and no ladder at all. The first new fixture to write. - 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. - 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.
- 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 | installation.create); moved to PG 18 the same day |
| 2 | rallly | T1: DB engine: rallly-postgres | 0 | 1 | 1 | |
| 3 | zipline | T1: DB engine: zipline-postgres | 0 | 1 | 1 | 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.