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.
This commit is contained in:
2026-07-21 15:30:15 +02:00
parent 34d50a33ac
commit b3eabfd611
12 changed files with 575 additions and 71 deletions
+71 -52
View File
@@ -1,68 +1,87 @@
# REPORT — CAMPAIGN 7: full app-catalog sweep (2026-07-18 → 2026-07-19)
# REPORT — catalog honesty pass (TASK-E Part 3), 2026-07-21
**Scope:** every app under `templates/` — bump the pin, deploy it through the real pipeline on the
demo box, prove it works, audit its healthcheck, record, remove.
**Box:** demo guest 9201 (`demo-felhom` on `felhom-pve`), controller **0.146.0**.
**Full matrix + ranked findings:**
`felhom.eu/documentation/audits/CAMPAIGN-7-catalog-sweep-2026-07-19.md`
## What ran
## Outcome
Diagnose-and-re-pin for the two Campaign 7 apps whose images "do not resolve at all", plus R-41
slice 1 (a standing resolvability gate). Baseline `34d50a3`, clean tree.
| | count |
## wanderer — RE-PINNED (upstream is alive; the template pointed at a ghost)
| | |
|---|---|
| apps attempted | **53 / 53** (full coverage, no remainder) |
| passed end-to-end | **45** |
| still failing | **4** (glance, gokapi, plant-it, wanderer) |
| not automatable | **1** (plex — needs a real `PLEX_CLAIM` token) |
| template fixes committed + live-re-validated | **13** |
| broken healthchecks found and fixed | **7** |
| apps that had NEVER been deployable, now fixed | **5** |
| Old pin | `ghcr.io/flomp/wanderer:0.16.0`**does not resolve** (verified `docker manifest inspect`) |
| Failure class | **repo split + registry move + org rename**, all at once |
| New pins | `flomp/wanderer-web:v0.20.0`, `flomp/wanderer-db:v0.20.0`, `getmeili/meilisearch:v1.36.0` |
| Evidence | upstream `docker-compose.yml` at tag `v0.20.0` (github.com/open-wanderer/wanderer); latest release 2026-07-07; all three tags confirmed present |
## Pre-flight gate — PASSED
Compose restructured from 1 service to 3 (web + PocketBase + Meilisearch), per upstream. Notable
consequences, none of them cosmetic:
`artifact_golden_version` = 0.146.0 (sha `4834c703…e955`), `min_controller_version` = 0.146.0,
guest running `felhom-controller:0.146.0` healthy and reporting. The sweep validated on the version
customers will actually run.
- **A second public hostname was unavoidable.** `PUBLIC_POCKETBASE_URL` is browser-side, so
PocketBase must be publicly reachable; added `SUBDOMAIN_DB` (default `hike-db`). Upstream's own
proxy example does the same.
- **New required volume** `/data/plugins` — v0.20.0 moved the Strava/Komoot/Hammerhead integrations
into a WASM plugin sandbox.
- **New generated secret** `POCKETBASE_ENCRYPTION_KEY` (`hex:16` → exactly 32 chars, as required).
- **Meilisearch pinned DOWN v1.49 → v1.36.0**, applying the R-42 ruling (sidecar follows the app
template's proposed pin). Campaign 7 had bumped it while the app image was dead.
- `mem_limit` 384M → 1024M = the exact sum of the three services (REUSE.md §2 rule).
- Probe container name kept equal to the stack dir (`wanderer`), or the health probe would hit the DB.
## The headline finding
**Not deployed live.** This is a from-scratch 3-service restructure of an app that has never once
deployed successfully; it wants a real deploy on the demo box before anyone is invited to it.
**A lying healthcheck does not merely mislead — it takes the app off-line.** Traefik refuses to
route to a container in `unhealthy` state, so a probe that cannot execute (missing `wget`/`curl`,
wrong port, wrong path, HEAD-vs-GET) produces a permanent `unhealthy`, and the customer gets a
**404 while the app is serving 200 perfectly well on its own port**. Seven apps were in that state.
## plant-it — RETIRED (operator ruling 2026-07-21)
## Second finding
The pin was only slightly wrong — repository is `msdeluise/plant-it-server`, and `0.10.0` was the
right version. Correcting the name would have been the wrong fix:
**5 of 53 apps had never been deployable at all** — papra, zipline, wishlist, homebox, glance —
and none of it was caused by this sweep; the sweep is simply the first thing that ever tried to
deploy them. Four are fixed. The catalog has no standing "does every template still deploy?" check,
and that absence is the real gap.
- Upstream **discontinued self-hosting**: `backend/` and `deployment/` are deleted from `main`; the
project is now an Android app on F-Droid/Obtainium, maintainer notes development has slowed.
- Last server image pushed **2024-12-10** — a security-frozen Spring Boot 3.4.0.
- Requires **MySQL 8.0 + Redis**. The template claimed "Database: None (file-based)" — never true.
## End-state (verified against the dashboard's own state, not just `docker ps`)
Moved to `retired/plant-it/` rather than deleted: the controller discovers apps by directory, so this
un-offers it while keeping it revivable with one `git mv`. Catalog is now **52 apps**.
- Deployed customer apps: **bookstack, calibre-web, immich** — the trio, freshly redeployed on
current versions, all containers healthy, offsite toggles restored to their recorded
pre-uninstall state (`offbox: true` on all three).
- Protected stacks untouched and running: traefik, cloudflared, filebrowser, samba, controller.
- Orphaned stacks: **none**. Stuck `deploying` flags: **none**.
- `app.yaml` present for the trio only; every other campaign app removed via the real delete flow.
- Image prune run. Docker volume: **179 GB free** (pre-campaign 178 GB).
- No campaign residue in `/opt/docker/stacks/` beyond synced templates.
## R-41 slice 1 — `scripts/check-image-resolvable.py`
## Needs a decision or a follow-up task
Complements the syntactic pin gate, which cannot see rot. Resolves each unique pin one image at a
time; exit 0 / 1 (GONE) / 2 (inconclusive). 14 fixture tests, no network.
1. **Multi-hop major upgrades** — nextcloud states "You cannot skip major releases"; the template
moved 31 → 34. Fresh deploys are fine, but an existing customer's update button cannot express a
staged upgrade. Systemic; ROADMAP.
2. **DB/cache sidecar majors** — deliberately not bumped (postgres 16→18, redis 7→8, mariadb→12).
Rationale in the campaign doc §4. Wants an explicit ruling.
3. **gokapi v2 config migration** — parked on v1.9.6 until the seeded `config.json` is regenerated.
Security-relevant: it should not sit on a superseded line indefinitely.
4. **plant-it / wanderer** — images unresolvable; need upstream research (finding, not a deletion).
5. **glance** — needs a seeded default `glance.yml`; it has never started from this catalog.
**Two traps encoded, both hit live during this change:**
## Secrets
1. `docker manifest inspect` prints `toomanyrequests: …` and **still exits 0** — the same
exits-0-on-failure shape as the ISO tooling's `validate-answer`. stderr is therefore checked
even on rc=0.
2. The inverse, and the more dangerous one: the **first full sweep called 24 of 65 pins dead**,
including `postgres:16-alpine` and `redis:7-alpine`, purely because Docker Hub throttled it
partway through. Ambiguity now resolves to INCONCLUSIVE, never to an accusation.
No secret value was logged, printed, or written to evidence at any point. Generated deploy secrets
are recorded as `<generated-secret, not recorded>`; the trio's credentials live in their `app.yaml`
on the box, out-of-band as usual.
### Sweep result — INCOMPLETE, and that is the honest answer
The full 65-pin sweep **could not be completed**: DooPlex is not logged in to Docker Hub, and the
anonymous limit was exhausted partway. The pre-throttle probes are trustworthy and are what the
findings above rest on:
| Image | Result | When |
|---|---|---|
| `ghcr.io/flomp/wanderer:0.16.0` | **GONE** | pre-throttle |
| `msdeluise/plant-it:0.10.0` | **GONE** | pre-throttle |
| `getmeili/meilisearch:v1.49` | resolves | pre-throttle |
| `flomp/wanderer-web:latest`, `flomp/wanderer-db:latest` | resolve | pre-throttle |
| `msdeluise/plant-it-backend/-frontend:latest` | resolve | pre-throttle |
| `flomp/wanderer-{web,db}:v0.20.0`, `getmeili/meilisearch:v1.36.0`, `msdeluise/plant-it-server:0.10.0` | exist (Hub tag API, HTTP 200) | post-throttle cross-check |
**Follow-up for the operator:** re-run `python3 scripts/check-image-resolvable.py` after
`docker login` to get the complete 52-app table. The gate now reports the throttle as INCONCLUSIVE
(exit 2) instead of inventing 24 dead images, so a repeat is safe to trust.
## Observations (recorded, not acted on)
- `REUSE.md` cited `felhom-controller/internal/stacks/healthprobe.go`; the real path has an extra
`controller/` segment. Corrected, but `reuse_refs_check.py` still flags it — it resolves citations
against the repo root and cannot follow a cross-repo pointer. Pre-existing checker limitation.
- The `-app`-suffixed wanderer tags (`v0.20.0-app`) are undocumented upstream; deliberately not used.
- 6 apps' `.felhom.yml` still carry `mem_limit` values that do not equal the sum of their services'
limits. wanderer is now correct; the others were out of scope.