109 lines
6.8 KiB
Markdown
109 lines
6.8 KiB
Markdown
# 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.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` — 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`.
|