v0.164.0: deliberately stopped apps no longer alarm (banner + email)

A UI stop (Leallitas -> compose down -> StateStopped) is the user's own
action, not a fault, and must not raise the deadapp banner OR the
app_start_failed event. Filter at the single fix-3 derivation point:
extract scanDeployedAppRunStates's pure core to classifyRunStates and
change the down predicate to IsDownState(st.State) && st.State !=
StateStopped. Suppresses StateStopped from both the banner dead-list and
the notifier Down-set at once.

Rests on two invariants (recorded at the seam, README, CONTEXT):
 I1 StopStack = compose down => zero containers => StateStopped
 I2 P2 census: all catalog services unless-stopped => faults never rest
    at stopped (they surface as exited/degraded).
IsDownState unchanged; out-of-band 'compose stop' (containers remain ->
exited) still alerts. Tests +4 (notify 3->4, main 4->7), both red-proofs
verified. No template/funcmap/notifier/counter/copy change.
This commit is contained in:
2026-07-24 10:50:14 +02:00
parent 77956d8df2
commit c23a0f6d2d
7 changed files with 257 additions and 6 deletions
+36
View File
@@ -1,5 +1,41 @@
## Changelog
### v0.164.0 — Deliberately stopped apps no longer alarm (banner + email) (2026-07-24)
No agent coupling; MinAgent unchanged. Operator finding on 9201: stopping an app via the UI
(Leállítás) raised the global warning banner "Telepített alkalmazás nem fut: … (stopped)" on every
page — including the launcher, where the tile already shows the greyed state — and fired the
`app_start_failed` notification event on the running→down transition. A deliberate user action is not
a fault; it must not alarm the user anywhere. Genuine faults keep alerting exactly as before.
- **The fix is a one-line filter at the single fix-3 derivation point.** `scanDeployedAppRunStates`
(cmd/controller/main.go) is the only place both the banner dead-list and the notifier Down-set are
computed. Its pure core was extracted to `classifyRunStates([]stacks.Stack)` (testable without a
live Manager), and the down predicate changed from `stacks.IsDownState(st.State)` to
`stacks.IsDownState(st.State) && st.State != stacks.StateStopped`. `StateStopped` is therefore
suppressed from BOTH surfaces: no banner on any page (launcher included) and `Down=false` fed to
the notifier ⇒ no `app_start_failed` event and a clean transition tracker.
- **Why `StateStopped` ⇒ deliberate (two invariants, recorded at the seam and in CONTEXT.md):**
(I1) the UI stop path `Manager.StopStack` runs `docker compose down` → containers are removed, and
a deployed stack with zero containers aggregates to `StateStopped` (refreshStatusLocked). (I2) the
P2 restart-policy census (2026-07-21, 53 templates / 78 services) found every catalog service on
`unless-stopped`, so a crashing app never comes to rest at `stopped` — faults surface as
`restarting` / `unhealthy` / `exited` / `degraded`. **If either invariant changes, revisit this
suppression.** An out-of-band `docker compose stop` leaves containers present → `StateExited`
still alerts (out-of-band tampering is reportable — acceptable).
- **`IsDownState` deliberately UNCHANGED** — other callers (e.g. `CommittedMemory`, bootrecon) rely
on stopped counting as down. The suppression lives ONLY at the scan; no template, funcmap,
notifier, dashboard-counter, or Hungarian-copy change. The launcher tile still shows greyed +
"Leállítva"; the monitoring page and dashboard RunningCount/StoppedCount are unchanged (factual
display is not an alarm). A pre-existing banner self-clears on the next health cycle (state-based).
- **Tests +4** (notify 3→4, main 4→7): Group A — `classifyRunStates` over [running, stopped, exited,
degraded] yields dead={exited,degraded} and Down flags {false,false,true,true} (red-proof: revert
the filter → both assertions fail, verified). Group B — fault parity: exited+degraded both in the
dead list, both Down=true, raw state string carried through. Group C — stop→start→crash drives
`NotifyAppStartFailures` to exactly ONE event for the crash and zero for the stop (red-proof: mark
the stop Down=true → the zero-for-stop assertion fails, verified). Plus a skip test for
deploying/undeployed.
### v0.163.1 — Launcher polish: monogram reveal-on-failure + placeholder on every icon surface (2026-07-24)
No agent coupling; MinAgent unchanged. Two live findings from the v0.163.0 operator browser pass on 9201.