From 8e5edb28659ac092e18e140c1c0353562f8b64b2 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Fri, 24 Jul 2026 11:06:57 +0200 Subject: [PATCH] REPORT: v0.164.0 live-validated on 9201 (stop silent; fault alarms; stop->start clean) --- REPORT.md | 161 +++++++++++++++++++++++++++++------------------------- 1 file changed, 87 insertions(+), 74 deletions(-) diff --git a/REPORT.md b/REPORT.md index 889dd22..a2ef645 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,95 +1,108 @@ -# REPORT — Launcher polish: monogram reveal-on-failure + placeholder on every icon surface (v0.163.1) +# REPORT — v0.164.0: deliberately stopped apps no longer alarm (banner + email) -**Date:** 2026-07-24 · **Repo:** felhom-controller · **Target:** v0.163.1 · No agent coupling; MinAgent unchanged. +## Summary -## 1. Baseline used +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. -- felhom-controller `main` @ `7a53cb4`, version `v0.163.0` — **matched exactly** (clean tree, - HEAD == origin/main, CHANGELOG top = v0.163.0). No drift. +## Baselines -## 2. Files modified +| 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 | -**Templates / CSS** -- `controller/internal/web/templates/launcher.html` — both tile branches' `onerror` final step now - adds `this.parentElement.classList.add('launch-tile--noimg')`. -- `controller/internal/web/templates/style.css` — `.launch-mono { display: none; inset:0; … }` + - `.launch-tile--noimg .launch-mono { display: flex; }` (monogram = failure-only). -- `controller/internal/web/templates/backups_apps.html` — the allowlisted aligned row's icon now - uses the canonical `data-fallback="/static/app-placeholder.svg"` + 3-step chain; header comment updated. -- `controller/internal/web/templates/stacks.html` — `data-fallback` always present: - `{{if $im}}/static/infra-logo.svg{{else}}/static/app-placeholder.svg{{end}}`. -- `controller/internal/web/templates/app_info.html` — hero logo aligned to the 3-step chain + - placeholder; **screenshots untouched** (still `onerror="this.style.display='none'"`). -- `controller/internal/web/templates/deploy.html` — deploy logo aligned; kept its `.LogoURL`/`.LogoPNGURL` data source. +## Files modified -**Tests** -- `controller/internal/web/launcher_polish_test.go` — NEW (5 test functions). +- `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). -**Docs**: `CHANGELOG.md` (v0.163.1), `CONTEXT.md` (rule recorded), `REPORT.md` (this file). +Commit: `c23a0f6` (code + tests + docs). REPORT committed separately (post-validation). -**Gate script:** `app_row_dedup_gate.py` does NOT pin the `onerror` chain (only `app-row-icon` -presence + forbidden structures), so no gate-script edit was needed; all three gates stay green. +## The rule and its invariants (recorded at the seam, README, CONTEXT, CHANGELOG) -## 3. Commits pushed to `main` +`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`. -- `2c80868` — templates + CSS + tests + CHANGELOG + CONTEXT. -- `` — this REPORT.md. +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. -## 4. Tests + red-proofs +## Tests — results + red-proofs (count 3→4 notify, 4→7 main; +4 total) -Five new tests, all green. **2 red-proofs applied → observed FAIL → restored → re-green:** +| 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) | -| # | Group | Mutation | Test | Observed FAIL | -|---|-------|----------|------|---------------| -| A | launcher | removed `classList.add('launch-tile--noimg')` from both `onerror` else branches | `TestLauncher_MonogramRevealOnFailure` | reveal-hook count 0 (want 2) — the monogram would never re-appear for a logo-less tile | -| B | placeholder | reverted `backups_apps.html` to the old 2-step `onerror` chain | `TestBackupsApps_IconPlaceholder` | canonical 3-step chain absent → logo-less row dead-ends hidden | +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. -**New test functions (5):** `TestLauncher_MonogramRevealOnFailure` (Group A), -`TestBackupsApps_IconPlaceholder`, `TestStacks_IconPlaceholderAndInfra`, -`TestAppInfo_HeroPlaceholder_ScreenshotsUntouched`, `TestDeploy_LogoPlaceholder` (Group B). +## Deployment -Two test-only fixes made while writing them (not code bugs): the reveal class legitimately appears -inside the `onerror` JS string, so Group A asserts its count == 2 (both are `classList.add` hooks, -none in a `class=` attribute); and `{{.LogoPNGURL}}` is JS-escaped (`\/…`) inside `onerror`, so the -deploy test asserts the slash-free filename `docmost-logo.png`. +- 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)`. -## 5. Test count & suite +## Live validation (guest 9201, customer `demo-felhom`; endpoint-level — no browser) -- `go build ./... && go vet ./... && go test ./...` — **all green, 0 FAIL/panic.** -- Gates green: `app_row_dedup_gate.py`, `template_id_gate.py`, `emoji_gate.py`. -- Web package: +5 test functions vs v0.163.0. +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**. -## 6. Deployed version + verification +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. -- Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.163.1` (145M) from the clean pushed - tree (`2c80868`, HEAD == origin/main); deployed to guest 9201 via the bootstrap flow. -- Live: `docker ps` → `…felhom-controller:0.163.1 Up (healthy)`; log → - `controller_started (info) — Controller elindult (0.163.1)` + `settle-gate: GO … we are 0.163.1`. +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. -## 7. Live validation +## Observations -Method: **endpoint-level, authenticated** (real session cookie via `POST /login`; browser automation -unavailable on DooPlex), `curl` inside the `felhom-controller` container on guest 9201. The 9201 box -now has 5 launcher apps incl. Docmost (which has no logo asset — the exact reported case). - -1. **Launcher monogram (check 1).** On the live `/launcher`: `launch-tile--noimg` appears **5 times, - all 5 inside `onerror` `classList.add(...)` hooks, 0 in any `class=` attribute** — i.e. no tile - statically carries the reveal class, so no white letter bleeds through a loaded transparent glyph. - 5 tiles / 5 monogram spans (4 operational + 1 stopped — both branches exercised live). Served - `style.css` carries `.launch-mono { display: none; inset:0; … }` and - `.launch-tile--noimg .launch-mono { display: flex; }`. -2. **Placeholder on Biztonsági mentés → Alkalmazások (check 2).** The live `/backups/apps` page's - **Docmost row now carries `data-fallback="/static/app-placeholder.svg"`** and the canonical 3-step - chain (4 rows total with the placeholder default) — the reported hidden-icon dead-end is gone. -3. **No regression on Alkalmazások (check 3).** The live `/stacks` page: **all 56 `stack-logo-lg` - imgs still render** with a fallback — 52 default to `/static/app-placeholder.svg`, 4 infra cards - keep `/static/infra-logo.svg`. Every card keeps its logo source; no logo vanished. - -## 8. Observations (not acted on) - -- The four aligned copies (`backups_apps` aligned row, `stacks`, `app_info` hero, `deploy`) share the - same `onerror` grammar but differ in class/size/data-source, so consolidating them into one partial - is a possible future cleanup — deliberately NOT done here (backups_apps is gate-exempted because it - owns its collapse toggle; the task scoped this to chain-grammar alignment only). -- The launcher now has a live stopped tile (a greyed app on 9201), so the stopped-branch reveal hook - is exercised on real data, not only in the render test. +- 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`.