Adds the CAMPAIGN 7 audit doc (53/53 apps deployed and validated on the demo box
through the real controller endpoints; 45 pass, 13 template fixes, ranked
findings) and the uninstall-semantics map row it proves out: remove requires stop
first, named docker volumes are ALWAYS destroyed including the DB, HDD data and
backups survive unless explicitly ticked, and the per-app offsite toggle outlives
the app while tier-2 config is cleared.
ROADMAP +3:
R-40 the update path cannot express a multi-hop major upgrade (nextcloud
forbids skipping majors; the template moved 31 -> 34)
R-41 no standing 'does every template still deploy?' check -- the sweep was
the first thing to try, and found 5 apps that had NEVER been deployable
R-42 ruling needed on DB/cache sidecar majors (deliberately not bumped)
CLAUDE.md: REPORT.md is overwritten, so a second concurrent session in this repo
writes REPORT-<topic>.md instead -- applied to this run's own report.
Campaign 7 catalog sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
**DB/cache sidecar majors were deliberately NOT bumped** (postgres 16→18, redis 7→8,
mariadb→12.3 on apps other than bookstack, postgis 16→17). This is a conscious deviation
from a literal reading of "newest stable for every pin":
- a DB major is a **data-plane decision the application owns**, not a currency decision —
immich proves it, upstream pins one specific tested postgres build;
- tags like `postgres:16-alpine` / `redis:7-alpine` already track the newest patch inside
their major, so they are not stale;
- blind-bumping ~8 apps onto PG18 would have manufactured failures that are artifacts of
the sweep's own choice rather than real findings, and would have burned the wall-clock the
rail explicitly told us to protect.
→ carried to ROADMAP as a decision item, not silently skipped. See §7.
---
## 5. Healthcheck audit — the wget lesson, systematically
Every deployed app's compose healthcheck was parsed and the probe binary checked **inside
the image**.
> **Finding C7-F4 (HIGH) — a lying healthcheck does not merely mislead; it takes the app
> OFF-LINE.** Traefik will not route to a container in an `unhealthy` state. So a probe that
> ENOENTs → permanent `unhealthy` → **Traefik returns 404 to the customer while the app is
> serving 200 perfectly well on its own port**. This was not theoretical: both apps below
> were completely unreachable for that reason, and both looked "deployed" in docker ps.
| app | lie | reality | fix | verified |
|---|---|---|---|---|
| **adventurelog** (frontend) | `wget --spider` | distroless image: no shell, no wget, no curl; node only at `/nodejs/bin/node`, off PATH | Node-exec family via the absolute interpreter path | 404 → **200**, 3/3 healthy |
| **emby** | `curl -f` | no curl, no standalone wget — image ships only BusyBox v1.38 | BusyBox-wget family via `/bin/busybox wget` | unhealthy/404 → **healthy/302** |
Both fixed in-template and live-re-validated on the demo box.
---
## 6. Result matrix
**All 53 catalog apps were attempted.**`http` is through the real Traefik ingress; a 3xx to a login
page is a PASS. `login` was attempted where credentials are documented and the flow is scriptable.
### 6.1 Passed (45)
| app | old → new pin | MAJOR | deploy | health | http | logs | settle |
|---|---|---|---|---|---|---|---|
| actualbudget | 26.1.0 → 26.7.0 | | ok | ok | 200 | clean | 10s |
¹ the customer trio — left RUNNING as the end state (see §3). Login: bookstack **ok**,
calibre-web **ok**, immich page-only (deliberate, §3). For the other 40 apps login was
`not-attempted` — a rendered login page through the real ingress was taken as PASS per the rail.
### 6.2 Not passing (4) + 1 not attempted
| app | outcome | cause | disposition |
|---|---|---|---|
| **glance** | FAIL — crash-loop | needs `/app/config/glance.yml`; the template mounts an EMPTY config volume and never seeds one | **pre-existing** — proven: v0.7.4 (the pre-campaign pin) fails identically. Bump KEPT, finding raised |
| **gokapi** | FAIL — crash-loop | v2.2.4 refuses to run against the seeded ConfigVersion-21 config: *"Please update to version 2.0.0 before running this version"* | **pin REVERTED to v1.9.6** (last known-good); v2 migration needs the seeded config regenerated |
| **plant-it** | FAIL — image | `msdeluise/plant-it` does not resolve on Docker Hub — **neither 1.0.1 nor the shipped 0.10.0** | **pre-existing**; no replacement registry found. Needs upstream research |
| **wanderer** | FAIL — image | `ghcr.io/flomp/wanderer` does not resolve — **neither 0.20.0 nor the shipped 0.16.0** | **pre-existing**; meilisearch sidecar bumped v1.12 → v1.49. Needs upstream research |
| **plex** | not attempted | `PLEX_CLAIM` is a required field with no default — a real claim token from plex.tv is needed | not automatable; genuinely owner-supplied. Not a defect |
---
## 7. Findings, ranked
**F4 (HIGH) — a lying healthcheck takes the app OFF-LINE, it does not merely mislead.**
Traefik refuses to route to a container in `unhealthy` state, so an ENOENT'ing probe →
permanent `unhealthy` → **404 for the customer while the app serves 200 on its own port**.
Seven apps were affected. Detail in §5 and F5 below.
**F5 (HIGH) — 7 of 53 apps shipped a broken or wrong healthcheck.** All fixed and live-re-validated:
| app | the lie | reality | fix |
|---|---|---|---|
| adventurelog (frontend) | `wget --spider` | distroless: no shell/wget/curl; node only at an absolute path | Node-exec via `/nodejs/bin/node` |
| emby | `curl -f` | no curl, no standalone wget — BusyBox only | `/bin/busybox wget` |
| R-37 | **Post-RESET health card shows stale pre-RESET warnings.** After a RESET the card should read **„RESET óta nincs adat"** instead of carrying warnings about a lifecycle that no longer exists. | XS | **SHIPPED (hub v0.67.0, 2026-07-18)** | The customer page raises a banner when a RESET **completed** after the newest report, quoting „RESET óta nincs adat" and the reset timestamp, because until the box reports again every health figure describes a lifecycle that no longer exists. Deliberately narrow: an **in-flight** reset does not trigger it (only a completed one), and it **clears itself** on the first post-RESET report. Ties resolve to STALE — SQLite timestamps are second-resolution and a same-second report almost certainly arrived just before the reset destroyed what it describes; erring the other way would hide the banner exactly when it matters most. Red-proofed (neutering the predicate fails the assertion). — Origin: 2026-07-18 rehearsal. Same family as R-36 — the hub knows the state changed and the UI has not caught up |
| R-38 | **Installer GRUB slice.** A single default „Felhom telepítés" entry; the **interactive installers REMOVED** (safety: an interactive entry is how a wrong-disk manual install happens); felhom background. | S | idea | Origin: 2026-07-18 rehearsal, alongside R-21's physical closure. **Squashfs/theme rebranding explicitly DEFERRED** — this item is the menu and the safety, not a skin |
| R-40 | **[P2-HIGH] The update path cannot express a MULTI-HOP major upgrade.** A template pin is a single value; the customer's update button pulls whatever the catalog now says. For apps whose upstream forbids version skipping this produces a broken upgrade. Nextcloud is explicit: *"You cannot skip major releases. Please re-run the upgrade until you have reached the highest available release."* Campaign 7 moved its template **31 → 34** (a fresh deploy validates fine — 302, 3/3 healthy), so an existing 31 customer pressing update would attempt a jump Nextcloud refuses. | M | idea | Origin: CAMPAIGN 7 (`audits/CAMPAIGN-7-catalog-sweep-2026-07-19.md` §7 F7). Not nextcloud-only — any app with sequential-major rules (gitea, tandoor, outline…) has the same shape. Directions: a per-app `upgrade_path:`/`max_hop:` in `.felhom.yml` that the update button walks in stages; or refuse-and-explain when the installed major is >1 behind; or pin an intermediate "stepping-stone" tag. **Until this exists, a >1-major catalog bump is safe for NEW deploys and unsafe for the update button** — which is exactly the asymmetry the campaign's MAJOR flag was meant to record but cannot enforce |
| R-41 | **[P2-HIGH] The catalog has no standing "does every template still deploy?" check.** Campaign 7 was the first thing that ever tried to deploy all 53 apps, and found **5 that had NEVER been deployable**: papra (missing required `AUTH_SECRET`), zipline (v4 renamed `CORE_DATABASE_URL` → `DATABASE_URL`), wishlist (Docker Hub image gone; upstream moved to ghcr.io), homebox (upstream dropped the `v` tag prefix + new required env), glance (needs a seeded `glance.yml` the template never provides — PROVEN pre-existing: the pre-campaign v0.7.4 pin fails identically). Plus **7 broken healthchecks** and 2 apps whose images no longer resolve at all (plant-it, wanderer). | M | idea | Origin: CAMPAIGN 7 (§7 F5/F6). The repo already has the right pattern in `scripts/check-image-pins.py` — a mechanical gate run on every change. Cheap first slice: a **resolvability gate** (`docker manifest inspect` every pin) would alone have caught plant-it, wanderer, wishlist and homebox, and needs no box. Full slice: a periodic deploy-all sweep on the demo box reusing the campaign's engine. **Silent rot is the real risk** — an app can die upstream and nobody learns until a customer clicks Telepítés |
| R-42 | **Ruling needed: do DB/cache sidecar majors follow the app, or the newest tag?** Campaign 7 deliberately did NOT bump sidecar majors (postgres 16→18, redis 7→8, mariadb 11.6→12.3, postgis 16→17) while bumping ~40 app images to current. | S | **decision pending (Viktor)** | Origin: CAMPAIGN 7 §4. The case for not bumping: a DB major is a **data-plane decision the application owns** — immich proves it, upstream pins one specific tested `postgres:14-vectorchord…` build — and `postgres:16-alpine`/`redis:7-alpine` already track the newest patch inside their major, so they are not stale. The case for bumping: EOL majors eventually stop getting security patches, and "we never bump" silently becomes "we ship EOL databases". Suggested shape: per-app sidecar pin follows **upstream's own compose** where upstream publishes one, else stay within the current major and revisit at that major's EOL date |
## Pre-invite checklist — what stands between here and the first remote tester
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.