# REPORT — app lifecycle replaces the `retired/` move (TASK-F Part 1, catalog half), 2026-07-21 Baseline `857ba53`, clean tree. ## What changed and why The `retired/` directory mechanism shipped earlier the same day is **withdrawn**. It un-offers an app correctly, but the controller discovers catalog templates by directory — so removing one makes the orphan detector see the template as GONE for anyone already running the app, flagging their working install `Elavult` and offering a Törlés button. Withdrawing an app must never take a working app away from a customer. Replaced by an optional top-level `lifecycle:` in `.felhom.yml` (controller v0.158.0/.1): `available` (default, absent/empty ≡ this) / `hidden` / `abandoned`. - **`plant-it` returns to `templates/`** with `lifecycle: abandoned` — the first user of the mechanism and the case that motivated it. `retired/` is removed. Catalog: **53 apps** (52 offered). - Its compose is deliberately **unchanged**. It pins `msdeluise/plant-it:0.10.0`, a repository that does not exist (the real one is `-server`), and the app is not installable — rewriting it would imply it is. The evidence for abandonment is recorded in the template's own header comment. ## Live-proven on 9201 Through the normal git-sync and the real endpoints, not a hand-copy: | check | result | |---|---| | `lifecycle: abandoned` present in `/opt/docker/stacks/plant-it/.felhom.yml` | yes | | plant-it on the Alkalmazások page | **0 occurrences** (control `bookstack`: 10) | | direct `POST /api/stacks/plant-it/deploy` | **HTTP 409** „Ez az alkalmazás jelenleg nem telepíthető." | | `/apps/plant-it` | 200, permanent abandoned notice, no Telepítés button | ## The resolvability gate is now lifecycle-aware An abandoned app's dead image is the **expected end state**, not a finding. Left unhandled, plant-it would have made the gate permanently exit 1 — and a gate that is always red is a gate nobody reads, which is the same lesson as yesterday's throttle false-alarm. - Non-available apps are **skipped by default and REPORTED**, never silently dropped. - `--all` includes them again. - An all-skipped run is a **pass (0)**, not the "no images found" error — that would have been a false alarm of its own. 6 new fixture tests (**19 total**, no network), including the `lifecycle:` line-parse table: absent, quoted, unknown (→ available), and an indented `lifecycle:` that must NOT match because it is not a top-level key. ## Gates `check-image-pins.py` → OK, 53 templates, 0 unpinned. `test_check_image_resolvable.py` → 19/19. ## Remaining - **The full resolvability table is still owed** — Part 4 was skipped by operator ruling (no Docker Hub login on DooPlex), so the sweep would still hit the anonymous throttle and exit 2. One `docker login` + `python3 scripts/check-image-resolvable.py` closes it. - **`hidden` has no user yet.** Implemented and tested, but only `abandoned` is exercised end to end. - **wanderer is still undeployed** (from the previous session): re-pinned to a 3-service upstream shape with a second public hostname, and never once deployed. It wants a real deploy on the demo box before anyone is invited to it.