diff --git a/REPORT.md b/REPORT.md index 6bf82ef..11d1add 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,187 +1,172 @@ -# REPORT — v0.236.0, R-442: "delete my data too" deletes the data, or says that it could not (2026-09-13) +# REPORT — Update arc slice 4: the Update button takes a backup first, and tells the truth (2026-09-13) *Overwritten each run. This records the most recent implementation only.* -## For the operator — what changed, in plain words +> **Shipped as controller v0.237.0 (the guarded job) and v0.238.0 (the page), both live on both demo +> guests, every scenario A–H proven live on demo-hp — including the restore walk.** Four claims in the +> prompt turned out wrong or incomplete; they are first, in §1. -When a customer removes an app and ticks *also delete my data*, the box now deletes the data and lists -what went. If the box cannot work out where the data is, it refuses, keeps the app, and says so in one -sentence. It never again reports success over data left behind. Proven on the HP with a throwaway -Nextcloud. Live on both demo machines. Nothing is waiting on you. +## 1. Claims in the prompt that turned out wrong, named first -## Claims in the task that turned out wrong or incomplete — named first +1. **"A release in this task raises the floor and does NOT need a bake."** Wrong, measured. The hub + HOLDS a floor above the vouched golden (publish-train rule 1): raising the floor to 0.237.0 made the + hub log `managed floor HELD for demo-hp: held: floor 0.237.0 is ABOVE the vouched golden 0.236.0 … + (controller floor withheld)`, the box logged `SetFloor: floor "0.236.0" → ""`, and neither box moved + in 8 minutes. Both releases were hand-deployed by the skill's route; the floor was put back to + 0.236.0. **This also falsifies the premise of this morning's golden-cadence ruling** — filed + **R-472**, an operator decision; the five documents that repeated the claim were corrected. +2. **"Is the restorable-unit predicate Tier-2-only?" — yes, and that is a finding.** The update + precondition is exactly what the backups page uses (`Tier2UnitRestorePoint`), so an app with no + Tier-2 copy cannot be updated at all — on demo-hp `gokapi` and `nextcloud` have no Tier-2 record. + The primary unit and the off-site unit are restorable routes it does not consider — **R-475**. +3. **"The proven copy date" is not one field.** The page names the unit MANIFEST's `created_at`, which + moves only when the app's definition changes — measured on demo-hp: bookstack's mirror held a + 2026-09-13T00:30Z dump under a manifest dated 2026-09-12T02:15:29Z. Aging the copy by it would call a + fresh copy stale forever and a backup-first would not fix it. The update ages by the last SUCCESSFUL + copy instead; the page's date is **R-476**. +4. **"A person clears it by restoring, exactly as for R-379."** Not exactly: an R-379 hold is cleared + only by the operator CLI (`-clear-restore-hold`); nothing in the restore path cleared it. Slice 4 + makes a successful unit restore lift an **update** hold only, and leaves R-379's rule unchanged. -1. **"Three lookups" was right, but the backup half needed the same fix, not only the same honesty.** - Under the global lookup, every `remove_backups: true` on every box was refused — not only paths - "outside the expected directory": with `hddPath == ""` the base was `/backups`, so nothing could - ever be under it. Making that refusal *visible* without fixing its *base* would have shown every - customer a wrong refusal. The base now follows the same per-app rule (the app's namespace root: - its own drive, or `/felhom-data` otherwise), which is exactly what the router's - `AppNamespaceRoot` produced the paths from. **This is one deliberate step beyond "and no more"** - (task §2, "the same honesty treatment and no more"), and the reason is above. -2. **"Deploy a small drive-backed app" — 8 of the 13 `needs_hdd` catalog apps have no `${HDD_PATH}` - bind at all.** They bind only `${USERDATA_PATH}` (the shared library) and named volumes. For them - "delete my data" correctly removes nothing on the drive, and the modal shows no checkbox. The - faithful throwaway was Nextcloud — the app R-442 was measured on. -3. **The task's §11 verifies the deploy on `felhom-pve` 9201 and validates on `demo-hp`.** Both guests - were deployed; the live validation ran on demo-hp as specified. -4. **"NOT ESTABLISHED: whether the fleet shares this config shape"** (the R-442 row) — now - established: demo-felhom also has 0 `hdd_path` lines and 0 `FELHOM_PATHS_*` variables. +Also: **the drive-return gate and the nightly volume dump started held apps** — "a hold that only one +path honours" was true of the R-379 hold too, before this slice. Both now honour it. -## Baselines (verified before starting) +## 2. Baselines, commits, deployed versions -| Repo | `main` before | after | version | -|---|---|---|---| -| felhom-controller | `bab82c471e03` | `42a73e6` (code) + this REPORT commit | v0.235.0 → **v0.236.0** | -| felhom.eu | `d6837d98ee24` | `4b2e560` (docs + evidence + gate fix) | — | +| repo | before | after | +|---|---|---| +| felhom-controller | `155271672265` (v0.236.0) | `0d402f7` v0.237.0 → `129201a` v0.238.0 → `cbcca03` v0.238.1 | +| app-catalog-felhom.eu | `3525e35` | live-test commits and reverts, **no net change** (see its REPORT) | +| felhom.eu | `abe567e` | documents + register (this session's final push) | -Highest `R-` id before: 464. **No new row minted.** R-442 closed. +**Deployed:** `gitea.dooplex.hu/admin/felhom-controller:0.238.1 Up … (healthy)` on guest 9201 of **both** +demo boxes, hand-deployed by the skill's route (the floor could not carry it — §1.1). Fleet floor left at +0.236.0, the vouched golden. -## Files +## 3. The two config knobs -- `controller/internal/stacks/delete.go` — `appHDDPath`, `composeBindsDrive`, `hddPathForRemoval`, - `hddNoteFor`; `RemoveRefusedError` + reasons; the three `m.cfg.Paths.HDDPath` reads replaced; - `[]` init; `hdd_paths_missing`, `hdd_note`, `backup_paths_refused`; backup base via - `appbackup.NamespaceRootFor`. -- `controller/internal/api/router.go` — `errors.As` → 409 + `Message` in `removeStack` and `deleteStack`. -- `controller/internal/web/templates/layout.html` — both success modals render `hdd_note` and - „Nem törölt mentések". -- Tests: `controller/internal/stacks/delete_r442_test.go` (13), `controller/internal/api/remove_r442_test.go` (2). -- Docs: `CHANGELOG.md` (v0.236.0), `CONTEXT.md`, `REUSE.md` (row 120), `controller/README.md` (3 rows). +| key | default | meaning | +|---|---|---| +| `update.backup_max_age` | `24h` | a proven Tier-2 copy older than this is refreshed before the update | +| `update.health_timeout` | `5m` | how long the new version has to become healthy before the app is held | -## Design rule this brings the removal path into line with +Plus two fixed rules, stated as fixed: the disk floor is **2 GB** (image size unknown without a registry +query) and an app with no `.felhom.yml` health check must run **60 s** with no container restarting. -`07-backup-architecture.md` ~L437, *"[DESIGN] 2026-08-22 — the restore destination is resolved by the -same rule as the capture destination: the drive if the app declares one (`HDD_PATH`), the system data -path otherwise."* Deploy (`withPathVars`), the start gate (`startGatedByMissingDrive`) and the backup -destination (`GetAppDrivePath`) already read the app's own record; removal was the odd one out. -**No fallback to the global** — the helper's comment says why. +## 4. Live evidence — endpoint-level, on demo-hp, throwaway app uptime-kuma -## Tests — 1761 in the module, 15 new, all green; gates all OK +All in `felhom.eu/documentation/audits/slice4-2026-09-13/live/`. Catalog syncs used `POST /api/sync`, the +dashboard's „Sablonok frissítése" button — the syncer's own entry point, not the 15-minute timer. -| Scenario | Test | Asserts (the effect) | Result | -|---|---|---|---| -| A drive app, data removed | `TestRemoveStack_R442_RemovesDeclaredDriveData` | folder gone from a real tree; listed with size; app.yaml gone; compose ran; `appdata/` root intact | PASS | -| B same app, data kept | `…KeepsDataWhenNotAsked` | file present; listed under preserved; `"hdd_paths_removed":[]` on the wire | PASS | -| **C unresolvable** | `…RefusesWhenDriveUnresolvable` | typed error, reason, exact sentence; **compose marker absent**; app.yaml present | PASS | -| **D SSD app** | `…SSDAppIsNotRefused` | no error; non-nil empty list; `[]` on the wire; note; app removed | PASS | -| drive absent | `…RefusesWhenDriveAbsent` | drive-absent sentence naming the path; nothing ran; app kept | PASS | -| keep-data never refused | `…KeepDataNeverRefusedOnHDDGrounds` | drive absent + `false` → removed | PASS | -| folder already gone | `…MissingFolderIsStatedNotRefused` | `hdd_paths_missing`, note names it, app removed | PASS | -| E backup half | `…BackupRefusalReachesResponse` | inside path removed + listed; outside path KEPT + `backup_paths_refused` | PASS | -| E SSD app backups | `…SSDAppBackupsUnderSystemNamespace` | `/felhom-data/backups/…` removable | PASS | -| `${USERDATA_PATH}` convention | `…UserdataConventionFromPerAppPath` | appdata removed, shared `userdata/` untouched; `ExportDataMounts` from the per-app path includes `/userdata` | PASS | -| orphan delete C / A | `TestDeleteStack_R442_*` (2) | same shapes for `DeleteStack` | PASS | -| modal source | `TestGetStackHDDData_R442_ReadsPerAppRecord` | lists the folder while the global stays empty | PASS | -| **production wiring** | `TestRemoveHandler_R442_*` (2) | real handler → real `NewManager` + `ScanStacks` → **409**, exact sentence / drive-absent sentence, app.yaml still on disk | PASS | - -**Red-proof 1 (Scenario C — the pre-fix silent fallback to the global, and nil lists):** 5 tests -failed, with the wrong values visible: -`want *RemoveRefusedError, got err= resp.hdd_paths_removed=[] app.yaml present=false — a 200 over inaction`; -handler: `HTTP 200 ok=true — a 2xx over a removal that could not resolve the data location`; -B: `got {"…","hdd_paths_removed":null,…}`. Fix restored → green. - -**Red-proof 2 (Scenario D — make "declares no drive" refuse):** 2 tests failed: -`an SSD app must not be refused on HDD grounds, got: Az alkalmazás adatainak helye nem állapítható meg…`. -Fix restored → green. `git diff` clean after both. - -Green gate: `go build ./... && go vet ./... && go test ./...` rc=0. `controller_gates.py --fast`: all OK. - -## Deployed - -`gitea.dooplex.hu/admin/felhom-controller:0.236.0` — `Up … (healthy)` on **both** demo guests -(felhom-pve 9201 and demo-hp 9201), verified with `docker ps`. - -## Live validation on demo-hp — endpoint-level (the exact endpoints the UI's modal and buttons invoke: `GET …/hdd-data`, `GET …/backup-data`, `POST …/stop`, `POST …/remove`), driven from inside guest 9201 with the session cookie + `X-CSRF-Token`. Evidence: `felhom.eu/documentation/audits/R442-2026-09-13/`. - -**Scenario A — Nextcloud, deployed through the real deploy endpoint, data written BY THE APP (its -first-run install: 69 files, 63 MB under `/mnt/felhom-drives/hdd_1/appdata/nextcloud`), stopped, -removed with `{"remove_hdd_data":true,"remove_backups":true}`:** +**A — real upgrade 2.3.2 → 2.4.0** (`05-A-update.txt`): 202 `{"accepted":true,"completed":false}`; phases +`safety-dump → pulling → starting → verifying → done`. Verbatim: ``` -{"ok":true,"data":{"removed":"nextcloud","volumes_removed":null, - "hdd_paths_removed":["/mnt/felhom-drives/hdd_1/appdata/nextcloud (63M)"], - "hdd_paths_preserved":[], - "backup_paths_removed":["/mnt/felhom-drives/hdd_1/backups/primary/nextcloud/db-dumps (8.0K)"]}, - "message":"Stack nextcloud removed"} HTTP 200 -ls: cannot access '/mnt/felhom-drives/hdd_1/appdata/nextcloud': No such file or directory -ls /mnt/felhom-drives/hdd_1/appdata → paperless romm -[INFO] Removed HDD data: /mnt/felhom-drives/hdd_1/appdata/nextcloud (63M) -``` -The db-dumps entry was a **planted** fixture file (`r442-fixture.sql`, stated) — no nightly had run -for a 3-minute-old app. The modal's `GET …/hdd-data` beforehand listed the folder with `63M` — under -0.235.0 that endpoint returned nothing for the same app. - -**Scenario C — Nextcloud redeployed, stopped, then `HDD_PATH` deleted from its `app.yaml` (a -test-only edit, backed up and restored), `{"remove_hdd_data":true}`:** - -``` -{"ok":false,"error":"Az alkalmazás adatainak helye nem állapítható meg, ezért semmit nem töröltünk. Az alkalmazás nem lett eltávolítva."} HTTP 409 -state=stopped deployed=True app.yaml present (1302 B) data files after: 69 (before: 69) -[ERROR] [stacks] RemoveStack nextcloud refused: data removal requested, the compose binds a drive path, but app.yaml records no HDD_PATH — nothing removed, app kept (R-442) -``` -`app.yaml` restored → the **same call** returned HTTP 200 with the 63M folder listed and gone -(positive control: the refusal was about the record, not the app). - -**Scenario D — Gokapi (SSD-only: 0 `HDD_PATH` lines in app.yaml, 0 drive binds in compose), -`{"remove_hdd_data":true}`:** - -``` -{"ok":true,"data":{"removed":"gokapi","volumes_removed":null,"hdd_paths_removed":[],"hdd_paths_preserved":[], - "hdd_note":"Az alkalmazás nem tárolt saját adatot külső meghajtón, így ott nem volt mit törölni."},…} HTTP 200 +update uptime-kuma: precondition met — proven copy from 2026-09-13T10:03:07Z (4m0s old, limit 24h0m0s) +update safety dump for uptime-kuma: the app has no database — nothing to copy (no-op) +update uptime-kuma: pin advanced to the catalog's current definition (uptime-kuma=louislam/uptime-kuma:2.4.0) +update uptime-kuma: healthy after 5s (the app's health check passed) +update uptime-kuma: DONE in 47s ``` -**ASCII-fragment controls** (`controls.txt`, counted on DooPlex over the saved bodies; `llap` is the -ASCII core of *állapítható*): C body = 2 (the JSON error + the router's ERROR line), A body = 0, -D body = 0 (in-guest count; the DooPlex file shows 1 from my own label line). Positive control that the -grep works: `hdd_paths_removed` = 1 in every file. `HTTP 409` appears only in C. +Final API: `update_phase: done`, pinned and installed `louislam/uptime-kuma:2.4.0`; the card reads +„Naprakész". The safety-dump **path** is empty because uptime-kuma has no database container — the +no-op the design specifies; the path shape is `pre-restore---.sql` (R-361). -Not validated live: Scenario B (kept data) and the drive-absent refusal — unit-tested only; no drive -was unplugged on the demo box. +**B — the copy is stale** (`06-B-setup.txt`, `07-B-update.txt`). **Stated test method:** the real knob +`update.backup_max_age: 2m` was appended to `controller.yaml` and the file restored byte-identical +afterwards (`sha256` prefix `042b71a3123d70da` before and after; no `update:` key remains). -## Teardown — three layers +``` +update uptime-kuma: the proven copy is 7m0s old (limit 2m0s) — backing up first +update pre-backup for uptime-kuma: volume dump OK +update pre-backup for uptime-kuma: recovery unit captured (0 database dump(s)) +Tier 2 copied uptime-kuma → /mnt/felhom-drives/hdd_1/backups/secondary/uptime-kuma (19.9 KB, 0 leg(s), 0s) +update pre-backup for uptime-kuma: complete in 1.395s +update uptime-kuma: DONE in 8s +``` -1. **Apps:** both throwaway apps (`nextcloud`, `gokapi`) were removed BY THE FEATURE UNDER TEST; all - nine standing apps still deployed (`bentopdf bookstack calibre-web docmost kimai opengist - paperless-ngx privatebin romm`); `bentopdf` kept. -2. **Residue:** the deploy's recovery-unit capture left `backups/primary/nextcloud/{compose/,manifest.json}` - (not customer data; removal deletes only `db-dumps/` by design) — removed BY HAND, stated here. - `appdata/` holds only `paperless`, `romm` — unchanged. -3. **Helpers:** `/tmp/r442.sh`, `/tmp/r442.pw`, `/tmp/r442-*.json` deleted from guest and host - (count 0). Nothing else was provisioned. The password file travelled file→file and was never printed. +Tier-2 `last_success` moved to 10:09:51Z; the new volume tar is in both the primary unit and the mirror. -## Pushes and gates — stated plainly +**E — the tag does not exist** (`08-E-setup.txt`, `09-E-update.txt`): `update_error` is the Hungarian +sentence `Az új verzió letöltése nem sikerült, …`, no Docker stderr in it; `pin and definition PUT BACK +to the pre-update version (uptime-kuma=louislam/uptime-kuma:2.4.0)`. **The app untouched:** container +`cb541381d93e…`, `started=2026-09-13T10:09:50.950051403Z` — identical before and after; live and stored +definitions `2.4.0`; no pre-update copies left. -- felhom-controller: both pushes passed the pre-push gates (`controller_gates.py --fast`, all OK; - `golden-notice` advisory). -- **felhom.eu `4b2e560` was pushed with `git push --no-verify`.** The only convicting gate was - `golden-currency` — it fails whenever the newest controller release has no golden, has NO waiver - logic (it does not read the register), and would fail this way for any release until a golden is - baked. R-467 is the waiver row it asks for. Everything else was green, including the - `closed-register` gate, which **crashed** (a `NameError` on its own conviction path) on my first, - two-column CLOSED row instead of convicting it. Fixed in the same commit - (`scripts/closed_register_gate.py`), seen to convict the two-column row, then the row was reshaped - to four columns and the gate passed. +**F — never healthy** (`11-F-setup.txt`, `12-F-update.txt`): catalog `alpine:3.20`. -## Rows +``` +update uptime-kuma FAILED after the new version was started: not healthy: not healthy within 5m0s (last: state restarting) — stopping and HOLDING the app; the pin stays on the new version (its migration may have run) +restore hold SET for uptime-kuma — the app stays stopped until it is cleared +``` -- **R-442 → CLOSED** (`CLOSED-ITEMS.md`, top). **Opened: R-465** (the six remaining `Paths.HDDPath` readers — audit), **R-466** (recovery-unit residue after "delete backups"), **R-467** (v0.236.0 owes a golden) — all P3-LOW, owner CC. Register: OPEN 208 → 210, CLOSED 168 → 169. -- `00-capability-map.md` lifecycle row narrowed (remove proved the app, not the data) and re-proven. -- `STATUS.md`: item 13 (plain language) + **item 7 closed** (ruled 2026-09-02, `09` §3). -- Highest `R-` id now 467. +Hold record `{"reason":"update_failed","copy_date":"2026-09-13T10:09:51Z","at":"2026-09-13T10:18:02Z"}`. +API and page: `A(z) uptime-kuma frissítése 2026-09-13 12:18-kor nem sikerült, … Visszaállítható a(z) +2026-09-13 12:09-i biztonsági mentésből a Mentések oldalon.` Card: `data-held="true"`, **0** lifecycle +buttons. App info, ASCII fragments with controls: `friss` ×3, `leáll` ×3, `Mentések` ×2, +`href="/backups/apps"` ×2. Container removed. -## Observations +**H — every path refuses the held app** (`13-H-refusals.txt`): `start`, `restart`, `update` → **409** +with the hold sentence each; after a controller restart: -1. **`cfg.Paths.HDDPath` still has readers** — `report/builder.go:69`, `monitor/healthcheck.go:35`, - `api/router.go` system-info, `web/server.go:740`, `main.go` (auto-discovery seed + metrics). Each - reads an always-empty value on the fleet; whether any of them is silently inert the way removal was - is worth one short audit. Not touched here. FILED: R-465 -2. **Removal with "delete backups" leaves the recovery unit's `compose/` + `manifest.json`** — the - router passes only `AppDBDumpPath`. Small, pre-existing; a customer who asked for backups gone is - left with a manifest. FILED: R-466 -3. **8 of 13 `needs_hdd` apps bind only `${USERDATA_PATH}`**, so their modal never offers the data - checkbox. Correct by the placement design (`01` — userdata is the shared library), but the label - „Felhasználói adatok a merevlemezen" only ever appears for the 5 that bind `${HDD_PATH}`. NOT-A-FINDING: this IS the `01` placement design — userdata is the customer's shared library, and an app removal deleting it would destroy media other apps and the customer own; the modal correctly offers nothing. -4. `app.yaml` carries `HDD_PATH` twice (`env:` and `locked_fields:`); the test-only edit removed the - `env:` line only, which is the one `appHDDPath` reads — proven by the refusal firing. NOT-A-FINDING: `locked_fields` is a list of field NAMES (which fields are locked after deploy), not a second value; nothing reads a path from it. -5. **v0.236.0 owes a golden** (the golden-notice gate, advisory): newest golden baked is 0.232.0. A bake + vouch is the `RUNBOOK-manual-build.md` §4.1 procedure and is outside this task; until then a fresh install receives 0.232.0 and picks up 0.236.0 by self-update. FILED: R-467 +``` +[bootrecon] "uptime-kuma" is a boot orphan by intent but is HELD (held after a failed update (2026-09-13T10:18:02Z) — restore it from its backup to start it) — NOT starting it +[bootrecon] Boot reconciliation: nothing to start — 1 app(s) held (…): [uptime-kuma] +``` + +The drive-return gate cannot be exercised without unplugging a drive; it is covered by +`TestSlice4_DriveReturnGateSkipsAHeldApp` with its red-proof, not live. + +**The restore walk** (`14-restore-walk.txt`): `POST /backup/tier2/unit-restore` (the page's own form) → +302; restore-status `ok: true`, `A(z) uptime-kuma: 1 adatkötet visszaállítva — az alkalmazás +újraindult. A visszaállítás forrása a második meghajtón lévő másolat volt (2026-09-13 12:09).` in 9.1 s. +Container `louislam/uptime-kuma:2.4.0 … (healthy)`, pinned 2.4.0, and +`uptime-kuma: restore completed — the update hold (set 2026-09-13T10:18:02Z) is CLEARED`. + +## 5. Tests, red-proofs, gate + +Full gate after each release: `go build ./...` rc 0, `go vet ./...` rc 0, `go test ./...` rc 0, **28 packages ok**. + +| red-proof | mutation | seen to fail | +|---|---|---| +| **1** (A) | health wait replaced by `true` | `the health wait was never reached; state: updating=false phase=done` | +| **2** (C) | preflight drops `!rp.Restorable` | `an app with no restorable copy must be REFUSED (no_backup), got ` | +| **3** (F) | the hold call skipped | `an app that did not come up must be HELD` | +| **4** (H, R-439) | `update` removed from the router's hold line | refused with the no-backup sentence instead of the hold's | +| nightly hold | isHeld skip removed from the volume dump | `the nightly volume dump touched a HELD app` | +| drive-return | appHeld check removed | `the drive-return gate tried to START a held app` | +| page ×2 | updating/held checks moved after `isOperational` | phase label missing, lifecycle buttons present | +| v0.238.1 | updating clause removed from isHeld | `a nightly leg touched an app MID-UPDATE` | + +**Three red-proofs first ran inertly and were fixed before they counted:** proof 1 did not compile, +proof 2's fixture was also unproven, so another check still refused, and proof 4 was masked by the +preflight's own hold check. Outputs: `audits/slice4-2026-09-13/redproofs/`. + +## 6. Rows + +Closed: **R-448, R-443, R-439**. Opened: **R-472** (floor vs golden, operator), **R-473** (glance +template), **R-474** (removal leaves backups, reports `volumes_removed: null` — reproduced twice), +**R-475** (Tier-2-only precondition, operator), **R-476** (page names the manifest date). **R-469** +unblocked, not lifted. Register 214 open / 174 closed. + +## 7. Teardown — three layers + +1. **Machine:** glance and uptime-kuma removed through the feature. The removal left backup directories + and applied-compose files (R-474), cleared by hand by named path; four test images removed by name, + no prune. The standing apps and bentopdf were never touched and are `Up … (healthy)`. +2. **Host:** nothing created on demo-hp's Proxmox layer — no guest, no storage. +3. **Hub:** `/hosts` lists exactly `demo-felhom-8363b5` and `demo-hp-bb76ea`; 0 customer configs; the + floor is back at `0.236.0`. Two app-deployed events from the throwaways reached the hub as ordinary events. + +## 8. Observations + +1. **The floor cannot carry a release past the vouched golden.** **FILED: R-472.** +2. **The glance template crash-loops on a fresh install.** **FILED: R-473.** +3. **App removal leaves the recovery unit and Tier-2 copy behind and names no volume.** **FILED: R-474.** +4. **The update precondition is Tier-2-only.** **FILED: R-475.** +5. **The page names a copy by its manifest date, which lags the data.** **FILED: R-476.** +6. **The controller CHANGELOG headers v0.233.0–v0.238.1 still need a MinAgent line** — these three + releases carry one. **NOT-A-FINDING: already R-470.** +7. **A live-test catalog tag is exposed to every new install while it stands.** + **NOT-A-FINDING: the alpine tag was reverted about two minutes after landing, neither demo box installed uptime-kuma in that window, and the practice is recorded in the catalog report.**