From 0f2eab7a3e73f1f1d4cab0212cf71abf631f9d37 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 6 Oct 2026 01:29:08 +0200 Subject: [PATCH] R-621: the hold panel points at the log the hold kept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A held app's page now says, inside the hold panel, that the app's own log from before it was stopped was kept, with a Napló link to the logs page that serves it (0b93e1a). Shown only for an update hold with a non-empty kept log (Manager.HoldLogKept). New copy through the bundle (hu + en), parity case app_info_hold_log captured when born. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- controller/internal/i18n/locales/en.json | 1 + controller/internal/i18n/locales/hu.json | 1 + controller/internal/stacks/manager.go | 12 + controller/internal/web/handlers.go | 7 + controller/internal/web/i18n_parity_test.go | 12 + .../internal/web/r621_hold_panel_test.go | 81 +++ .../internal/web/templates/app_info.html | 2 +- .../i18n_parity/app_info_hold_log.html | 586 ++++++++++++++++++ 8 files changed, 701 insertions(+), 1 deletion(-) create mode 100644 controller/internal/web/r621_hold_panel_test.go create mode 100644 controller/internal/web/testdata/i18n_parity/app_info_hold_log.html diff --git a/controller/internal/i18n/locales/en.json b/controller/internal/i18n/locales/en.json index af54c70..2c589cc 100644 --- a/controller/internal/i18n/locales/en.json +++ b/controller/internal/i18n/locales/en.json @@ -157,6 +157,7 @@ "app_info.mentesek": "Backups", "app_info.mire_hasznalhato": "What is it for?", "app_info.naplo": "Log", + "app_info.hold_log_kept": "The app's own log from before it was stopped was kept — it shows why the new version did not start.", "app_info.nincs_folyamatban_athelyezes": "No move in progress.", "app_info.regi_adatok_torlese": "Deleting old data", "app_info.telepites": "Install", diff --git a/controller/internal/i18n/locales/hu.json b/controller/internal/i18n/locales/hu.json index 8cbc696..54fd8c9 100644 --- a/controller/internal/i18n/locales/hu.json +++ b/controller/internal/i18n/locales/hu.json @@ -153,6 +153,7 @@ "app_info.mentesek": "Mentések", "app_info.mire_hasznalhato": "Mire használható?", "app_info.naplo": "Napló", + "app_info.hold_log_kept": "Az alkalmazás saját naplója megmaradt a leállítás előttről — ebből látszik, miért nem indult el az új változat.", "app_info.nincs_folyamatban_athelyezes": "Nincs folyamatban áthelyezés.", "app_info.regi_adatok_torlese": "Régi adatok törlése", "app_info.telepites": "Telepítés", diff --git a/controller/internal/stacks/manager.go b/controller/internal/stacks/manager.go index 9feac13..b223c6d 100644 --- a/controller/internal/stacks/manager.go +++ b/controller/internal/stacks/manager.go @@ -1480,6 +1480,18 @@ func (m *Manager) GetLogs(name string, lines int) (string, error) { return output, nil } +// HoldLogKept reports whether the hold kept a non-empty log of this app's own output (R-621) — the +// app page's hold panel then points at the logs page, which serves that kept log (GetLogs). Read-only; +// false for an unknown app. Pinned by TestR621_HoldPanelPointsAtTheKeptLog (internal/web). +func (m *Manager) HoldLogKept(name string) bool { + stack, ok := m.GetStack(name) + if !ok || stack.ComposePath == "" { + return false + } + _, _, kept := latestHoldLog(filepath.Dir(stack.ComposePath)) + return kept +} + // latestHoldLog returns the newest non-empty hold-logs//compose-logs.txt under a stack dir. The // directory names are UTC timestamps (20060102T150405Z), so the lexical order is the time order. func latestHoldLog(dir string) (content, ts string, ok bool) { diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index e690ad4..bd1b191 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -896,6 +896,13 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s data["AutoUpdateLine"] = s.msg(r, "app_info.auto_update_done", undoneWhen(found.AppConfig.LastAutoUpdate.At)) } + // R-621: a held app's own log, kept by the hold before its containers were removed, is reachable + // from the hold panel — the panel says it was kept and links the logs page that serves it. Only + // for an update hold (an unhealthy stop keeps no log), and only when a non-empty log exists. + if found.Deployed && found.HoldReason != "" && found.HoldKind != "unhealthy_stop" && s.stackMgr != nil { + data["HoldLogKept"] = s.stackMgr.HoldLogKept(found.Name) + } + s.executeTemplate(w, r, "app_info", data) } diff --git a/controller/internal/web/i18n_parity_test.go b/controller/internal/web/i18n_parity_test.go index 0520c51..63652c3 100644 --- a/controller/internal/web/i18n_parity_test.go +++ b/controller/internal/web/i18n_parity_test.go @@ -468,6 +468,18 @@ func i18nCases() []i18nCase { d["AppInfo"] = st.Meta.AppInfo return d }}) + // R-621: the hold panel's "the app's own log was kept" line and its Napló link, captured when born. + base = append(base, i18nCase{"app_info_hold_log", "app_info", func() map[string]interface{} { + d := i18nLayoutData("stacks", "AdventureLog") + st := stacks.Stack{Name: "adventurelog", Deployed: true, State: stacks.StateStopped, + HoldReason: "Az AdventureLog frissítése nem sikerült, az alkalmazás leállítva marad."} + st.Meta = stacks.Metadata{DisplayName: "AdventureLog", Slug: "adventurelog"} + d["Stack"] = st + d["Meta"] = st.Meta + d["AppInfo"] = st.Meta.AppInfo + d["HoldLogKept"] = true + return d + }}) return base } diff --git a/controller/internal/web/r621_hold_panel_test.go b/controller/internal/web/r621_hold_panel_test.go new file mode 100644 index 0000000..ea55791 --- /dev/null +++ b/controller/internal/web/r621_hold_panel_test.go @@ -0,0 +1,81 @@ +package web + +import ( + "context" + "html" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" +) + +// heldGuards holds every app; nothing else is exercised by the page. +type heldGuards struct{} + +func (heldGuards) HoldFor(string) (bool, string) { return true, "HOLD SENTENCE" } +func (heldGuards) Busy(string) (bool, string) { return false, "" } +func (heldGuards) RestorePoints(context.Context, string, func(stacks.UpdateRestorePoint) bool) (stacks.UpdateRestorePoint, bool, []stacks.UpdateRestorePoint) { + return stacks.UpdateRestorePoint{}, false, nil +} +func (heldGuards) CanBackUp(string) (bool, string) { return false, "" } +func (heldGuards) BackupNow(context.Context, string) error { return nil } +func (heldGuards) SafetyDump(context.Context, string) ([]string, error) { return nil, nil } +func (heldGuards) HoldAfterFailedUpdate(string, time.Time, stacks.UpdateRestorePoint, string) error { + return nil +} + +// R-621 (the last half): the app page's HOLD PANEL points at the log the hold kept. Driven through the +// REAL handler (appDetailHandler) and the REAL template, both languages, with the kept-log file present +// and absent — a render per branch of the gate (seam-wiring rule). +// +// COMPANION RED-PROOF: drop the HoldLogKept block from appDetailHandler — the "kept" branch fails. +func TestR621_HoldPanelPointsAtTheKeptLog(t *testing.T) { + s, _ := credsHarness(t) + s.stackMgr.SetUpdateGuards(heldGuards{}) + s.loadTemplates() + sd := filepath.Join(s.cfg.Paths.StacksDir, "crafty") + + render := func(lang string) string { + rr := httptest.NewRecorder() + s.appDetailHandler(rr, httptest.NewRequest("GET", "/apps/crafty?lang="+lang, nil), "crafty") + return html.UnescapeString(rr.Body.String()) + } + const link = `` + const huLine, enLine = "Az alkalmazás saját naplója megmaradt", "The app's own log from before it was stopped was kept" + + // No kept log: the panel is there (positive control) and says nothing about a log. + h := render("hu") + if !strings.Contains(h, `data-held="true"`) { + t.Fatal("positive control: the held panel must render — nothing below would be judged") + } + if strings.Contains(h, `data-hold-log="true"`) { + t.Error("with no kept log the panel must not claim one") + } + + p := filepath.Join(sd, "hold-logs", "20260922T080000Z") + if err := os.MkdirAll(p, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(p, "compose-logs.txt"), []byte("web-1 | never bound :8000\n"), 0o644); err != nil { + t.Fatal(err) + } + for _, tc := range []struct{ lang, want, notWant string }{{"hu", huLine, enLine}, {"en", enLine, huLine}} { + h := render(tc.lang) + i := strings.Index(h, `data-held="true"`) + if i < 0 { + t.Fatalf("%s: the held panel is gone", tc.lang) + } + panel := h[i:] + panel = panel[:strings.Index(panel, "")] + if !strings.Contains(panel, `data-hold-log="true"`) || !strings.Contains(panel, tc.want) || !strings.Contains(panel, link) { + t.Errorf("%s: the hold panel must say the log was kept and link the logs page; panel=%q", tc.lang, panel) + } + if strings.Contains(panel, tc.notWant) { + t.Errorf("%s: the other language's sentence is on the page", tc.lang) + } + } +} diff --git a/controller/internal/web/templates/app_info.html b/controller/internal/web/templates/app_info.html index 4ff4179..b213837 100644 --- a/controller/internal/web/templates/app_info.html +++ b/controller/internal/web/templates/app_info.html @@ -33,7 +33,7 @@ {{else if and .Stack.HoldReason (eq .Stack.HoldKind "unhealthy_stop")}}
{{holdText .Stack}}
{{else if .Stack.HoldReason}} -
+
{{holdText .Stack}}{{if not .Stack.HoldNoWholeCopy}} {{T "app_info.mentesek"}}{{end}}{{if .HoldLogKept}}
{{T "app_info.hold_log_kept"}} {{T "app_info.naplo"}}{{end}}
{{else if .Stack.UpdateError}}
{{updateErrorText .Stack}}
{{else if .UpdateUndoneLine}} diff --git a/controller/internal/web/testdata/i18n_parity/app_info_hold_log.html b/controller/internal/web/testdata/i18n_parity/app_info_hold_log.html new file mode 100644 index 0000000..4e55ecc --- /dev/null +++ b/controller/internal/web/testdata/i18n_parity/app_info_hold_log.html @@ -0,0 +1,586 @@ + + + + + + + + AdventureLog — Felhom.eu + + + + + + + + +
+ + +
+ + +
+ + + + + + + + + + + +
Az AdventureLog frissítése nem sikerült, az alkalmazás leállítva marad. Mentések
Az alkalmazás saját naplója megmaradt a leállítás előttről — ebből látszik, miért nem indult el az új változat. Napló
+ + + + + +
+ +
+ +

+ +
+ ~ RAM + + + Csak x86 + +
+ +
+
+ + +
+ + + +
+ + + + + + + + + + +
+ + + +