6.8 KiB
REPORT — v0.164.0: deliberately stopped apps no longer alarm (banner + email)
Summary
Stopping an app from the UI (Leállítás) previously raised the global warning banner
"Telepített alkalmazás nem fut: … (stopped)" on every page (launcher included) and fired the
app_start_failed hub event on the running→down transition. A deliberate user action is not a fault.
v0.164.0 suppresses StateStopped from both the banner dead-list and the notifier Down-set at a
single derivation point, while every genuine fault (exited/degraded) keeps alerting byte-identically.
Baselines
| Item | Value |
|---|---|
| Repo | felhom-controller |
main before |
77956d8 (v0.163.1) — clean tree, HEAD == origin/main verified |
main after (code) |
c23a0f6 |
| Target version | v0.164.0 — built + deployed to guest 9201, healthy |
| Agent coupling | none; MinAgent unchanged |
Files modified
controller/cmd/controller/main.go— extractedscanDeployedAppRunStates's pure core toclassifyRunStates([]stacks.Stack) ([]web.DeadApp, []notify.AppRunState); changed the down predicate tostacks.IsDownState(st.State) && st.State != stacks.StateStopped; documented invariants I1/I2 at the seam.controller/cmd/controller/classify_runstates_test.go— new; Groups A/B + skip test.controller/internal/notify/deadapp_test.go— added Group C (stop→start→crash sequence).controller/README.md— new "Deliberate stops are silent (v0.164.0)" paragraph + fix-3 wording fix.REUSE.md— newclassifyRunStatesseam row.CHANGELOG.md(v0.164.0 entry on top),CONTEXT.md(ruling with I1+I2).
Commit: c23a0f6 (code + tests + docs). REPORT committed separately (post-validation).
The rule and its invariants (recorded at the seam, README, CONTEXT, CHANGELOG)
StateStopped ⇒ deliberate, because:
- I1 — the UI stop path
Manager.StopStackrunsdocker compose down→ containers are removed, and a deployed stack with zero containers aggregates toStateStopped(refreshStatusLocked). Proven live: after the stop,docker ps -ashowed no calibre-web container. - I2 — the P2 restart-policy census (2026-07-21, 53 templates / 78 services) found every catalog
service on
unless-stopped, so a crash never rests atstopped— faults surface asexited/degraded/restarting/unhealthy.
If either invariant changes, revisit the suppression. IsDownState left unchanged (other callers rely
on stopped counting as down). Out-of-band docker compose stop (containers remain → exited) still
alerts — acceptable. The stopped_by_user intent flag was considered and parked.
Tests — results + red-proofs (count 3→4 notify, 4→7 main; +4 total)
| Test | Result |
|---|---|
TestClassifyRunStates_StoppedIsSuppressed (Group A) |
PASS — dead={immich(exited),nextcloud(degraded)}, Down flags {false,false,true,true} |
TestClassifyRunStates_FaultParity (Group B) |
PASS — both faults in dead list, both Down=true, raw state string carried |
TestClassifyRunStates_SkipsDeployingAndUndeployed |
PASS |
TestNotifyAppStartFailures_StopStartCrashSequence (Group C) |
PASS — exactly one event for the crash, zero for the stop |
Full suite go build/vet/test ./... |
PASS (all packages green) |
Red-proofs (mechanically executed, then reverted):
- Group A red-proof — reverted the filter to bare
stacks.IsDownState(st.State):TestClassifyRunStates_StoppedIsSuppressedFAILED ("dead list must be exactly …, got […cwa/stopped…]"). Restored → PASS. - Group C red-proof — flipped the stop cycle to
Down:true:TestNotifyAppStartFailures_StopStartCrashSequenceFAILED ("a deliberate stop must fire no event, got 1"). Restored → PASS.
Deployment
- Built
0.164.0on DooPlex (build.sh 0.164.0 --push), pushed togitea.dooplex.hu/admin/felhom-controller:0.164.0. - Deployed to guest 9201 (bootstrap: pull →
/etc/felhom-controller-image→ restart bootstrap service). docker ps:gitea.dooplex.hu/admin/felhom-controller:0.164.0 Up (healthy).
Live validation (guest 9201, customer demo-felhom; endpoint-level — no browser)
Method: authed session to the in-guest controller (container IP 172.17.0.2:8080, Host: felhom.demo-felhom.eu,
session cookie + X-CSRF-Token), driving the exact UI endpoints; banners read from rendered HTML
(ASCII substring nem fut); event surface read from the hub SQLite events table (the true email
trigger). Event watermark before: max id 1753.
- Deliberate stop is silent (Scenario A).
POST /api/stacks/calibre-web/stop→{"ok":true};docker ps -a→ calibre-web container gone (I1 confirmed). After one health cycle:- Banner on
/: none. Banner on/launcher: none. - Launcher: calibre-web rendered as a greyed off-tile (
launch-cell--off, 2 off-tiles). - Hub events since 1753: none — no
app_start_failed. (Contrast: under 0.163.1 the stopped BookStack fired app_start_failed events 1750/1752 at 08:02/08:17 the same morning.)
- Banner on
- Faults still alarm (Scenario B). Fault-injected
immichby stopping its supervised primaryimmich-server(siblings redis/postgres/machine-learning stayed up →StateDegraded). Rationale:docker killon anunless-stoppedcontainer self-restarts (→ restarting/running, never rests degraded), so the persistent-dead-member fault is injected withdocker stop. After one cycle:- Banner on
/: "nem fut: Immich (degraded)". - Hub event id 1754
app_start_failed"…Immich" fired (running→down transition). - calibre-web (still stopped) remained absent from the banner — suppression holds beside a real fault.
- Restore:
docker start immich-server→ healthy; banner self-cleared (none on/) on the next cycle.
- Banner on
- Stop→start stays correct (Scenario C).
POST /api/stacks/calibre-web/start→{"ok":true}; calibre-web healthy; launcher off-tiles dropped 2 → 1 (tile un-greyed). Hub events since 1753: only the immich1754; max id still 1754 — the stop AND the start produced zero events.
Final state: all four deployed apps (calibre-web, docmost, filebrowser, immich) healthy; system restored to baseline; 0.164.0 live. In-guest helper + local credential/DB copies removed.
Observations
- The one remaining launcher off-tile after Scenario C is a pre-existing non-operational app unrelated to this change (the 2→1 drop is exactly calibre-web un-greying).
- Hub event
1754is the legitimate audit record of the Scenario-B fault injection (info severity — the hub's own classification, unchanged); left in place. - No template/funcmap/notifier/dashboard-counter/Hungarian-copy change was made — the entire semantic
change is the one-line predicate at
classifyRunStates.