# REPORT — TASK-F: app lifecycle + HP USB write + R-55 live leg (2026-07-21) Baselines at start (verified, all clean): controller `a96226a` v0.157.0, app-catalog `857ba53`, felhom.eu `0b2c598`, agent `eba040d` v0.93.0 (source only — untouched this task). | Part | Status | |---|---| | 1 — app lifecycle states | **DONE + LIVE-PROVEN** — controller v0.158.0, plus **v0.158.1 fixing a defect I shipped** | | 2 — HP USB write | **DONE — byte-proven** | | 3 — R-55 live leg | **DONE — PROVEN LIVE** | | 4 — resolvability table | **SKIPPED by operator ruling** (no Docker Hub login) | ## Part 2 — the HP boot stick Written on felhom-pve under a format-operation evidence bar. **Two USB-shaped devices were present, and that turned out to matter:** - **`/dev/sdb` — ADATA HD710 PRO, 932G. EXCLUDED. It is the live customer data drive**, mounted at `/mnt/hdd_1` and bind-mounted into guest 9201 (`mp8`) — immich and calibre-web data. Its by-id path is `usb-ADATA_…`, but **`udevadm` reports `ID_BUS=ata` and sysfs `removable=0`**, so the two required positive checks exclude it on their own. A bare-`/dev/sdX` workflow would have offered `sdb` and `sdc` side by side with nothing but a letter between them. - **`/dev/sdc` — Samsung Flash Drive, serial `0375123010001586`, 119.5 GiB. The target.** Checks, all passed: `ID_BUS=usb`; `removable=1`; absent from `findmnt` (device and every partition); absent from `/etc/fstab`; not an LVM PV (only `/dev/sda3`); no ZFS pools exist on the host; not referenced by any guest config; 0 open handles. Destroyed content: an old `iso9660` "PVE" install image. **Operator confirmed the printed device before any byte was written.** The identity was **re-asserted at write time**, not only at survey time, in case of re-enumeration between the confirm and the write. ``` 1705338880 bytes (1.7 GB, 1.6 GiB) copied, 55.9 s, 30.5 MB/s device sha256: 2b51d74c0eaf5155fd42a137dff803b9ab8692294cedcb727c9a80658c9f7823 iso sha256: 2b51d74c0eaf5155fd42a137dff803b9ab8692294cedcb727c9a80658c9f7823 RESULT: MATCH - byte-proven ``` `dd … oflag=direct conv=fsync`, then the ISO's exact byte-length read back **from the device** and hashed — byte-proof, not exit-code-proof. The source ISO's sha256 was verified after transfer to felhom-pve and matches the published hash. The customer data drive was re-checked after the write and is still mounted at both its paths. ### Boot cheat-sheet (unchanged, restated) 1. **Boot the stick with Secure Boot ON.** This is the shim variant. Expect the Felhom boot screen, then the installer **aborting having touched no disk** — that is success. 2. **On a GRUB failure** (`relocation`, "no bootable device", a hang before the boot screen): use the **mkimage** ISO and set **Secure Boot OFF**. It is unsigned; SB-enforcing firmware refuses it. That ISO is at `180:/mnt/5_hdd/felhom.eu/felhom-iso/out/…-hp-generic-mkimage.iso` and is not on this stick. 3. **Both artifacts are inert** (match-nothing disk filter). The real install ISO does not exist yet — it needs the HP's internal SSD serial, which needs the board in hand. Send me the serial and which loader worked, and I will build it. 4. **The first boot is yours.** No remote install step was attempted. ## Part 1 — app lifecycle (summary; detail in `felhom-controller/REPORT.md`) `available` / `hidden` / `abandoned` as a `.felhom.yml` field, because the `retired/` directory move shipped hours earlier would have orphaned every customer running a withdrawn app. Deploy gate is server-side and fail-closed; unknown values fail open; orphan detection never sees the field. Live on 9201 through the real endpoints: plant-it renders **0 times** on the catalog page (control app 10), a direct deploy POST returns **409 „Ez az alkalmazás jelenleg nem telepíthető."**, the app page carries the permanent notice with no Telepítés button. **v0.158.0 shipped a defect that I caught during live validation, not from the suite:** the lifecycle predicates had pointer receivers, and html/template cannot call those on the non-addressable value the handler passes — so **every `/apps/` returned 500**, for every app, while compiling cleanly with a fully green test suite. Fixed in v0.158.1. The lesson is recorded in the controller report and the R-57 row: **a template method call is only checked when that template actually runs**, so a render test is the only coverage for template-facing methods. ## Part 3 — R-55 live leg, PROVEN immich (drive-backed) stopped through the **real UI endpoint** → `compose down` → 0 containers; calibre-web (drive-backed) and bookstack (not drive-backed) left running; `pct reboot 9201`. > `14:29:28 [gate] … live bind confirmed — recreating drive-backed app calibre-web … onto /mnt/felhom-drives/hdd_1` > `14:29:29 [gate] … 1 drive-backed app(s) left stopped — zero containers means the customer stopped them on purpose` **immich came back STOPPED.** Hours earlier the identical fixture brought it back running — same experiment, opposite result. calibre-web recreated, bookstack back, `[bootrecon] no boot-orphaned apps`, **zero alerts**, reboot → steady state in **~15 s**. The `left stopped` line fired in production for the first time, so the honoured path is observable rather than silent. ## Part 4 — skipped Operator ruled no Docker Hub login. The gate therefore still exits 2 (INCONCLUSIVE) on a full sweep rather than producing anonymous throttled accusations — which is the designed behaviour, not a failure. One `docker login` on DooPlex plus `python3 scripts/check-image-resolvable.py` closes it whenever convenient. ## Remaining - **Agent v0.93.0 is source-only** — the recovery-code wordlist fix is committed but unbuilt and unpublished; it rides the next agent train. Nothing on any host runs it yet. - **R-17** — deleting the `u629193-sub1` archive is an operator **console** action; no tooling verb targets it. Exact steps are in the previous session's report. - **HP first boot** is physically yours (above). - **The full catalog resolvability table** is still owed (Part 4). - **wanderer** was re-pinned last session to a 3-service upstream shape and has never been deployed. - **`hidden`** lifecycle state has no catalog user yet; only `abandoned` is exercised end to end. ## Observations 1. **I pushed a commit that does not build.** A `git stash`/`stash pop` used to compare `reuse_refs_check.py` output silently discarded the staged index, so the first lifecycle commit carried only the new test files. Repaired with a follow-up commit rather than a force-push — rewriting already-pushed history is the worse trade. **Never `git stash` mid-staging.** 2. **The stale-credential trap bit again, and the memory note already described it.** The controller password in `~/.config/credentials` is wrapped in **single** quotes; I stripped only double quotes, sent the quotes as part of the password, and concluded the credential was stale. It was not. Strip both quote styles. 3. **Two USB devices, one of them live customer data**, is the case the `ID_BUS=usb` + `removable=1` rules are actually for — the dangerous device carried a `usb-…` by-id name and would have looked plausible in any listing that showed model and size alone.