From 9dbed025e03b26ace52dcfbad5bfea44bd229d5b Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Mon, 4 May 2026 20:19:09 +0100 Subject: [PATCH] =?UTF-8?q?ui:=20F1=20=E2=80=94=20populate=20OpenAlerts=20?= =?UTF-8?q?in=20baseView=20so=20nav=20badge=20updates=20everywhere?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flagged in review of 35dee98: the Alerts tab badge should show the open count from any page, not just /alerts. baseView now takes the request and queries store.ListAlerts(Status: "open") to fill view.OpenAlerts on every page render. All call sites updated. --- internal/server/http/ui_alerts.go | 3 +-- internal/server/http/ui_handlers.go | 25 +++++++++++++++--------- internal/server/http/ui_notifications.go | 4 ++-- internal/server/http/ui_repo.go | 4 ++-- internal/server/http/ui_restore.go | 12 ++++++------ internal/server/http/ui_schedules.go | 8 ++++---- internal/server/http/ui_sources.go | 8 ++++---- 7 files changed, 35 insertions(+), 29 deletions(-) diff --git a/internal/server/http/ui_alerts.go b/internal/server/http/ui_alerts.go index c6eb360..1a736b3 100644 --- a/internal/server/http/ui_alerts.go +++ b/internal/server/http/ui_alerts.go @@ -59,8 +59,7 @@ func (s *Server) handleUIAlerts(w stdhttp.ResponseWriter, r *stdhttp.Request) { } page.Counts = computeAlertCounts(s, r) - view := s.baseView(u) - view.OpenAlerts = page.Counts.Open + view := s.baseView(r, u) view.Title = "Alerts · restic-manager" view.Active = "alerts" view.Page = page diff --git a/internal/server/http/ui_handlers.go b/internal/server/http/ui_handlers.go index 5b568fe..c5293d7 100644 --- a/internal/server/http/ui_handlers.go +++ b/internal/server/http/ui_handlers.go @@ -93,12 +93,20 @@ func (s *Server) requireUIUser(w stdhttp.ResponseWriter, r *stdhttp.Request) *ui // OpenAlerts is populated via a quick store count so the nav badge // stays current on every page load without requiring a page-specific // store call. -func (s *Server) baseView(u *ui.User) ui.ViewData { - return ui.ViewData{ +func (s *Server) baseView(r *stdhttp.Request, u *ui.User) ui.ViewData { + view := ui.ViewData{ User: u, Active: "dashboard", Version: s.version(), } + + // Populate OpenAlerts from the store so the nav badge shows the + // current count on every page. + if open, err := s.deps.Store.ListAlerts(r.Context(), store.AlertFilter{Status: "open"}); err == nil { + view.OpenAlerts = len(open) + } + + return view } // version returns the binary's build version — passed in via Deps so @@ -231,8 +239,7 @@ func (s *Server) handleUIDashboard(w stdhttp.ResponseWriter, r *stdhttp.Request) slog.Warn("ui dashboard: list pending hosts", "err", perr) } - view := s.baseView(u) - view.OpenAlerts = summary.OpenAlerts + view := s.baseView(r, u) view.Page = dashboardPage{ Hosts: rows, HostCount: len(hosts), @@ -299,7 +306,7 @@ func (s *Server) handleUIAddHostGet(w stdhttp.ResponseWriter, r *stdhttp.Request if u == nil { return } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Add host · restic-manager" view.Page = addHostPage{ServerURL: s.publicURL(r)} if err := s.deps.UI.Render(w, "add_host", view); err != nil { @@ -371,7 +378,7 @@ func (s *Server) handleUIAddHostPost(w stdhttp.ResponseWriter, r *stdhttp.Reques } } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Add host · restic-manager" view.Page = page w.WriteHeader(stdhttp.StatusUnprocessableEntity) @@ -438,7 +445,7 @@ func (s *Server) handleUIPendingHost(w stdhttp.ResponseWriter, r *stdhttp.Reques } } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Pending host · restic-manager" view.Page = page if err := s.deps.UI.Render(w, "pending_host", view); err != nil { @@ -616,7 +623,7 @@ func (s *Server) handleUIHostDetail(w stdhttp.ResponseWriter, r *stdhttp.Request shown = shown[:cap] } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = host.Name + " · restic-manager" view.Page = hostDetailPage{ hostChromeData: s.loadHostChrome(r, *host, "snapshots", "snapshots"), @@ -716,7 +723,7 @@ func (s *Server) handleUIJobDetail(w stdhttp.ResponseWriter, r *stdhttp.Request) nextSeq = logs[n-1].Seq } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = job.Kind + " · " + host.Name + " · restic-manager" view.Page = jobDetailPage{ Job: *job, diff --git a/internal/server/http/ui_notifications.go b/internal/server/http/ui_notifications.go index 8df3619..b580a7e 100644 --- a/internal/server/http/ui_notifications.go +++ b/internal/server/http/ui_notifications.go @@ -108,8 +108,8 @@ func (s *Server) loadSettingsPage(r *stdhttp.Request) (*settingsPage, error) { // renderSettingsPage renders the settings shell, setting HTTP 422 on // validation failure (pass status=0 for the normal 200). -func (s *Server) renderSettingsPage(w stdhttp.ResponseWriter, _ *stdhttp.Request, u *ui.User, page *settingsPage, status int) { - view := s.baseView(u) +func (s *Server) renderSettingsPage(w stdhttp.ResponseWriter, r *stdhttp.Request, u *ui.User, page *settingsPage, status int) { + view := s.baseView(r, u) view.Title = "Settings · restic-manager" view.Active = "settings" view.Page = *page diff --git a/internal/server/http/ui_repo.go b/internal/server/http/ui_repo.go index ac42cc9..461081f 100644 --- a/internal/server/http/ui_repo.go +++ b/internal/server/http/ui_repo.go @@ -244,7 +244,7 @@ func (s *Server) handleUIHostRepo(w stdhttp.ResponseWriter, r *stdhttp.Request) return } page.SavedSection = r.URL.Query().Get("saved") - view := s.baseView(u) + view := s.baseView(r, u) view.Title = host.Name + " repo · restic-manager" view.Page = *page if err := s.deps.UI.Render(w, "host_repo", view); err != nil { @@ -268,7 +268,7 @@ func (s *Server) renderRepoPage(w stdhttp.ResponseWriter, r *stdhttp.Request, u page.AdminCredsError = adminErr page.BandwidthError = bwErr page.MaintenanceError = mntErr - view := s.baseView(u) + view := s.baseView(r, u) view.Title = host.Name + " repo · restic-manager" view.Page = *page w.WriteHeader(stdhttp.StatusUnprocessableEntity) diff --git a/internal/server/http/ui_restore.go b/internal/server/http/ui_restore.go index c43fa31..65acab8 100644 --- a/internal/server/http/ui_restore.go +++ b/internal/server/http/ui_restore.go @@ -105,7 +105,7 @@ func (s *Server) handleUIRestoreGet(w stdhttp.ResponseWriter, r *stdhttp.Request } } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Restore · " + host.Name view.Page = page if err := s.deps.UI.Render(w, "host_restore", view); err != nil { @@ -161,7 +161,7 @@ func (s *Server) handleUIRestorePost(w stdhttp.ResponseWriter, r *stdhttp.Reques break } } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Restore · " + host.Name view.Page = page w.WriteHeader(status) @@ -329,7 +329,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, Error: "agent offline", } - view := s.baseView(u) + view := s.baseView(r, u) view.Page = page _ = s.deps.UI.RenderPartial(w, "tree_node", view) return @@ -345,7 +345,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, Error: err.Error(), } - view := s.baseView(u) + view := s.baseView(r, u) view.Page = page _ = s.deps.UI.RenderPartial(w, "tree_node", view) return @@ -355,7 +355,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, Error: result.Error, } - view := s.baseView(u) + view := s.baseView(r, u) view.Page = page _ = s.deps.UI.RenderPartial(w, "tree_node", view) return @@ -382,7 +382,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, Children: children, } - view := s.baseView(u) + view := s.baseView(r, u) view.Page = page if err := s.deps.UI.RenderPartial(w, "tree_node", view); err != nil { slog.Warn("ui restore tree: render partial", "err", err) diff --git a/internal/server/http/ui_schedules.go b/internal/server/http/ui_schedules.go index a4daf4d..b436787 100644 --- a/internal/server/http/ui_schedules.go +++ b/internal/server/http/ui_schedules.go @@ -112,7 +112,7 @@ func (s *Server) handleUISchedulesList(w stdhttp.ResponseWriter, r *stdhttp.Requ chrome.ScheduleCount = len(scheds) chrome.SourceGroupCount = len(groups) - view := s.baseView(u) + view := s.baseView(r, u) view.Title = host.Name + " schedules · restic-manager" view.Page = hostSchedulesPage{ hostChromeData: chrome, @@ -140,7 +140,7 @@ func (s *Server) handleUIScheduleNewGet(w stdhttp.ResponseWriter, r *stdhttp.Req stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) return } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "New schedule · " + host.Name + " · restic-manager" view.Page = scheduleEditPage{ hostChromeData: s.loadHostChrome(r, *host, "schedules", "new schedule"), @@ -186,7 +186,7 @@ func (s *Server) handleUIScheduleEditGet(w stdhttp.ResponseWriter, r *stdhttp.Re for _, gid := range sc.SourceGroupIDs { selected[gid] = true } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Edit schedule · " + host.Name + " · restic-manager" view.Page = scheduleEditPage{ hostChromeData: s.loadHostChrome(r, *host, "schedules", "edit schedule"), @@ -415,7 +415,7 @@ func (s *Server) renderScheduleFormError(w stdhttp.ResponseWriter, r *stdhttp.Re saveAction = "/hosts/" + host.ID + "/schedules/" + sid + "/edit" crumb = "edit schedule" } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Schedule · " + host.Name + " · restic-manager" view.Page = scheduleEditPage{ hostChromeData: s.loadHostChrome(r, *host, "schedules", crumb), diff --git a/internal/server/http/ui_sources.go b/internal/server/http/ui_sources.go index c4581a5..617a79f 100644 --- a/internal/server/http/ui_sources.go +++ b/internal/server/http/ui_sources.go @@ -121,7 +121,7 @@ func (s *Server) handleUIHostSources(w stdhttp.ResponseWriter, r *stdhttp.Reques // loadHostChrome already counted groups; reuse count we just got. chrome.SourceGroupCount = len(groups) - view := s.baseView(u) + view := s.baseView(r, u) view.Title = host.Name + " sources · restic-manager" view.Page = hostSourcesPage{hostChromeData: chrome, Groups: rows} if err := s.deps.UI.Render(w, "host_sources", view); err != nil { @@ -139,7 +139,7 @@ func (s *Server) handleUISourceGroupNewGet(w stdhttp.ResponseWriter, r *stdhttp. if !ok { return } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "New source group · " + host.Name + " · restic-manager" view.Page = sourceGroupEditPage{ hostChromeData: s.loadHostChrome(r, *host, "sources", "new source group"), @@ -173,7 +173,7 @@ func (s *Server) handleUISourceGroupEditGet(w stdhttp.ResponseWriter, r *stdhttp stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) return } - view := s.baseView(u) + view := s.baseView(r, u) view.Title = g.Name + " · " + host.Name + " · restic-manager" form := formFromGroup(*g) form.PreHook = s.decryptHookOrFallback(g.PreHook, "", host.ID, "pre") @@ -362,7 +362,7 @@ func (s *Server) handleUISourceGroupDelete(w stdhttp.ResponseWriter, r *stdhttp. // typed input intact + an error banner. Returns 422 to signal "form // rejected" while still returning HTML (mirrors handleUIAddHostPost). func (s *Server) renderSourceFormError(w stdhttp.ResponseWriter, r *stdhttp.Request, u *ui.User, host *store.Host, gid string, isNew bool, form sourceFormData, msg string) { - view := s.baseView(u) + view := s.baseView(r, u) view.Title = "Source group · " + host.Name + " · restic-manager" saveAction := "/hosts/" + host.ID + "/sources/new" crumb := "new source group"