## Changelog ### v0.156.0 — a dead primary alerts (R-51); a boot orphan restarts itself (R-52) (2026-07-21) **No new agent coupling — MinAgent stays 0.90.0.** Two independent failures from the same live audit, both unattended-resilience holes: the box was broken and nobody was told, then the box could have fixed itself and did not. **R-51 — a multi-container app whose MAIN container is dead now counts as down.** On 2026-07-20 `immich-server` sat `Exited` for **18 hours** with the app 100 % unreachable, and the box produced no dead-app banner and no `app_start_failed` event — while single-container Calibre-Web, down for the same reason, alerted in 90 seconds (AUDIT-vacation-remote-ops-2026-07-20 F4). The defect was one branch in `aggregateState`: a stack with *some* members running and *some* stopped returned `StateRunning` — "partial" — and `IsDownState` (correctly) does not treat running as down. So the alarm never had anything to fire on. *(The ROADMAP row's diagnosis — "aggregation classifies such a stack `unhealthy`" — is wrong at the source; corrected in the row.)* - New `StateDegraded`. The mixed branch now asks each DOWN member for its restart policy: a member docker is supposed to keep running (`always` / `unless-stopped`) makes the stack **degraded**, a finished one-shot (`no` / `on-failure`) leaves it running. `IsDownState` gains `degraded` and **nothing else** — the `unhealthy` / `restarting` / `paused` / `unknown` exclusions are byte- identical, because folding `unhealthy` into down is what fix-3 removed the flapping by not doing. - An **unreadable** policy counts as supervised (fail-CLOSED), the opposite of the IsDownState fail-open rule and for a different reason: there the *state* is ambiguous, here a member is known dead and only the excuse is missing. The P2 census backs it — all 53 catalog templates / 78 services are `unless-stopped`, and zero one-shot containers exist today. - The policy read is one `docker inspect` per down member of a *mixed* stack, cached per container+state and pruned to the live container set, so the 10 s refresh does not grow a docker call per container. - Everything that asks "are there live containers here" learns the state too: quiesce (`RunningAppStacks`), delete's stop-first guard, the export stop-first guard, telemetry, health probes. Everything that asks "is this app working" counts it as down: the dashboard counter, the stopped filter, the dead-app banner and the alarm. UI: „Részlegesen leállt", warn colour, and the URL is flagged unpublished (Traefik 404s when the routed member is the dead one). **R-52 — an app the boot left behind now gets exactly one recovery.** The same shutdown left immich and calibre-web `Exited` while ten sibling containers came back; the controller *reported* them for 18 hours and never started them (F5). - New `internal/bootrecon`: one bounded sweep at startup — at most 2 attempts, 30 s apart, then it stops and the alarm owns the problem. **Never a restart loop.** - **A deliberate Stop survives a reboot.** The UI's Stop is `compose down`, which REMOVES the containers; an interrupted boot leaves them behind as `Exited`. So the boot-orphan signature is "deployed, has containers, and they are down", and a zero-container stack is never touched. - The whole sweep (5 s settle + one 30 s gap) fits inside the 90 s `deadAppBootGrace`, so a successful recovery never alerts and a failed one alerts honestly. A test asserts that arithmetic rather than leaving it to a comment. **Seam discipline (the reason both features have a wiring test).** Two inert-seam defects shipped in the two days before this: controller v0.154.0 and agent v0.91.0, both a correct component with green tests and no production caller. So the boot sweep is asserted from `package main` — including an AST walk proving `func main()` actually contains the `go runBootReconcile(...)`. That test was written first as a `strings.Contains` and **its own red-proof passed it**, because a commented-out call still contains the string. Comments are not callers; the AST version fails as it should. Red-proofs (all run, all failed on the pre-fix shape, all restored): the mix branch reverted to `return StateRunning` → the immich fixture and both production-path tests fail with `"running"`; the boot hook commented out → the wiring test fails; the zero-container gate dropped → the user-stopped app is started, which is the one thing R-52 must never do. ### v0.155.0 — the restore wizard read the wrong "is something running" flag (2026-07-21) **No new agent coupling — MinAgent stays 0.90.0.** Fixes a defect shipped in v0.154.0 and found by the operator on the first live click-through, plus the dead phase-strip label from the same release. **The bug.** `backup.Manager` carries two different booleans and v0.154.0 read the wrong one: | flag | read by | set by | covers the verification restore? | |---|---|---|---| | `running` | `IsRunning()` | `acquireRunning()`, **inside** the goroutine | **no — `RestoreOffboxScratch` never acquires it at all** | | `opRunning` | `RestoreStatus()` | `BeginRestoreOp()`, in the handler, synchronously | yes, all four offsite actions | The wizard sourced `OpRunning` from `IsRunning()`. For „Ellenőrzés" and the full-restore preparation — the wizard's two most-used actions, and the long ones, since they stream from restic — that flag is false for the *entire* operation. So the execution step was unreachable: the page kept offering all three intents with live buttons while a restore was downloading, and the progress banner (which polls the op status) contradicted the phase strip on the same screen. Any button pressed there would have been refused by the handler — which is exactly the "offering a control guaranteed to fail" dishonesty R-48 exists to remove. **The fix** is one line of behaviour behind a named seam: `restoreOpInFlight(st)` takes the `RestoreOpStatus` the handler already reads once, and its doc comment states which flag is which and why. The handler now takes a single `RestoreStatus()` read, so the strip, the suppression decision and the running-op name can no longer disagree with each other. **Why the v0.154.0 tests missed it.** The Scenario-E table proved `deriveWizardStep` behaves correctly *given* `OpRunning=true`; nothing proved the handler ever computes `true`. Hollow at exactly that seam. `TestRestoreOpInFlight_UsesDisplayFlagNotConcurrencyFlag` now drives a real `Manager` through `BeginRestoreOp` and asserts the wizard suppresses every form — red-proofed against the v0.154.0 shape. **„Eredmény" is now reachable.** The fourth phase label never lit up in v0.154.0. The strip's highlight is now its own derived value (`Phase`), separate from `Step`: a finished restore is back on the intent step — everything is available again — while the strip rightly reads „Eredmény" and an outcome card shows the result. Bounded by `restoreResultWindow` (10 min) so a stale result cannot claim to be fresh, and bound to the app, so a finished bookstack restore does not light up immich's page with bookstack's message. The card survives a reload, which the redirect flash does not. ### v0.154.0 — one restore entry per app, and the intent is a described choice (2026-07-21) Closes **R-48**. **No new agent coupling — MinAgent stays 0.90.0.** This is a UI-layer change: `internal/backup`, `internal/appbackup` and `internal/selfupdate` are untouched, and the release adds **no mutation endpoint** — every action still posts to the `/backup/offbox/*` handler it always did, with the same field names and the same gates. **The defect.** The „Ellenőrző visszaállítás a távoli tárolóból" list rendered up to five inline `