diff --git a/CHANGELOG.md b/CHANGELOG.md index 654430f..b5a1444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +## v0.238.0 — the page follows the update, and a held app offers no way to start it (2026-09-13, update arc slice 4 Part 4) + +**MinAgent: 0.129.0** (unchanged) + +**No behaviour change on the box — the surface only.** v0.237.0 made the update a job; this release +makes the page show it. + +- **Pressing `Frissítés` follows the job.** The button text becomes the phase label — + `Ellenőrzés…`, `Biztonsági mentés készül a frissítés előtt…`, `Adatbázis pillanatkép…`, + `Új verzió letöltése…`, `Indítás az új verzióval…`, `Működés ellenőrzése…` — polling + `GET /api/stacks/{name}` every 3 s, and the page reloads when `updating` goes false. It no longer + reloads the instant the request returns. +- **An updating card offers no lifecycle button** and shows the phase as a progress tag. +- **A held card** (failed update OR failed restore) shows the hold sentence and a `Mentések` link, and + offers nothing that would start or update the app — `Eltávolítás` stays. +- **A failed update that held nothing** (backup failed, pull failed, precondition vanished) shows its + sentence above the buttons. +- **The updating and held checks come BEFORE `isOperational`.** That predicate counts `restarting` as + operational, which is how the 2026-09-01 spike saw a green `Frissítés` beside a crash loop. Pinned + with fixtures in `StateRestarting`; red-proofed by moving the checks after it (both tests fail). +- The app info page shows the same three notices under its header. **No new CSS**, no version number. + ## v0.237.0 — the Update button takes a backup first, and tells the truth (2026-09-13, update arc slice 4 — R-448, R-443, R-439) **MinAgent: 0.129.0** (unchanged) diff --git a/controller/README.md b/controller/README.md index 4a66198..41abef3 100644 --- a/controller/README.md +++ b/controller/README.md @@ -596,6 +596,11 @@ the boot sweep) puts a pin back or marks an interrupted update for `ResumeInterr since v0.237.0 the drive-return gate and the nightly volume dump. The nightly capture and Tier-2 run skip a held app so its restore point is not overwritten. +**The page (v0.238.0).** `Frissítés` follows the job — the button shows the phase label and the page +reloads when the update ends. An updating card offers no lifecycle button; a held card shows the hold +sentence with a `Mentések` link and nothing that would start it; a failed update that held nothing +shows its sentence. These checks run BEFORE `isOperational`, which counts `restarting` as operational. + **Not done, deliberately:** the old version is never put back automatically — whether that works is per-app and was measured unpredictable. Reasoning: `felhom.eu/documentation/architecture/09-update-architecture.md` §6. diff --git a/controller/internal/web/slice4_page_test.go b/controller/internal/web/slice4_page_test.go new file mode 100644 index 0000000..4dfc9ea --- /dev/null +++ b/controller/internal/web/slice4_page_test.go @@ -0,0 +1,70 @@ +package web + +import ( + "strings" + "testing" + + "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" +) + +// Slice 4 Part 4 — the card while an update runs, while an app is held, and after an update that +// failed without a hold. Rendered through the REAL stacks template. +// +// THE TRAP THESE PIN: isOperationalState counts `restarting` as operational — that is how the +// 2026-09-01 spike saw a green Frissítés beside a crash loop. Both fixtures below are in +// StateRestarting on purpose, so a template that checked isOperational FIRST would render all three +// lifecycle buttons and fail here. +// +// COMPANION RED-PROOF (REPORT.md): move the `{{if .Updating}}…{{else if .HoldReason}}` branches below +// `isOperational` in stacks.html — the held and updating tests fail with the buttons present. + +var slice4LifecycleButtons = []string{ + `stackAction(event, 'bookstack', 'update')`, + `stackAction(event, 'bookstack', 'restart')`, + `stackAction(event, 'bookstack', 'stop')`, + `stackAction(event, 'bookstack', 'start')`, +} + +func TestSlice4_Page_UpdatingCardShowsThePhaseAndNoLifecycleButton(t *testing.T) { + st := ubStack(nil, map[string]string{"web": "nginx:1.27"}, "2026-07-18") + st.State = stacks.StateRestarting + st.Updating, st.UpdatePhase, st.UpdatePhaseLabel = true, stacks.UpdatePhasePulling, stacks.UpdatePhaseLabel(stacks.UpdatePhasePulling) + h := renderBackupPage(t, "stacks", ubStacksData(st)) + if !strings.Contains(h, `data-update-phase="pulling"`) || !strings.Contains(h, stacks.UpdatePhaseLabel(stacks.UpdatePhasePulling)) { + t.Error("an updating card must show its phase label") + } + for _, b := range slice4LifecycleButtons { + if strings.Contains(h, b) { + t.Errorf("an updating card must offer no lifecycle button, found %q", b) + } + } +} + +func TestSlice4_Page_HeldCardShowsTheHoldAndTheWayBack(t *testing.T) { + st := ubStack(nil, map[string]string{"web": "nginx:1.27"}, "2026-07-18") + st.State = stacks.StateRestarting + st.HoldReason = "A(z) bookstack frissítése 2026-09-13 10:00-kor nem sikerült — HOLD" + h := renderBackupPage(t, "stacks", ubStacksData(st)) + if !strings.Contains(h, `data-held="true"`) || !strings.Contains(h, "HOLD") || !strings.Contains(h, `href="/backups/apps"`) { + t.Error("a held card must show the hold sentence and link Mentések") + } + for _, b := range slice4LifecycleButtons { + if strings.Contains(h, b) { + t.Errorf("a held card must offer nothing that starts or updates it, found %q", b) + } + } +} + +func TestSlice4_Page_FailedUpdateWithoutHoldKeepsTheButtonsAndSaysWhy(t *testing.T) { + st := ubStack(nil, map[string]string{"web": "nginx:1.27"}, "2026-07-18") + st.UpdateError = stacks.MsgUpdatePullFailed + h := renderBackupPage(t, "stacks", ubStacksData(st)) + if !strings.Contains(h, `data-update-error="true"`) || !strings.Contains(h, stacks.MsgUpdatePullFailed) { + t.Error("a failed update that held nothing must still say what happened") + } + for _, b := range slice4LifecycleButtons[:3] { + if !strings.Contains(h, b) { + t.Errorf("positive control: a running, unheld app keeps %q", b) + } + } +} diff --git a/controller/internal/web/templates/app_info.html b/controller/internal/web/templates/app_info.html index d9772c2..f94edfb 100644 --- a/controller/internal/web/templates/app_info.html +++ b/controller/internal/web/templates/app_info.html @@ -28,6 +28,14 @@ +{{if .Stack.Updating}} +