- closed: R-922 R-255 R-266 R-333 R-577 R-554 R-562 (controller 0.308.0 on demo-hp, demo-felhom, Tester 1), R-288 (map 212 KB -> 48 KB, detail per section) - R-521 narrowed to F7; R-388 design in 08 §8.1 (two questions for the operator) - privacy draft: why the registered address stays (S1); 02 module map: setup wizard deleted - STATUS + REPORT-answers-2026-10-12.md; docs.md routing line names capability-map-detail/ Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
37 KiB
Capability map — detail, section B. Apps & catalog
What this file is. The long text of every row of section B of
00-capability-map.md, moved here verbatim on 2026-10-12 (R-288, R-290 folded in;09§3 decisions 214 and 229) from the map at commit1b45bb4d. Each entry is one former table row, its cells laid out one per paragraph; the cell text itself was not edited. The map row carries the capability's ONE status word and its dated evidence pointer; this file keeps the argument, the corrections and the history. Where this text and the map row disagree on the status, the map row is current (see the map header for the words that were normalised).A new correction goes under the entry it corrects, dated — never into the map row.
B1 — Deploy an app from the catalog (env config, memory guard, health-aware progress)
Map row: B1 · former map line 105.
Scenario (as written): Deploy an app from the catalog (env config, memory guard, health-aware progress)
Components (as written): controller, catalog (~52 apps, images pinned)
Status (as written): PROVEN-LIVE
Evidence (as written): CAMPAIGN-2 T-DEPLOY-SET (7 apps, env config, health-aware); RERUN-p1p3 (×4 PASS)
Gap / roadmap (as written): Memory-guard FIRING is not live-shown (T-RES-MEMGUARD never fired: ample RAM / auth-walled) — implemented + unit-level only
B2 — App lifecycle: start/stop/restart/update/logs/remove/redeploy
Map row: B2 · former map line 106.
Scenario (as written): App lifecycle: start/stop/restart/update/logs/remove/redeploy
Components (as written): controller
Status (as written): PROVEN-LIVE — the ACTIONS work. NARROWED 2026-09-13: CAMPAIGN-3 proved remove removes the APP, not the DATA — the "delete my data" half was INERT on every box until controller v0.236.0 (R-442). RE-PROVEN 2026-09-13 on demo-hp: data written by the app itself (63 MB) gone after removal and listed; an unresolvable data location is REFUSED (409) with the app kept; an SSD app gets [] and a note.
Evidence (as written): CAMPAIGN-2 T-LIFECYCLE (stop/start/restart/update/logs); remove (app only) live in CAMPAIGN-3; remove WITH data: audits/R442-2026-09-13/; data behaviour: audits/SPIKE-app-update-2026-09-01.md (2026-09-01)
Gap / roadmap (as written): Redeploy-after-remove edge remains open (T-REMOVE-REDEPLOY never cleanly passed — stale dryrun journal); non-pilot-critical
B3 — Update is GUARDED: it refuses without a restorable backup, backs up first when the copy is stale, and HOLDS an app that does not come up — on ANY backup tier, and the release itself arrives by the managed floor
Map row: B3 · former map line 107.
Scenario (as written): Update is GUARDED: it refuses without a restorable backup, backs up first when the copy is stale, and HOLDS an app that does not come up — on ANY backup tier, and the release itself arrives by the managed floor
Components (as written): controller v0.237.0 + v0.238.0 + v0.238.1 + v0.239.0, hub v0.112.0
Status (as written): PROVEN-LIVE (2026-09-13, and again the same afternoon for any tier + floor delivery) — afternoon (audits/rulings-r472-r475-2026-09-13/): an undeclared floor above the golden refused with nothing stored (02); a declared floor 0.239.0 / MinAgent 0.129.0 served from declared and both demo boxes self-updated in 14 s and 15 s (03); nothing on any tier → backed up first, Tier 1 chosen, done (04); gokapi updated on its Tier-1 unit alone (05); a never-healthy update held naming „saját meghajtó" (07); restored from „helyi", hold cleared (08). Morning: scenarios A (real upgrade, success only after health), B (stale copy → backup first), E (pull failure → pin back, app untouched), F (never healthy → held, hold text on API and page), H (start/restart/update and the boot sweep all refuse the held app) and the restore walk (Mentések unit restore → back on the old version, hold cleared), on demo-hp with a throwaway app
Evidence (as written): audits/slice4-2026-09-13/ (live/, redproofs/, gates/); design architecture/09-update-architecture.md §6.1
Gap / roadmap (as written): Tier-2-only precondition — superseded by v0.239.0 (any tier, R-475 CLOSED); a Tier-1 route back restores only what the unit holds (R-479); the card keeps the failure sentence after a successful restore (R-480); no automatic rollback, by measurement; a release does not reach the fleet by floor between golden bakes (R-472) WIDENED 2026-09-21 (the update night) from 3 apps to 21 edges across 19 apps, and NARROWED in one place by the same run. audits/DRILL-update-night-2026-09-21.md. On scratch guest 9202 (controller v0.261.0), against a private drill catalog so the live catalog carried no test reference at any point, 21 edges across 19 apps real within-a-major upstream edges were walked through the product's own guarded Update, each app seeded and read back through its own front door (R-156) with a negative control on every readback: 14 proven, 3 failed, 4 inconclusive. What the PROVEN edges prove, precisely: the app moved, the four version observables agreed, and the data the app itself was given came back through the app's own interface afterwards. Ten of them printed a verbatim migration line. What the FAILED edges prove, and they are the more valuable half. adventurelog (a real upstream edge that migrates and then never serves), tandoor (an update that SUCCEEDED and was stopped by its own wrong health port), and the PostgreSQL engine major, which refused exactly as predicted. adventurelog v0.12.1 → v0.13.0 applied nine database migrations successfully and then never bound its port; the update held after the full health wait, the hold sentence named the tier, the date and what the copy holds, and the restore the sentence names brought the app back. That is this row's own promise, exercised on a real upstream edge rather than a staged one. AND THE NARROWING, which this row must carry because it is the same mechanism: the verifying phase trusts the .felhom.yml probe absolutely, and two of the 53 templates name a probe the app does not answer — tandoor (port 8080; it listens on 80) and zipline (/api/health; it answers 404 there, while the compose healthcheck in the same file uses /api/healthcheck and is green). For those apps a successful update is stopped by its own health wait: tandoor was measured serving HTTP 200 on the new version at four samples across five minutes, with docker's own healthcheck green, and was then stopped by failAndHold and the household sent to a restore they did not need. R-618, P1. No data was lost and the restore works — but "the update is guarded" must not be read as "the guard is right about whether the app came up". Still true and unchanged: no automatic rollback (by measurement); the route back is the restore; a multi-major jump ends held honestly. Not measured on this venue, and named rather than assumed: every event and every customer mail. Guest 9202 runs hub.enabled: false and the notifier returns before it logs (R-620), so the whole "who was told" half of 08 was structurally unobservable tonight. THE NARROWING ABOVE WAS CLOSED THE NEXT DAY, 2026-09-22 — and re-widened the row. audits/PROBE-FIX-2026-09-22.md. All three wrong probes were corrected in the catalog (app-catalog-felhom.eu@793c4fb: tandoor 8080→80, wger 80→8000, zipline /api/health→/api/healthcheck) and red-proofed live on 9202 through the product in both directions: at the live pin all three read Nem egészséges / Not healthy on their own app page while docker reported every container healthy and the front door served a real page; after the real sync all three read Fut / Running with no redeploy. tandoor's edge was then re-walked with nothing else changed and ended done at +41.1 s, seed read back, where the identical edge had ended failed at +361.9 s with the app stopped — so the tally is now 15 proven, 2 failed, 4 inconclusive, and all fifteen are on the live catalog. A --fast catalog gate (check-probe-matches-compose.py) now refuses a probe that does not match the same service's own compose healthcheck, with four red-proofs and ten decoys including the no-PyYAML mode CI actually runs. WHAT THIS ROW STILL CANNOT CLAIM, and the reason is exactly R-96 rule 3: the guard is now shown correct for 47 of 53 templates. paperless-ngx's probe has never run on any box — no container name matches its stack name, so it is silently skipped and its badge can never go red (R-630); and five more cannot be judged statically, one of which (home-assistant) is right only because its check type cannot fail (R-631). An absent alarm is equally consistent with healthy and with never checked. And the sweep's ceiling, counted: 28 of the 53 templates have never been deployed by any drill (R-632). THAT CEILING WAS REMOVED THE SAME NIGHT, 2026-09-22 — all 28 walked (audits/DRILL-the-28-2026-09-22.md), so every template in the catalog has now been attempted at least once. 26 of 28 deployed, 6 proven, 5 inconclusive, 14 with no within-a-major edge upstream, 1 failed honestly and 2 undeployable — one of those (plant-it) by design, refused by the product's lifecycle gate, proven live for the first time. Each app also got the half the update night skipped: a restore from its own copy, with the seed read back again — 21 restored, and 2 were correctly REFUSED with the sentence 07 §6.2 predicts for a class-A app whose local copy holds no file leg. AND THE NIGHT NARROWED THIS ROW AGAIN, in the place the probe work could not reach. paperless-ngx has no container matching its stack name, so no probe is ever built for it — and verifying does not skip: it waits out the full update.health_timeout and HOLDS, stopping an app whose three containers all read healthy. The controller's own words: not healthy within 5m0s (last: no probe container) — stopping and HOLDING the app, at +313.0 s. R-630, raised to P1. So "the update is guarded" is now shown correct for 47 of 53 templates, wrong for none, and actively harmful for the one template that has no probe at all. Two further limits on what this row may claim, both about STATE rather than health: a remove sent while a restore is still running reports success and leaves a container restarting with a live public route (R-633) — while the product already refuses exactly that clash for update and for restore, naming the blocking operation; and an app can be running, healthy and serving while recorded as deployed: false, in which state the product refuses to remove it at all (R-634). In both, a person needed a shell to clear what the product could not. ALL THREE ARE FIXED IN CONTROLLER v0.262.0 (2026-09-22), and the first is PROVEN LIVE. The stopped app: verifying no longer loops on a probe that resolves to nothing — it settles on container state, the way an app declaring no check is judged, and says which it did. Measured on paperless-ngx: the identical Update that ended failed at +313.0 s with the app stopped now ends done at +53.4 s, with no no probe container warning in the log because the explicit healthcheck.container resolved the target. The ghost: RemoveStack consults the backup side's Busy guard — which the product already applied to update and to restore — and then WATCHES the compose project for 25 s after down, removing anything that carries its label and answering verified: true/false, because down returning 0 is a request rather than a result. The unremovable app: the refusal now asks whether anything EXISTS (containers, a compose file, an app.yaml) instead of reading a flag. WHAT THIS ROW STILL MAY NOT CLAIM: R-634's MECHANISM — why deployed goes false while containers run — is not diagnosed; only the consequence is fixed. And a probe can be right about the port and still wrong about what a 200 means: romm answered 200 from nginx for six hours while its workers were OOM-killed behind it (R-635). "The update is guarded" has never meant "the new version runs".
B4 — A failed update is UNDONE by the box itself — the previous version back with its data from seconds before the update; the app is held only if that undo fails too
Map row: B4 · former map line 108.
Scenario (as written): A failed update is UNDONE by the box itself — the previous version back with its data from seconds before the update; the app is held only if that undo fails too
Components (as written): controller v0.263.2 (the undo), v0.264.0 + hub v0.120.0 (the mail)
Status (as written): PROVEN-LIVE (2026-09-23)
Evidence (as written): audits/undo-live-2026-09-23/README.md (+ audits/undo-bakeoff-2026-09-23/ for the method). On scratch guest 9202, through the endpoints the UI invokes: docmost (PostgreSQL), romm (MariaDB) and vikunja (SQLite in a volume), each a real migrating edge failing a deliberately wrong probe, undone in 30–52 s with seeds written before the backup, after it and seconds before the press ALL read back through each app's front door, ledgers equal to before; page line in hu and en. Cut-off copy → HOLD (untouched), prefix in the box's language; power cut (pct stop) during undoing → resumed after boot and undone; a person's press after an undo → done; removal deletes kept copies. NARROWED 2026-09-23 night (audits/DRILL-night-2026-09-23.md Part D): a controller kill -9 in verifying → resumed after restart → undone, seeds read back (round 9); a 1 GB memory hog and a whole-box backup during verifying did not disturb it (rounds 6, 8). Two holes found: after ANY restore the app's volumes lose their compose label and the undo copies NOTHING (R-658, P1); and a held FILE-LEG app is told to restore from a copy the restore then refuses, with no other route on a box without an off-site tier (R-659, P1).
Gap / roadmap (as written): Folder copy of NAMED volumes only (09 §3 decision 19); bind folders never touched. The household is told (2026-09-23, audits/undo-fleet-2026-09-23/): on guest 9201 the household and the operator each received ONE mail per app per outcome — undone and held, in Hungarian (vikunja) and, after one language switch, in English (glance) — with the app named in the subject; notification_log rows 937–946 all sent. Not built: the automatic caller (part 7) — this protects the manual button today. Leftovers: R-647.
B5 — The catalog holds only TESTED steps — an image move without a proven test record (bench + box, digests the registry still serves) is refused at push time
Map row: B5 · former map line 109.
Scenario (as written): The catalog holds only TESTED steps — an image move without a proven test record (bench + box, digests the registry still serves) is refused at push time
Components (as written): catalog 6db08a5 (scripts/check-test-record*.py, ladder.py, upgrade-test.py --write-ladder)
Status (as written): PROVEN (2026-09-23 night)
Evidence and gap (as written — this row's text carries a | outside code, so its last cells are kept together as they stood): audits/DRILL-night-2026-09-23.md Part B/C | 16 decoy cases both ways + 3 red-proofs; the gate judged every one of the night's 12 published steps against the live registry and refused a wrong-digest control. 21 earlier moves backfilled from their records. | Closed 2026-09-24 by the row below (v0.268.0 reads the ladder).
B6 — A box behind climbs ONE tested step per press, each with its own definition (09 §3 decision 14)
Map row: B6 · former map line 110.
Scenario (as written): A box behind climbs ONE tested step per press, each with its own definition (09 §3 decision 14)
Components (as written): controller v0.268.0 (stacks/ladder.go) + catalog 5ed599c (steps/<StepKey>.yml, gate rule 4)
Status (as written): PROVEN-LIVE (2026-09-24)
Evidence and gap (as written — this row's text carries a | outside code, so its last cells are kept together as they stood): audits/ladder-2026-09-24/partD/10-romm-two-steps.json | romm 5.3.0/11.4 → 5.3.1/11.4 → 5.3.1/11.8 in two presses on 9202, seeded account read back after each; page count 2 → 1 → none; spike confirmed v0.267.0 jumped (partD/00-spike.json). 6 unit tests + red-proofs. | Nothing climbs by itself yet (part 7). A step has no .felhom.yml of its own (R-664).
B7 — A file app comes back WHOLE from the second drive — its files by four rules (never delete, never overwrite a newer file, bring back what is missing, keep an older replaced file beside it), then its settings and database (09 §3 decision 26)
Map row: B7 · former map line 111.
Scenario (as written): A file app comes back WHOLE from the second drive — its files by four rules (never delete, never overwrite a newer file, bring back what is missing, keep an older replaced file beside it), then its settings and database (09 §3 decision 26)
Components (as written): controller v0.269.0 (backup/tier2_whole.go)
Status (as written): PROVEN-LIVE (2026-09-24)
Evidence (as written): audits/night-2026-09-24/A1/21-readback.json, A1/30-held-then-whole.*, E/round-03.json
Gap / roadmap (as written): nextcloud on 9202: 1 file brought back, the household's newer edit kept, 137 unchanged, 3/3 volumes + 1/1 database in 38 s; again from a hold that named the second drive, and again after a power cut mid-update (round 3). R-538's refusal of the unit-only restore stays.
B8 — A crash-looping or out-of-memory app is STOPPED by the box, the household and operator are told, and Start gives one more try (decision 28)
Map row: B8 · former map line 112.
Scenario (as written): A crash-looping or out-of-memory app is STOPPED by the box, the household and operator are told, and Start gives one more try (decision 28)
Components (as written): controller v0.269.0, hub v0.123.0
Status (as written): PROVEN-LIVE (2026-09-24)
Evidence (as written): audits/night-2026-09-24/A3/21-gokapi-page-event.txt, A3/30-start-then-trip2.*, E/round-01/07/08.json
Gap / roadmap (as written): gokapi stopped at 6 restarts in 10 min; Start lifted it, 9 fast restarts, stopped again with the support-informed sentence (hu + en); chaos hour: OOM storm stopped after a power cut (+185 s), a crash loop under a backup run (+116 s), a storm on a nearly full disk (+102 s).
B9 — A held app whose page says support is informed can only be removed KEEPING its data (decision 27)
Map row: B9 · former map line 113.
Scenario (as written): A held app whose page says support is informed can only be removed KEEPING its data (decision 27)
Components (as written): controller v0.269.0
Status (as written): PROVEN-LIVE (2026-09-24)
Evidence (as written): audits/night-2026-09-24/A2/10-held-no-copy-keep-data.*
Gap / roadmap (as written): the dialog reads keep_data_only; Remove with data or backups → 409 in both languages; the app stayed installed.
B10 — The box runs the exact TESTED image of a floating tag, and an installed app keeps its image until a guarded Update moves it (09 §6.4 part 6, box half)
Map row: B10 · former map line 114.
Scenario (as written): The box runs the exact TESTED image of a floating tag, and an installed app keeps its image until a guarded Update moves it (09 §6.4 part 6, box half)
Components (as written): controller v0.269.1 (stacks/digest.go)
Status (as written): PROVEN-LIVE (2026-09-24)
Evidence (as written): audits/night-2026-09-24/B/21-floating-tag-0.269.1.*, B/01-compose-accepts-digests.txt
Gap / roadmap (as written): redis:7-alpine: a newer tested digest → the „Frissítés elérhető" badge, the sync left the running file alone (v0.269.0's sync did not — fixed), Update pulled exactly that digest. Compose accepts tag@digest for all 25 ladder apps (37 digests).
B11 — Automatic updates at night — the update leg (09 §6.4 part 7)
Map row: B11 · former map line 115.
Scenario (as written): Automatic updates at night — the update leg (09 §6.4 part 7)
Components (as written): controller v0.271.0
Status (as written): PROVEN-LIVE on scratch 9202 (six simulated nights, 2026-09-24/25) — the demo boxes' first real night: see audits/DRILL-night-2026-09-25.md Part D
Evidence (as written): audits/night-2026-09-25/C/, B/redproofs/
Gap / roadmap (as written): after the off-site leg on every path; one step per app per night; needs_person never, files_may_change only with a whole copy; a failed step not re-pressed until the catalog re-tests it (R-680); a power cut mid-step resumed and finished; a controller kill mid-step put back; switch off = nothing pressed; data read back after every night. Not claimed: W+5h with steps left and a FAILING off-site leg live (unit only), the full-system gate waiting live (9202 has no agent), resume of the leg after a restart (R-686).
B12 — What restart and update do to a deployed app whose compose file the catalog already moved
Map row: B12 · former map line 116.
Scenario (as written): What restart and update do to a deployed app whose compose file the catalog already moved
Components (as written): controller v0.235.0
Status (as written): CHANGED 2026-09-06 — they NO LONGER upgrade it. The row below records what shipped; this text records what it replaced, because every box under v0.235.0 still behaves the old way. Up to v0.234.0: PROVEN-LIVE (2026-09-01) — they UPGRADE it. Every lifecycle action ends in docker compose up -d, which makes the container match the file and PULLS the image itself when it is missing (measured: 18.3 s with a pull, 0.5 s without; negative control with an unchanged file did not even recreate the container). This is DELIBERATE on the restart path — Manager.RestartStack says so in a comment — but the syncer moves the file under a deployed app on a 15-minute cycle with no deployed check (R-438), and NOTHING tells the customer.
Evidence (as written): audits/SPIKE-app-update-2026-09-01.md §2, §3
Gap / roadmap (as written): No safety copy is taken by any of them — writeSafetyDump is DATABASE-ONLY and is not on the update path at all. R-438, R-440, R-443.
B13 — Whether the box UPGRADES an app by itself, with nobody pressing anything
Map row: B13 · former map line 117.
Scenario (as written): Whether the box UPGRADES an app by itself, with nobody pressing anything
Components (as written): controller
Status (as written): PROVEN-LIVE (2026-09-01) — YES, but only when an app fails to come back. A plain power cut does NOT upgrade: Docker's restart: unless-stopped restores the old containers and the reconciler logs no boot-orphaned apps (nothing to start). When an app does NOT return, Reconciler.Run (bootrecon.go:269) calls StartStack -> compose up -d and the app comes back on the NEW version, unattended (measured). 13 non-API call sites across 9 files reach up -d this way — not the five previously believed.
Evidence (as written): audits/SPIKE-app-update-2026-09-01.md §2, §8
Gap / roadmap (as written): The drive-return gate (intermediary.go:222) and AppStopGuard.Recover (appstop_marker.go:283) call the same function; located by reading, not exercised live — stated as such.
B14 — Whether an app UPGRADE can be undone
Map row: B14 · former map line 118.
Scenario (as written): Whether an app UPGRADE can be undone
Components (as written): controller + catalog
Status (as written): PROVEN-LIVE (2026-09-01) — NO, and "rollback" is the wrong word for it. Once a migration has RUN, putting the old image tag back yields a container that refuses to start: Nextcloud — "the version of the data (32.0.9.2) is higher than the docker image version (31.0.14.1) and downgrading is not supported". A 3-major jump is refused outright ("only possible to upgrade one major version at a time") and IS recoverable, precisely because nothing migrated. Positive control: the data is not destroyed — returning to 32.0.9 restored both seeded markers byte-identical.
Evidence (as written): audits/SPIKE-app-update-2026-09-01.md §7
Gap / roadmap (as written): The only route back is restoring DATA from a copy taken BEFORE the update — which no update path takes. And a restore's image-level rollback is itself overwritten by the syncer within 15 minutes (R-441). R-40 is confirmed live by the same measurement.
B15 — Protected infra stacks can't be stopped/removed from UI
Map row: B15 · former map line 119.
Scenario (as written): Protected infra stacks can't be stopped/removed from UI
Components (as written): controller
Status (as written): PROVEN-LIVE
Evidence (as written): CAMPAIGN-nomercy + RERUN-p1p3 T-SEC-PROTECTED (refuse stop/remove, stay Up)
Gap / roadmap (as written): (Cited CAMPAIGN-2 T-SEC-PROTECTED was a stale-dryrun FAIL — corrected to the runs with a real server-side refusal)
B16 — What VERSION a box is running, and whether it is behind the catalog
Map row: B16 · former map line 120.
Scenario (as written): What VERSION a box is running, and whether it is behind the catalog
Components (as written): controller v0.234.0 + catalog 69761cf
Status (as written): PROVEN-LIVE — both the record and the rendered badge.
Evidence (as written): tests/VALIDATION-update-slice12-2026-09-02.md — on demo-hp 0.233.0, through a REAL production caller (bootrecon → StartStack → compose up -d → recordInstalledImages, no hand-set state): bentopdf recorded 1 service and bookstack recorded 2, keyed by compose SERVICE name, and all three digests match the ground truth read independently from the containers before anything was touched. catalog_since reached the box on the normal 15-minute sync. bookstack's two encrypted secrets are byte-identical across the write. Badge evidence: same file §4. v0.234.0 startup backfill, PROVEN LIVE 2026-09-03 on demo-hp: the record was stripped from privatebin (1 service) and romm (3 services) to recreate the pre-0.233.0 shape, the controller restarted, and the backfill re-seeded exactly those two — every digest matching the ground truth read from the containers beforehand — while logging 2 app(s) recorded, 7 already had a record, 0 left unrecorded. All 9 deployed apps then carried „Naprakész" on /stacks (ASCII fragments with a negative control at 0). On demo-felhom the operator's own case, OpenGist, now renders the badge. Why the backfill exists at all: without it the label never reached an app that simply runs, which the operator found the morning after v0.233.0. Unit side: installed_test.go + updatebadge_test.go, incl. a wiring test through a real RestartStack, an AST walk of all four call sites, and three companion red-proofs.
Gap / roadmap (as written): THE UNEXERCISED LEG, NAMED: one badge STATE of four. „Frissítés elérhető" WITHOUT an age needs an app whose catalog_since is absent, malformed or future-dated, and all 53 now carry a valid one — unit-tested only (TestGroupF). The other three are live: „Naprakész" ×2 on /stacks and on /apps/bookstack; NO badge at all on /apps/docmost, a deployed app with no record — absent is UNKNOWN and is not rendered as current; and „Frissítés elérhető — 52 napja" on both surfaces, the age being real arithmetic on bentopdf's catalog_since 2026-07-12. The behind state was staged by editing bentopdf's compose tag ONLY — no container restarted, no up -d — then reverted byte-identically (sha256 equal, diff empty); so the RENDER is measured and the syncer's own half stays measured separately in the spike. Searched with grep -oF ASCII fragments plus positive AND negative controls. The Frissítés/Újraindítás/Leállítás buttons are unchanged on the live page in the behind state. Absent means UNKNOWN, never current — a legacy app.yaml renders NOTHING, red-proved. No version number is shown to the customer and no registry is queried, so „Naprakész" CAN BE FALSE for the 23 floating pins (R-446). Nothing about updating changed: R-438, R-440, R-441, R-443 all stand. Reasoning: architecture/09-update-architecture.md; remaining slices R-447..R-452
B17 — An app's VERSION is frozen to what the customer has; only a deliberate Update moves it, while template CORRECTIONS still arrive
Map row: B17 · former map line 121.
Scenario (as written): An app's VERSION is frozen to what the customer has; only a deliberate Update moves it, while template CORRECTIONS still arrive
Components (as written): controller v0.235.0
Status (as written): PROVEN-LIVE (2026-09-06) — by two REAL catalog pushes travelling the REAL 15-minute cycle, not a hand-edited file
Evidence (as written): tests/VALIDATION-update-slice3-2026-09-06.md — a non-image catalog change REACHED the pinned app (08:01:51Z) with the container untouched; an image change did NOT (08:20:29Z); and the restart afterwards took 0.1 s, did not recreate the container, and never pulled the new image, against the spike's 18.3 s with a pull for the identical sequence before. The Update button still moved the version (pin advanced 17 s BEFORE the pull completed) and the teardown update returned the container to the baseline digest byte for byte. The freeze holds in BOTH directions. Unit side: internal/sync/render_test.go (the whole render table, incl. self-healing in BOTH branches), internal/stacks/pin_test.go (adoption never guesses; the update advances the pin BEFORE the pull), TestGroupG (the badge reads the catalog, not the frozen file), TestGroupH (an AST walk of cmd/controller/main.go asserting the seam, adoption, and their ORDER against syncer.Start()). Three companion red-proofs, each run, failing, and reverted.
Gap / roadmap (as written): Operator ruling 2026-09-06, Option 1 (architecture/09-update-architecture.md §3.4). Nothing was added to the thirteen compose up -d call sites — most are repairs, and a repair that refuses to repair leaves an app down; they were made safe by removing the reason. pinned_images is INTENT, installed_images is an OBSERVATION — never fed from each other (R-166, one field over). Known limitations, all recorded rather than fixed: a frozen app is frozen WHOLE (§8.4); .felhom.yml keeps flowing, so a frozen app can get a probe for a newer version — false alarm, never data loss (R-458); and the Update button is still unguarded (R-448 is slice 4). Closes R-447, R-441, R-438
B18 — Catalog sync (git, 15 min) + orphan lifecycle + validation choke point (bad backup: block degrades to legacy, loudly)
Map row: B18 · former map line 122.
Scenario (as written): Catalog sync (git, 15 min) + orphan lifecycle + validation choke point (bad backup: block degrades to legacy, loudly)
Components (as written): controller v0.132, catalog
Status (as written): PROVEN-LIVE
Evidence (as written): CAMPAIGN-2 T-SYNC-IDEMPOTENT; v0.132 LoadMetadata red-proofs
Gap / roadmap (as written):
B19 — Lemez-egészség felügyelet: per-disk SMART kártya („Lemezek állapota") + degradáció-riasztás (Rendben/Figyelmeztetés/Hiba/Nincs adat)
Map row: B19 · former map line 123.
Scenario (as written): Lemez-egészség felügyelet: per-disk SMART kártya („Lemezek állapota") + degradáció-riasztás (Rendben/Figyelmeztetés/Hiba/Nincs adat)
Components (as written): agent v0.94.0→v0.95.0, controller v0.169.0→v0.171.0→v0.215.0, hub v0.73.1
Status (as written): PROVEN-LIVE (healthy path + delivery + the severity wire). IMPLEMENTED, NOT proven-live: the Hiba-from-counters path (v0.215.0) — it has never fired on real hardware, only against the committed fixture's values in unit tests (R-332)
Evidence (as written): 2026-07-25 (v0.95.0 + v0.171.0 — the SMART-coverage fix): the card on guest 9201 now shows BOTH real disks with real verdicts + human model labels — „AirDisk 512GB SSD" → Rendben (34°C) (the system SSD, via LVM/dm resolution) and „TOSHIBA MQ04ABF100" → Rendben (30°C) (the USB, via union-path SMART). /disks carries smart.health=PASSED + model_name for both. This reverses the 2026-07-24 „Nincs adat on a raw UUID" state (SPIKE-smart-coverage-2026-07-25.md had proven both disks answer smartctl -a -j PASSED but the agent never asked). Prior: verdict table (+≥90 red-proof); check first-run/degradation/recovery/UNKNOWN tests; hub allowlist test. Notification pipeline PROVEN-LIVE 2026-07-24 — a disk_health_degraded POST (the exact notify.PushEvent wire call) was 400-rejected by hub v0.73.0 and 200-accepted + „Operator email sent" by hub v0.73.1
Gap / roadmap (as written): No new smartctl load; feature-detect by payload presence → MinAgent floor unchanged; no sudoers/-d sat change. No global banner (deliberate). Agent v0.95.0 fixes: union-path SMART (Fix B) + LVM/dm whole-disk resolution incl. the builtin local on the LVM root (Fix A, SMART-only — never touches backing/durable_id) + model_name capture. 2026-08-14 — a genuinely failing disk HAS now been seen, and it broke three assumptions (audits/DIAG-smart-passed-trap-2026-08-14.md + two committed fixtures: raw smartctl -a -j and 406 smartd lines from ST3000VX010 S/N Z6A07P2G). (1) smart_status.passed is STRUCTURALLY incapable of failing on unreadable sectors — attrs 187/197/198 all carry thresh: 0 and a normalized value floors at 1 — so the drive read PASSED at 352 pending sectors and 1001 uncorrectable reads. (2) The alert it did produce carried severity "warn", which the hub coerces to info and never emails: the counterfactual is ZERO emails about this drive (R-328, fixed controller v0.215.0, and the warning-vs-warn pair proven side by side in notification_log on 2026-08-14 — sent vs no row at all). (3) The old check spoke once and forgot on restart, so between 8 and 352 sectors it emitted nothing. v0.215.0 adds the sustained/count/heat Hiba rules, persisted state and an hourly cadence. The verdict half of that arm remains unit+red-proof covered only — no live drive has reached Hiba from counters (R-332). SMART history/trending (hub-side) PARKED (ROADMAP R-73)
B20 — App crashes → customer notified (one event per transition, no flapping spam)
Map row: B20 · former map line 124.
Scenario (as written): App crashes → customer notified (one event per transition, no flapping spam)
Components (as written): controller v0.120, hub v0.48
Status (as written): IMPLEMENTED
Evidence (as written): controller v0.120.0 (dead-app alerting, app_start_failed, one-event-per-transition red-proofs); CAMPAIGN-3 F11 surfaced the gap
Gap / roadmap (as written): End-to-end crash→customer-email delivery never live-confirmed (6B deferred / 6C inconclusive: clean stop ≠ crash); anti-spam unit-proven
B21 — Post-deploy optional config (API keys etc.) with restart
Map row: B21 · former map line 125.
Scenario (as written): Post-deploy optional config (API keys etc.) with restart
Components (as written): controller, catalog .felhom.yml
Status (as written): IMPLEMENTED
Evidence (as written): feature long-standing; config page renders (CAMPAIGN-2 T-PAGE-ALL is GET-only)
Gap / roadmap (as written): The config-save+restart flow is exercised in no campaign (CAMPAIGN-3 explicitly skipped interactive app config). Demoted: T-PAGE-ALL is a page-render smoke test, not this flow
B22 — Backup classification: 13 bind-bearing apps carry mandatory/optional/excluded classes
Map row: B22 · former map line 126.
Scenario (as written): Backup classification: 13 bind-bearing apps carry mandatory/optional/excluded classes
Components (as written): catalog, controller v0.132–133
Status (as written): PROVEN-LIVE
Evidence (as written): SPIKE-backup-classification-2026-07-14, CAMPAIGN-6D/6E
Gap / roadmap (as written): Remaining ~39 apps are legacy-class by design (unit-only offsite)