Files
felhom.eu/documentation/audits/catalog-currency-2026-09-30.md
T
admin 42bf40bd2c
gates / gates (push) Successful in 29s
More apps update themselves at night (30+2 -> 35+3); the same-name security-fix gap measured (R-740, decision for the operator)
- 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
2026-09-30 20:27:28 +02:00

414 lines
32 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.
# 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.