That step still pinned immich-postgres at 512M. It re-runs the geodata import (v3.0.3 ships geodata
dated 2026-07-13, v3.2.2 2026-08-30 — read inside both images), the load R-732 measured at ~575 MB.
Re-proof on bench 9401, swap 0, on the step's OWN definition with only the limit changed (768M;
mem_limit 4096M -> 4480M): `Starting geodata import` ... `Geodata import completed` in 17 s, the
database's kill counter 0 from its birth, the album read back, 10-min watch 0 kills (anon peak 29.6 %).
Written by upgrade-test.py --restep; the ladder entry is untouched. No box reporting to the hub runs
immich (hub /apps, same day).
Evidence: felhom.eu/documentation/audits/more-night-apps-2026-09-30/D/
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The ONLY image move in this commit; the limit rides it (09 decision 39's precedent), because the step
itself re-runs immich's geodata import (228 294 -> 228 571 places) — the load that killed the database.
Cause, measured on the bench (audits/immich-first-start-2026-09-30/A-cause.md): the first-start import
runs up to 9 concurrent 5000-row INSERTs; the database then needs ~400 MB anon + ~170 MB touched
shared_buffers (the image's own postgresql.conf fixes 512MB). At 512M with no swap: 61 kills; with 512 MB
swap: 0 (swapped ~70 MB) — why 9202 passed; shared_buffers 128MB alone: still killed; 1024M: 0; 768M: 0.
Proof at the new definition, fresh install from birth, no swap: bench x2 (anon 409/412 MB = 53 %, 0 kills,
import 8.1 s) and box 9202 (anon 368 MB = 48 %, 0 kills, swap.peak 0).
Step, written by upgrade-test.py --write-ladder: bench (harness v4) proven, 10-min watch 0 kills 0
restarts, anon peak 51.1 %; box 9202 through the guarded Update: done 58.5 s, album read back, the running
database limit 805306368 after. The step carries `files_may_change` (the harness saw only immich's six
13-byte `.immich` folder markers rewritten at start) — the night leg takes it only with a whole copy.
Per-box cost: +256 MB on immich-postgres; `mem_limit` 4096M -> 4480M (the old figure was already 128 MB
under the sum of the four limits). Header comment corrected (it said postgres 256M).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22's move took immich-server to v3.2.2 and left machine-learning at
v3.0.3; this step aligns them. Bench: the admin + an album (its own API)
read back; memory watch 12 053 requests all 200: server 51.4 %, ML 10.2 %,
postgres 35.1 % own memory (its cgroup peak 100 % is file cache, decision
22); 0 kills; abort starts-and-serves. Box (9202): done, read back, R-626
clean. 09 decision 21.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
update_ladder: in .felhom.yml, one JSON entry per line (spiked live on
controller v0.266.0 and v0.267.0 first). Two gates: check-test-record.py
(static, CI too) and check-test-record-move.py (history + registry for
moved refs only). 16 decoys, 3 red-proofs. The ONLY writer is
upgrade-test.py --write-ladder (bench AND box proven, digests resolved).
Harness v3: box fixtures on the bench, files_may_change.
Backfill: the 21 moves of 2026-09-22, 21 proven from their records.
No image: line moved.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
No image: line moved.
paperless-ngx has no container named after its stack, so its probe had NEVER run on any box.
immich has four immich-* containers and no exact match, so the old first-prefix rule picked
whichever came first - possibly the database.
The gate now resolves the target by the same four rules as findProbeContainerMeta: exact name,
explicit container, a UNIQUE prefix, else refuse - and refusing is right, because verifying waits
on this probe and a successful update of such an app gets stopped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Walked through the product's own guarded Update on scratch guest 9202 during the
twenty-eight drill, seeded and read back through the app's own front door.
Evidence: felhom.eu/documentation/audits/the-28-2026-09-22/apps/immich/verdict.json
catalog_since -> 2026-09-22 (R-452).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
actualbudget, claper, docmost, emby, gitea, immich, kimai, komga, onlyoffice,
opengist, plant-it, rallly, recipe-importer, seerr, vaultwarden, zipline — 306
strings. EN_MISSING_CEILING 307 -> 1.
1 031 of 1 032 strings now carry an English twin. The one that does not is papra's
AUTH_SECRET description, a Hungarian defect (R-593) left to fall back rather than
translated wrongly.
The gate convicted two of my own sentences and was half right: vaultwarden's invite
step and sign-up setting ended "can open an account", and the retrieval-promise
pattern reads "can ... open" as the claim that sealed backups can be opened. Opening
an ACCOUNT is not that claim, so the conviction was a false positive — but the
wording was also the weaker wording, so both now read "can sign up". The gate has no
way to REGISTER a legitimate occurrence, which the shared vocabulary's own design
calls for; filed as R-594.
No Hungarian byte moved in any of the three batches; the freeze gate proves it on all
53 apps on every push.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Backfilled from this repo's own git history (newest commit whose image: set differs
from its parent's), excluding the two bentopdf SPIKE commits by hash — they moved a
pin and reverted it in the same hour and are a measurement, not a release.
Four anchors confirmed (nextcloud 5e2c1ae, grafana b789acc, calcom 147cee7,
vikunja 3fa63cd, all 2026-07-18); six apps re-checked against git log by hand.
Consumed by felhom-controller v0.233.0 to render 'Frissitesi elerheto - N napja'.
No version number is ever shown to the customer, so there is no version: key.
Rule added to CLAUDE.md; the missing drift gate is filed as a register row (the
gates runner fetches at --depth 1 and has no parent to diff an image: line against).
MAJOR: immich v3.0.0 drops pgvecto.rs support and requires VectorChord. Our pin
was already VectorChord, so fresh deploys are unaffected; an in-place update from
a pgvecto.rs-era install would need the upstream migration first.
Postgres sidecar moved to the vectorchord/pgvectors extension versions immich
v3.0.3 ships in its own compose (0.4.3 / 0.2.0), keeping our PG major (16)
rather than upstream's 14 to avoid a needless major change.
NOTE (recorded, not fixed): upstream v3 migrated redis -> valkey:9. Kept
redis:7-alpine here; the swap is a structural change, not a pin bump.
Campaign 7 catalog sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
Adds the referential-coupling backup: block to every app that binds
${HDD_PATH}/${USERDATA_PATH} (immich, paperless-ngx, nextcloud, calibre-web,
audiobookshelf, komga, navidrome, radarr, sonarr, emby, jellyfin, plex, romm).
Each lists its userdata:/hdd: binds with class mandatory|optional|excluded
(operator-ruled + spike SQ2).
Requires controller v0.132.0 (deployed) which parses+validates these blocks.
INERT — no backup tier changes behavior yet (Task 3/4 consume it). All 13
verified against the shipped parser: parse-clean, every bind resolves explicit.
audiobookshelf media/audiobooks = optional (PENDING Viktor veto to excluded).
Ref: felhom.eu/documentation/audits/SPIKE-backup-classification-2026-07-14.md
BusyBox wget (+ node/python/curl one-shots, incl mealie's socket tuple) resolve
localhost -> IPv6 ::1 with no cross-family fallback; an IPv4-only-binding app
reads docker-unhealthy while serving (vaultwarden, re-run 2026-07-06). Escalates
that instance to the class. Scoped strictly to healthcheck test: lines
(diff-reviewed: no env/config/label changed; .felhom.yml already clean). New
REUSE.md convention row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Intermediary-mount re-architecture: drives are visible in-guest at the stable
/mnt/felhom-drives/<name>. Composes already use ${HDD_PATH}/${USERDATA_PATH}
(injected + repointed by controller v0.67.0); this updates the UI placeholders,
templates.json defaults, and doc/script examples to the new convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Model A binds the guest mount /mnt/<drive> directly onto the host's
<drive>/felhom-data namespace, so the guest mount already IS felhom-data.
The templates' ${HDD_PATH}/felhom-data/appdata/<app> therefore double-nested
to <drive>/felhom-data/felhom-data/appdata/<app> on disk, diverging from the
provenance-aware backup helpers (NamespaceRoot(drive,true) -> single-nested).
Change all four HDD app templates (romm, nextcloud, immich, paperless-ngx)
to ${HDD_PATH}/appdata/<app>, matching AppDataDir(NamespaceRoot(HDD_PATH,true)).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each app template now declares a healthcheck: section in .felhom.yml
with appropriate probe type (http, api, or tcp) and endpoint based on
the app's known health endpoints. The controller uses these to verify
services are actually responding, not just that containers are running.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Compose templates were mounting app data at ${HDD_PATH}/appdata/ instead
of ${HDD_PATH}/felhom-data/appdata/ as designed in the v0.26.0+ path
structure. Affects: nextcloud, immich, paperless-ngx, romm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All 51 docker-compose.yml: replaced hardcoded subdomain.${DOMAIN}
with ${SUBDOMAIN}.${DOMAIN} in Traefik labels, app env vars, and
comments.
All 51 .felhom.yml: added SUBDOMAIN deploy field (type: subdomain)
with default matching existing subdomain metadata value.
Works with felhom-controller v0.27.0 which validates and stores the
user-chosen subdomain in app.yaml. Existing deployed apps get
SUBDOMAIN auto-injected via InjectMissingFields() on next sync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Part of v0.14.0 storage architecture overhaul — standardize
app data paths under appdata/ instead of storage/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>