Commit Graph

65 Commits

Author SHA1 Message Date
admin 405a127e7c probe target: explicit container for paperless-ngx and immich; ambiguity refused (R-630)
gates / gates (push) Successful in 1s
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
2026-09-22 20:28:40 +02:00
admin dcb6b1db68 CHANGELOG: the six proven moves (R-462)
gates / gates (push) Successful in 1s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 20:17:14 +02:00
admin f4eb94f195 romm: two web workers, not four - the actual cause (R-635)
gates / gates (push) Successful in 1s
WEB_SERVER_CONCURRENCY=2, limit left at 768M. No image: line moved.

768M was still a guess and it was wrong: kills slowed from ~12/min to ~7/min and stopped nothing.
Measured instead - each warm uvicorn worker holds ~216 MiB, so four plus the master reach ~882 MiB,
and the cgroup's memory.peak read exactly 768 MiB.

/init:143 runs --workers "${WEB_SERVER_CONCURRENCY:-4}". Four is a server default; this is one
household on one small box. Two measure ~450 MiB, fit 768M with headroom, and halve the CPU churn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 17:55:01 +02:00
admin 886956dcc7 romm: 768M, because 5.3.0 does not fit in 512M (R-635)
gates / gates (push) Successful in 1s
No image: line moved, so no catalog_since moved.

Measured on demo-hp after this morning's promotion: OOMKilled true, 4530 gunicorn worker SIGKILLs
in six hours, ~500% CPU in a permanent restart storm, host load 5.2 while otherwise idle. It ran
clean for two hours first, which is why the walk on the scratch guest did not catch it.

The update reported `done` and the app read `running` the whole time - nginx answers 200 while the
workers behind it die. Nothing alarmed; the operator heard the fans.

768M is a measured first step, not a final answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 17:46:24 +02:00
admin 1ad1f34b6e gates: probe-matches-compose runs in CI's no-PyYAML mode (R-618)
gates / gates (push) Successful in 2s
CI job 877 on 15d7c2b failed: the runner has no PyYAML and the gate answered INCONCLUSIVE, which
the runner rightly refuses to call a pass. A gate red on every push is bypassed within a week.

Falls back to a line reader and announces mode: DEGRADED. Over all 53 apps it returns exactly what
the full reader returns. Five more decoy cases with PyYAML shadowed out prove it still convicts the
three real faults; suite now 56 cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 11:02:53 +02:00
admin 15d7c2b7ed CHANGELOG: the fifteen proven catalog moves (R-462)
gates / gates (push) Failing after 2s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 11:00:34 +02:00
admin 6c690a1947 gates: refuse a health probe the app does not answer (R-618)
gates / gates (push) Failing after 2s
check-probe-matches-compose.py, a --fast gate so it bites in the hook and in CI.

The oracle was already in every template: the probed service's own compose healthcheck dials the
app on 127.0.0.1. The gate compares the .felhom.yml probe against it, statically.

Port mismatch REFUSES for every check type. Path mismatch REFUSES only where the probe can fail on
it (type api WITH expect) and WARNS otherwise, because probeHTTP calls any response healthy
otherwise - measured, not assumed. Six WARNs on the current catalog, each named in the CHANGELOG;
paperless-ngx is the loud one: no container matches the stack name, so no probe ever runs.

Four red-proofs and five decoys, suite now 51 cases. --root lets the suite judge its own clone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 10:51:34 +02:00
admin 793c4fba00 templates: three health probes dial where the app listens (R-618)
gates / gates (push) Successful in 1s
tandoor 8080->80, wger 80->8000, zipline /api/health->/api/healthcheck.

The probe dials <container-name>:<port><path> from inside the compose network, so the port is the
one the process LISTENS on. Each fix matches the port/path that the SAME service's own compose
healthcheck already dials on 127.0.0.1 — the oracle that was sitting in the file all along.

This is P1 and not cosmetic: the guarded update's `verifying` phase waits on this probe, so
`failAndHold` stopped a working app at the end of a SUCCESSFUL update. Measured on 9202 2026-09-21.

No image: line moved, so no catalog_since moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 10:50:01 +02:00
admin d4392e2a10 Vikunja fixture: the create verb is PUT, not POST (R-462)
gates / gates (push) Successful in 1s
Test code only. Vikunja creates a project with PUT /api/v1/projects; the fixture sent POST, which
answers 405 Method Not Allowed and reads like a broken app rather than a wrong verb. Corrected
box-side first, where the edge then walked clean (vikunja 2.3.0 -> 2.6.0, proven, 24.6 s); this is
the same correction in the ported copy.

Gates: catalog_gates.py --fast — all four OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 23:14:17 +02:00
admin 4463243f2e Upgrade harness: four fixtures and seven real upstream edges from the update night (R-462)
gates / gates (push) Successful in 1s
Test code only — no template changed and no image: line moved.

The update night walked real within-a-major upstream edges on scratch guest 9202 through the
product's own guarded Update, against a PRIVATE DRILL CATALOG; the live catalog was never
touched. This brings the expensive half of that work — the seed routes — back into the harness
so the same edges can be run here WITH their ABORT step, which the box deliberately does not
offer (09 6.1: whether the old image starts on migrated data is per-app and unpredictable).

- upgrade_fixtures.py: ActualBudget, Navidrome, AudiobookShelf, Vikunja. Each seeds through the
  app's OWN interface (R-156); each carries a negative control run on every verify(), so a
  readback that has broken into always succeeding fails instead of passing everything.
- upgrade-test.py: edges U1..U7, all real upstream moves existing 2026-09-21 that this catalog
  has NOT made, each holding its database engine constant.
- Limitations kept: Navidrome and AudiobookShelf seed the DATABASE half only, and say so.

OWED, stated so it is not mistaken for done: the U1..U7 harness RUNS, and with them the per-app
ABORT answers. The code is in; the runs are not.

Gates: catalog_gates.py --fast — image-pins, engine-major, catalog-since, copy-i18n all OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 21:00:50 +02:00
admin 5ff36d098c R-469: lift the MariaDB half of the engine-major rule; add R-450's own-edge clause
gates / gates (push) Successful in 1s
Slice 4 shipped 2026-09-13, so the rule's own expiry condition is met — for MariaDB.
PostgreSQL and MySQL stay refused (R-463: no pg_upgrade, refuses to start on an
older major's datadir across eleven templates).

A MariaDB major is now allowed ONLY as its own edge: never in the same commit as
another image move in that template (R-450, the bookstack 0b73e5e shape).

Two new decoy cases; two red-proofs, each seen to fail. 40 cases green.
No template moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-21 12:49:18 +02:00
admin 18a6d2d824 GATE FIX: the copy gate could not run in CI at all (R-595)
gates / gates (push) Successful in 1s
Six pushes in a row turned CI red while the local pre-push hook was green. The alarm
mail's own text says what that means and that it outranks the push it interrupted.

Cause, found by contrast rather than by reading a log: check-copy-i18n.py imports
PyYAML and is the ONLY gate in this repo importing anything outside the standard
library. The workflow's own header says the runner is "a host-mode container with
python3 and git and nothing else". The gate raised ImportError before checking
anything, so catalog_gates.py exited non-zero on every push, clean ones included.

The fix is a DEGRADED MODE, not a skip: without PyYAML the gate runs the check that
needs no parser and matters most — every frozen Hungarian string must still occur
verbatim in its app's bytes — and then prints in full what it did NOT check. Same
division catalog_gates.py already uses for engine-major on a shallow clone.

Measured before claimed: 1 030 of 1 032 frozen strings appear byte-for-byte in the raw
files; the two that do not are romm help_texts whose YAML escapes an inner double
quote, so the escaped spelling is accepted too. 1 032 of 1 032 found, so the degraded
check convicts nothing honest.

Five new decoy cases run with PyYAML shadowed by a module that refuses to import —
what CI actually executes. 38 cases in total.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 18:28:27 +02:00
admin 1f8065076b English copy — BATCH 3 of 3: the last 16 apps. THE CATALOG IS TRANSLATED.
gates / gates (push) Failing after 2s
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
2026-09-20 16:34:47 +02:00
admin 5fe70d1f8a English copy — BATCH 2 of 3: 17 apps, 317 strings (R-560 slice 5)
gates / gates (push) Failing after 2s
audiobookshelf, code-server, crafty-controller, glance, gokapi, gramps-web,
jellyfin, mealie, navidrome, plex, sonarr, tandoor, termix, uptime-kuma, vikunja,
wger, wishlist. EN_MISSING_CEILING 624 -> 307.

Two lines needed judgement rather than translation, and both are written up in the
CHANGELOG so a later reader does not take them for slips. Jellyfin's setup step tells
the reader to pick Hungarian in the wizard — wrong advice for an English household,
so the English says "choose your language". Mealie's "Hungarian is available too"
becomes "English and Hungarian among them". Neither adds a promise the Hungarian does
not make; the Hungarian is untouched in both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 16:32:18 +02:00
admin 0695d8eeda English copy — BATCH 1 of 3: 17 apps, 319 strings (R-560 slice 5)
gates / gates (push) Failing after 2s
adventurelog, bentopdf, bookstack, calcom, calibre-web, ghost, grafana,
home-assistant, homebox, homepage, n8n, nextcloud, outline, papra, radarr,
sparkyfitness, wanderer. EN_MISSING_CEILING 943 -> 624.

No Hungarian byte moved; no image, pin, catalog_since or compose line changed.

The blocks are GENERATED from a flat {path: english} map rather than hand-written:
fifty nested blocks whose keys must match the Hungarian exactly is fifty chances to
mistype an env_var, and a mistyped key is INERT on the box rather than an error, so
nobody would learn. The generator builds from the same flat paths the freeze uses,
derived from the Hungarian file itself, so an invented key cannot be written.

One string is deliberately untranslated: papra's AUTH_SECRET description is a
Hungarian DEFECT (it describes a session-signing key as "the app's subdomain").
Translating it faithfully would ship the error in a second language; changing the
Hungarian is forbidden in a localisation release. It falls back, papra stands at
13/14, and the ceiling's floor is 1 until R-593 is fixed — stated in the ceiling's
own comment so a later batch does not "fix" it by editing Hungarian.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 16:29:37 +02:00
admin e81d41e527 English copy — the PILOT: privatebin, paperless-ngx, romm (R-560 slice 5)
gates / gates (push) Failing after 2s
89 of 1 032 strings. No Hungarian byte moved; no image, pin, catalog_since or
compose line changed. EN_MISSING_CEILING 1032 -> 943 in this commit.

Chosen for SHAPE: privatebin exercises the plain case (description, tagline, lists);
paperless-ngx adds select options, a placeholder and a customer-facing folder label;
romm carries the catalog's only optional_config block, whose group has no id of its
own and is matched by `match_group` — the Hungarian group name it translates. All
three run on the demo box, so the English pages can be fetched rather than reasoned
about.

Two gate defects fixed while translating, each found by its own decoy rather than by
reading: coverage was counted only for the apps NAMED on the command line, so
`check-copy-i18n.py privatebin` reported 47 more missing strings than the same tree
unscoped and either number could have been made to "pass"; and the ASCII-Hungarian
stems matched as bare substrings, so „ird be" convicted "the third best" and „angol"
convicted "Angola". Both now have their own case in the decoy suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-20 14:34:22 +02:00
admin 73a9bc4807 vaultwarden: compose header matches invite-first; live proof lines (R-512/R-514)
gates / gates (push) Successful in 1s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-15 11:28:51 +02:00
admin e6aa4434c3 vaultwarden: signups closed by default, invite-first card (R-512); paperless: 1 worker, 1280M, card creds text (R-514/R-515)
gates / gates (push) Successful in 1s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-15 09:57:08 +02:00
admin a161ccb918 privatebin 2.0.6 -> 2.0.5: revert the BIGNIGHT drill bump (d5d91e0) in the same phase
gates / gates (push) Successful in 1s
2026-09-14 21:17:55 +02:00
admin d5d91e09a0 privatebin 2.0.5 -> 2.0.6: DRILL bump for the BIGNIGHT guarded-update walk (reverted in the same phase)
gates / gates (push) Successful in 1s
2026-09-14 20:59:36 +02:00
admin 8555660123 adventurelog: the backend router targets port 80 (nginx + X-Accel-Redirect), not gunicorn — photos came back empty (R-483) 2026-09-13 21:45:52 +02:00
admin 317225868c adventurelog: route /media, /static, /admin, /accounts to the backend — photos rendered as broken content (R-483)
gates / gates (push) Successful in 1s
2026-09-13 21:32:44 +02:00
admin 3f73c0e28a catalog-since gate: an image: move must bump the app's catalog_since (R-452) — hook-enforced, shallow CI skips out loud
gates / gates (push) Successful in 1s
2026-09-13 19:42:13 +02:00
admin 50ad28631e glance: seed a default glance.yml on first boot — the image crash-loops without one (R-473)
gates / gates (push) Successful in 1s
2026-09-13 19:23:51 +02:00
admin ed2c01855b adventurelog: DEBUG=False on the backend — the image defaults to Django debug pages on the public origin (R-482)
gates / gates (push) Successful in 1s
2026-09-13 19:22:35 +02:00
admin 48d2003b7e CHANGELOG + REPORT: slice-4 live-test commits, all reverted — no net catalog change
gates / gates (push) Successful in 1s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-13 12:29:59 +02:00
admin 3525e355a1 CHANGELOG + REPORT: MARIADB_AUTO_UPGRADE on four db services, engine-major gate, harness verdicts
gates / gates (push) Successful in 0s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-13 09:58:02 +02:00
admin b7ef0c4a09 upgrade-test.py: record the engine's own view of its datadir, beside the verdict
gates / gates (push) Successful in 1s
R-459. The harness returned proven for E3b while MariaDB was logging that the
conversion it requires had been skipped. The verdict was right - the app's data
survived, which is what it asked - but the harness watched the app and the
migration log, and neither looks at engine state.

engine_state_after now carries each database service's own answer: MariaDB's
datadir version plus 'mariadb-upgrade --check-if-upgrade-is-needed', and
PostgreSQL's PG_VERSION.

It sits BESIDE the verdict and is never folded into it. An unconverted datadir is
not known to be a failure - 5 of 5 restarts showed no degradation - so a verdict
that called it failed would encode an unproven judgement, which is worse than
reporting a fact and letting a person read both.

No template changed. Nothing with MARIADB_ in it is committed by this work: that
is a fleet-wide decision the operator owns, and it affects four apps.
2026-09-06 17:38:50 +02:00
admin 0474ce387e upgrade-test.py: measure whether a real app upgrade keeps the customer's data
gates / gates (push) Successful in 0s
R-449. Until today one upgrade out of 53 had ever been measured - Nextcloud, by
hand, in a spike - and the whole update arc was designed against that single data
point.

Per edge: deploy at FROM, seed through the app's OWN interface, prove the seed
reads back, swap to TO, ask the app for the data again, then put the FROM images
back and record what happens - verbatim, and never called a rollback.

Success is an application-level readback, not file identity: survive2.py's
sha256+inode rule is right for a redeploy and wrong for an upgrade, because a
migration is supposed to rewrite files. And nothing is ever seeded by hand (R-156)
- an app with no non-browser route is recorded inconclusive, never faked.

C3 is a negative control whose TO image exits immediately, and it must be run
first: it came back failed, which is what makes the greens mean anything.

The bookstack fixture uses artisan for both halves and carries its own negative
control on every call, because the obvious HTTP-login readback cannot work: the
template's https APP_URL makes the session cookies secure, so curl over http gets
419 on every login and it looks exactly like a wrong password.
2026-09-06 11:44:18 +02:00
admin 7b9b9b34a5 CHANGELOG: record the two v0.235.0 live-test pushes and their reverts as a measurement, not a release
gates / gates (push) Successful in 1s
2026-09-06 10:23:06 +02:00
admin 69761cf91b catalog_since on all 53 apps: the date this repo last moved each app's pinned images
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).
2026-09-02 20:15:33 +02:00
admin 5d8f25f611 CHANGELOG: record the bentopdf pin move + revert as a spike measurement, not a release
gates / gates (push) Successful in 1s
2026-09-01 19:59:22 +02:00
admin 29edad9c5b decoy sweep: no gate changed here, and that is the result (R-421)
gates / gates (push) Successful in 1s
All 29 gate scripts across the four repos were read and DECOYED - the label constructed without the
fact, the gate run, the verdict recorded. 16 were fooled. None of them were in this repo.

A decoy that nobody would write proves nothing, so the attempts that turned out illegitimate were
WITHDRAWN rather than counted. Both of this repo were withdrawn, and both are named in the audit.

The gates here that could not be given a plausible decoy are listed BY NAME in
felhom.eu/scripts/decoy_coverage_gate.py EXEMPT (R-426) as UNTESTED - not as sound. A gate nobody
tried to fool is UNKNOWN, and calling it sound would be the same confident guess this sweep exists
to find.

Survey table: felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md
2026-09-01 12:38:59 +02:00
admin 459766cb16 docs: R-168 is CLOSED — the "CI is still owed" sentence was stale (R-229 part 2)
gates / gates (push) Successful in 0s
Corrected in all four instruction files across all four repos. Found while confirming this
session own push by run ID, which is precisely the check that catches it.

In felhom-agent/CLAUDE.md the sentence contradicted the same file release section, which
already said R-168 mails the failure -- a contradiction inside one instruction file, the exact
class the R-229 work exists to find.

REPORT.md deliberately NOT overwritten in the sibling repos: a one-line docs correction must not
destroy the record of their last real implementation.
2026-08-06 11:03:02 +02:00
admin 122bbeea48 papra: mount the volume where the app actually writes (R-156, last leg)
gates / gates (push) Successful in 1s
papra mounted papra_data:/app/data while the application writes to
/app/app-data, so its database sat in the container's writable layer: lost on
redeploy, and tarred nightly as an empty directory while the healthcheck stayed
green. Last of the three apps R-156 convicted.

Decided from the IMAGE, not the README. docker inspect of
ghcr.io/papra-hq/papra:26.6.1-rootless gives WORKDIR=/app and all three data
paths under ./app-data (DATABASE_URL, DOCUMENT_STORAGE_FILESYSTEM_ROOT,
PAPRA_CONFIG_DIR) — and /app/data does not exist in the image at all.

Reconfiguring the app to write to /app/data was available and deliberately not
taken: it enumerates data paths, so a fourth added upstream would escape to the
writable layer again, silently — this defect re-armed. Mounting the app's own
data ROOT captures every current and future path by construction.

Precondition checked rather than inherited: docker ps -a (including stopped) on
BOTH demo guests, plus the hub fleet view (two enrolled hosts, zero papra) —
both boxes were wiped and rebuilt today, so the 2 August evidence was re-measured.

Proven by the runtime gate in both directions: CLEAN with the self-test passing,
and BROKEN when the mount is reverted, with the exact R-156 evidence. Full
catalog_gates.py papra: all three gates OK.
2026-08-03 11:29:51 +02:00
admin 7cb58ecdf8 docs: CHANGELOG + REPORT for the CI workflow
gates / gates (push) Successful in 1s
2026-08-02 16:35:45 +02:00
admin 340ff2a2d6 docs: CHANGELOG for catalog_gates --fast + the pre-push hook 2026-08-02 15:28:43 +02:00
admin fd7747d129 catalog gates: one entry point, mandated in CLAUDE.md (R-161 ruling)
scripts/catalog_gates.py runs all three gates - image-pins, image-resolvable,
volume-persistence - and exits non-zero if any fails. Mandated in CLAUDE.md the way
felhom.eu/scripts/site_gates.py is: run it after any template change, naming the
app(s) you touched.

Operator ruling, recorded because both alternatives were rejected for measured
reasons. Controller-side enforcement at template load was rejected because such a
check can only read the file, and a static audit of all 53 templates reports the
catalog clean INCLUDING papra - it would pass on the exact defect it exists to
catch; the property is decidable only at runtime. CI was rejected for now: neither
repo has any, and there are no users yet. What was chosen copies the shape that
demonstrably works here - of this project's gates, the only ones that ever get run
are the ones with a single entry point named in a CLAUDE.md; site_gates.py is run,
and R-29's three orphans are named nowhere and have stopped nothing.

Behaviour: 0 all clean / 1 convicted / 2 UNDETERMINED, never a pass; a conviction
outranks an undetermined result so the reader knows which they have. Gate output is
streamed, not captured. App names scope the two gates that accept scoping; with no
names the runtime gate deploys every template and belongs on a scratch host.
Adding a fourth gate means one line in GATES.

R-161 stays OPEN at reduced scope: this is convention, run by a person. Real
automatic enforcement is owed when a second person touches templates.

Verified: image-pins passes standalone (53 templates, 0 unpinned), the
unknown-option path exits 2, and the aggregation was unit-checked over five
gate-code combinations. The runtime leg was deliberately NOT executed - it deploys
templates via docker compose and DooPlex is the recovery chain - so the runner's
end-to-end invocation of that third gate is inferred, not measured, and is flagged
in REPORT.md to be closed on a scratch host at the next campaign.

REPORT.md overwritten per convention; the persistence sweep's report is preserved
at audits/persistence-sweep-2026-08-02/ and pointed to from the new one.
2026-08-02 14:03:55 +02:00
admin 2b22a23d60 persistence sweep: 53 templates measured; gramps-web + wishlist fixed; runtime gate added
Campaign 10's R-156 found papra writing its database into the container's writable layer while the
volume the template preserves stayed empty — a backup that completes, verifies, and contains
nothing. papra was never the point: nothing anywhere checked that the folder a template preserves
is the folder the app writes to. All 53 templates have now been measured live.

43 CLEAN / 3 BROKEN / 7 UNDETERMINED. UNDETERMINED is counted separately, each with its reason,
and never folded into CLEAN.

FIXED (neither app is deployed anywhere, so nothing was stranded):
- gramps-web mounted /app/data, /app/media, /tmp — and /app/data is a path the application never
  writes. Its accounts database and ITS FAMILY TREE both landed in the writable layer while
  gramps_data was tarred nightly as an empty directory. Now persists the eight paths the image's
  own environment names, matching upstream's reference compose. Proven: users.sqlite and the
  family-tree files survive a redeploy byte-identical, same inode.
- wishlist mounted wishlist_data:/data, another path the app never writes; prod.db landed in the
  ANONYMOUS volume from the image's VOLUME directive — absent from ResolveDockerVolumeNames, so
  never backed up, and orphaned by a redeploy. Now mounts /usr/src/app/data + /usr/src/app/uploads.
  Proven: prod.db byte-identical, same inode, across a redeploy.

Every corrected path confirmed by two independent sources — the shipped image's own
environment/Config.Volumes and upstream's reference compose — never inferred from a directory name.

papra is NOT fixed. It is live on one box, and changing the mount target makes the next compose up
recreate the container and destroy the writable layer its documents live in. The fix is prepared
and proven in the scratch guest (current: db.sqlite differs after a redeploy, so a real account
created via the API is lost; fixed: byte-identical, it survives). Referred to the operator with the
two options; no migration written.

NEW GATE scripts/check-volume-persistence.py — the third catalog gate and the only RUNTIME one.
This class is invisible to static analysis, measured not assumed: a static audit of all 53 composes
reports the catalog clean AND reports papra clean. Exit 0 clean / 1 REFUSED / 2 undecided. It
refuses to report at all unless it has just re-proven itself in both directions against two canary
templates that differ only in which path the volume mounts at, so every run carries a live
demonstration of R-156 and of its fix. No docker exec anywhere (Campaign 7 §1.1). 44 fixture tests
driving check(), the function __main__ calls; every rule red-proofed.

Enforcement is convention, not CI — this repo has no CI. Stated plainly in the report; raising it
is proposed as R-160.

Report, per-app evidence, proofs and proposed register entries (R-158..R-161, NOT filed — felhom.eu
is fenced this session): audits/persistence-sweep-2026-08-02/
2026-08-02 12:21:30 +02:00
admin a32541684a catalog: lifecycle field replaces the retired/ directory move
Moving a template out of templates/ un-offers it but also makes the
controller's orphan detector see it as GONE for anyone already running the
app - flagging their working install Elavult with a Torles button. Withdrawing
an app must never take a working app away from a customer.

Optional lifecycle: available|hidden|abandoned in .felhom.yml instead.
plant-it returns to templates/ as the first abandoned app; retired/ removed.
Resolvability gate skips (and reports) non-available apps.
2026-07-21 16:19:49 +02:00
admin b3eabfd611 catalog: re-pin wanderer to the current upstream shape, retire plant-it, add the resolvability gate
wanderer: ghcr.io/flomp/wanderer:0.16.0 is a ghost - upstream split the app
into web+db images, moved registry and renamed the org. Restructured to
upstream's own v0.20.0 compose (3 services, new /data/plugins volume, second
public hostname for PocketBase, meilisearch pinned DOWN to upstream's v1.36.0
per the R-42 ruling).

plant-it: retired. The repo name was wrong (plant-it-server) but upstream has
DELETED self-hosting; last server image is 2024-12-10 and it needs MySQL+Redis
the template never had. Moved to retired/ rather than deleted - reversible.

R-41 slice 1: check-image-resolvable.py. Encodes two traps - manifest inspect
exits 0 while printing toomanyrequests, and the inverse, where the first sweep
called 24 of 65 pins dead because Hub throttled it. Ambiguity is INCONCLUSIVE,
never an accusation.
2026-07-21 15:30:15 +02:00
admin 34d50a33ac docs: migrate workflow to DooPlex-local execution 2026-07-19 12:28:09 +02:00
admin a14c97db65 CHANGELOG + REPORT: campaign 7 full catalog sweep (53/53, 45 pass, 13 fixes)
Campaign 7 catalog sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-19 03:22:56 +02:00
admin 21e8df1e3a backup classification: backup: blocks for the 13 bind-bearing apps
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
2026-07-14 18:51:36 +02:00
admin 71828a81cf image pinning: eliminate :latest from all 5 unpinned templates + standing gate
bentopdf :latest -> v2.8.6; calibre-web :latest -> v4.0.6 (== running digest on
demo 9201, c31a738b - pin is a no-op); papra :latest -> 26.6.1-rootless (latest
was the rootless variant); recipe-importer :latest -> v0.9.11 (tag pre-existed,
digest-equal, no retag needed); termix :latest -> 2.5.0.

All five pins digest-identical to what :latest resolved to on 2026-07-12.
New gate scripts/check-image-pins.py (catches floating tags AND untagged refs;
red-proofed both shapes). Standing rule in CLAUDE.md + REUSE.md row.
2026-07-12 14:37:57 +02:00
admin 8ddd3c9da5 fix(healthcheck): sweep localhost -> 127.0.0.1 across all 48 templates
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
2026-07-06 20:25:54 +02:00
admin d86e25662f fix(vaultwarden): _ENABLE_SMTP boot-gate — fresh email-off deploys crash-looped (campaign F1)
Vaultwarden treats defined-but-empty SMTP_HOST/SMTP_FROM as "set"; with upstream
_enable_smtp defaulting true its validation errors out and the container
crash-loops on every fresh deploy with app-email off (the default). Gate the
SMTP group with _ENABLE_SMTP: compose default false (clean boot, mail off),
flipped "true" by the app-email injection via smtp_mapping.extra. Proven on the
pinned 1.33.2-alpine image (P1 repro exit 12 / P2 off boots / P3 on boots).
REUSE.md: strict-image TRAP row. Sweep note: the other 5 smtp-mapped templates
are campaign-boot-proven tolerant; no edits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-06 14:01:11 +02:00
admin 13eedb1a83 docs: CHANGELOG + REPORT — sparkyfitness finalization + validation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 19:48:31 +02:00
admin 15cdf7b090 docs: CLAUDE.md light expansion (repo purpose, deploy contract, pointers)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 11:26:12 +02:00
admin c5a3d1b15b docs: REUSE.md introduced — catalog conventions map (canonical app, healthcheck families, traps) + minimal CLAUDE.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 09:39:38 +02:00