Files
felhom-controller/REPORT.md
T

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 — extracted scanDeployedAppRunStates's pure core to classifyRunStates([]stacks.Stack) ([]web.DeadApp, []notify.AppRunState); changed the down predicate to stacks.IsDownState(st.State) && st.State != stacks.StateStopped; documented invariants I1/I2 at the seam.
  • controller/cmd/controller/classify_runstates_test.gonew; 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 — new classifyRunStates seam 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.StopStack runs docker compose down → containers are removed, and a deployed stack with zero containers aggregates to StateStopped (refreshStatusLocked). Proven live: after the stop, docker ps -a showed 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 at stopped — faults surface as exited/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_StoppedIsSuppressed FAILED ("dead list must be exactly …, got […cwa/stopped…]"). Restored → PASS.
  • Group C red-proof — flipped the stop cycle to Down:true: TestNotifyAppStartFailures_StopStartCrashSequence FAILED ("a deliberate stop must fire no event, got 1"). Restored → PASS.

Deployment

  • Built 0.164.0 on DooPlex (build.sh 0.164.0 --push), pushed to gitea.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.

  1. 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.)
  2. Faults still alarm (Scenario B). Fault-injected immich by stopping its supervised primary immich-server (siblings redis/postgres/machine-learning stayed up → StateDegraded). Rationale: docker kill on an unless-stopped container self-restarts (→ restarting/running, never rests degraded), so the persistent-dead-member fault is injected with docker 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.
  3. 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 immich 1754; 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 1754 is 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.