ui: F1 — populate OpenAlerts in baseView so nav badge updates everywhere

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.
This commit is contained in:
2026-05-04 20:19:09 +01:00
parent 35dee98cf9
commit 9dbed025e0
7 changed files with 35 additions and 29 deletions
+1 -2
View File
@@ -59,8 +59,7 @@ func (s *Server) handleUIAlerts(w stdhttp.ResponseWriter, r *stdhttp.Request) {
} }
page.Counts = computeAlertCounts(s, r) page.Counts = computeAlertCounts(s, r)
view := s.baseView(u) view := s.baseView(r, u)
view.OpenAlerts = page.Counts.Open
view.Title = "Alerts · restic-manager" view.Title = "Alerts · restic-manager"
view.Active = "alerts" view.Active = "alerts"
view.Page = page view.Page = page
+16 -9
View File
@@ -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 // OpenAlerts is populated via a quick store count so the nav badge
// stays current on every page load without requiring a page-specific // stays current on every page load without requiring a page-specific
// store call. // store call.
func (s *Server) baseView(u *ui.User) ui.ViewData { func (s *Server) baseView(r *stdhttp.Request, u *ui.User) ui.ViewData {
return ui.ViewData{ view := ui.ViewData{
User: u, User: u,
Active: "dashboard", Active: "dashboard",
Version: s.version(), 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 // 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) slog.Warn("ui dashboard: list pending hosts", "err", perr)
} }
view := s.baseView(u) view := s.baseView(r, u)
view.OpenAlerts = summary.OpenAlerts
view.Page = dashboardPage{ view.Page = dashboardPage{
Hosts: rows, Hosts: rows,
HostCount: len(hosts), HostCount: len(hosts),
@@ -299,7 +306,7 @@ func (s *Server) handleUIAddHostGet(w stdhttp.ResponseWriter, r *stdhttp.Request
if u == nil { if u == nil {
return return
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = "Add host · restic-manager" view.Title = "Add host · restic-manager"
view.Page = addHostPage{ServerURL: s.publicURL(r)} view.Page = addHostPage{ServerURL: s.publicURL(r)}
if err := s.deps.UI.Render(w, "add_host", view); err != nil { 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.Title = "Add host · restic-manager"
view.Page = page view.Page = page
w.WriteHeader(stdhttp.StatusUnprocessableEntity) 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.Title = "Pending host · restic-manager"
view.Page = page view.Page = page
if err := s.deps.UI.Render(w, "pending_host", view); err != nil { 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] shown = shown[:cap]
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = host.Name + " · restic-manager" view.Title = host.Name + " · restic-manager"
view.Page = hostDetailPage{ view.Page = hostDetailPage{
hostChromeData: s.loadHostChrome(r, *host, "snapshots", "snapshots"), 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 nextSeq = logs[n-1].Seq
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = job.Kind + " · " + host.Name + " · restic-manager" view.Title = job.Kind + " · " + host.Name + " · restic-manager"
view.Page = jobDetailPage{ view.Page = jobDetailPage{
Job: *job, Job: *job,
+2 -2
View File
@@ -108,8 +108,8 @@ func (s *Server) loadSettingsPage(r *stdhttp.Request) (*settingsPage, error) {
// renderSettingsPage renders the settings shell, setting HTTP 422 on // renderSettingsPage renders the settings shell, setting HTTP 422 on
// validation failure (pass status=0 for the normal 200). // 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) { func (s *Server) renderSettingsPage(w stdhttp.ResponseWriter, r *stdhttp.Request, u *ui.User, page *settingsPage, status int) {
view := s.baseView(u) view := s.baseView(r, u)
view.Title = "Settings · restic-manager" view.Title = "Settings · restic-manager"
view.Active = "settings" view.Active = "settings"
view.Page = *page view.Page = *page
+2 -2
View File
@@ -244,7 +244,7 @@ func (s *Server) handleUIHostRepo(w stdhttp.ResponseWriter, r *stdhttp.Request)
return return
} }
page.SavedSection = r.URL.Query().Get("saved") page.SavedSection = r.URL.Query().Get("saved")
view := s.baseView(u) view := s.baseView(r, u)
view.Title = host.Name + " repo · restic-manager" view.Title = host.Name + " repo · restic-manager"
view.Page = *page view.Page = *page
if err := s.deps.UI.Render(w, "host_repo", view); err != nil { 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.AdminCredsError = adminErr
page.BandwidthError = bwErr page.BandwidthError = bwErr
page.MaintenanceError = mntErr page.MaintenanceError = mntErr
view := s.baseView(u) view := s.baseView(r, u)
view.Title = host.Name + " repo · restic-manager" view.Title = host.Name + " repo · restic-manager"
view.Page = *page view.Page = *page
w.WriteHeader(stdhttp.StatusUnprocessableEntity) w.WriteHeader(stdhttp.StatusUnprocessableEntity)
+6 -6
View File
@@ -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.Title = "Restore · " + host.Name
view.Page = page view.Page = page
if err := s.deps.UI.Render(w, "host_restore", view); err != nil { 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 break
} }
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = "Restore · " + host.Name view.Title = "Restore · " + host.Name
view.Page = page view.Page = page
w.WriteHeader(status) w.WriteHeader(status)
@@ -329,7 +329,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques
HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, HostID: host.ID, SnapshotID: snapshotID, Path: pathArg,
Error: "agent offline", Error: "agent offline",
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Page = page view.Page = page
_ = s.deps.UI.RenderPartial(w, "tree_node", view) _ = s.deps.UI.RenderPartial(w, "tree_node", view)
return return
@@ -345,7 +345,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques
HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, HostID: host.ID, SnapshotID: snapshotID, Path: pathArg,
Error: err.Error(), Error: err.Error(),
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Page = page view.Page = page
_ = s.deps.UI.RenderPartial(w, "tree_node", view) _ = s.deps.UI.RenderPartial(w, "tree_node", view)
return return
@@ -355,7 +355,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques
HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, HostID: host.ID, SnapshotID: snapshotID, Path: pathArg,
Error: result.Error, Error: result.Error,
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Page = page view.Page = page
_ = s.deps.UI.RenderPartial(w, "tree_node", view) _ = s.deps.UI.RenderPartial(w, "tree_node", view)
return return
@@ -382,7 +382,7 @@ func (s *Server) handleUIRestoreTree(w stdhttp.ResponseWriter, r *stdhttp.Reques
HostID: host.ID, SnapshotID: snapshotID, Path: pathArg, HostID: host.ID, SnapshotID: snapshotID, Path: pathArg,
Children: children, Children: children,
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Page = page view.Page = page
if err := s.deps.UI.RenderPartial(w, "tree_node", view); err != nil { if err := s.deps.UI.RenderPartial(w, "tree_node", view); err != nil {
slog.Warn("ui restore tree: render partial", "err", err) slog.Warn("ui restore tree: render partial", "err", err)
+4 -4
View File
@@ -112,7 +112,7 @@ func (s *Server) handleUISchedulesList(w stdhttp.ResponseWriter, r *stdhttp.Requ
chrome.ScheduleCount = len(scheds) chrome.ScheduleCount = len(scheds)
chrome.SourceGroupCount = len(groups) chrome.SourceGroupCount = len(groups)
view := s.baseView(u) view := s.baseView(r, u)
view.Title = host.Name + " schedules · restic-manager" view.Title = host.Name + " schedules · restic-manager"
view.Page = hostSchedulesPage{ view.Page = hostSchedulesPage{
hostChromeData: chrome, hostChromeData: chrome,
@@ -140,7 +140,7 @@ func (s *Server) handleUIScheduleNewGet(w stdhttp.ResponseWriter, r *stdhttp.Req
stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError) stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
return return
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = "New schedule · " + host.Name + " · restic-manager" view.Title = "New schedule · " + host.Name + " · restic-manager"
view.Page = scheduleEditPage{ view.Page = scheduleEditPage{
hostChromeData: s.loadHostChrome(r, *host, "schedules", "new schedule"), 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 { for _, gid := range sc.SourceGroupIDs {
selected[gid] = true selected[gid] = true
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = "Edit schedule · " + host.Name + " · restic-manager" view.Title = "Edit schedule · " + host.Name + " · restic-manager"
view.Page = scheduleEditPage{ view.Page = scheduleEditPage{
hostChromeData: s.loadHostChrome(r, *host, "schedules", "edit schedule"), 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" saveAction = "/hosts/" + host.ID + "/schedules/" + sid + "/edit"
crumb = "edit schedule" crumb = "edit schedule"
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = "Schedule · " + host.Name + " · restic-manager" view.Title = "Schedule · " + host.Name + " · restic-manager"
view.Page = scheduleEditPage{ view.Page = scheduleEditPage{
hostChromeData: s.loadHostChrome(r, *host, "schedules", crumb), hostChromeData: s.loadHostChrome(r, *host, "schedules", crumb),
+4 -4
View File
@@ -121,7 +121,7 @@ func (s *Server) handleUIHostSources(w stdhttp.ResponseWriter, r *stdhttp.Reques
// loadHostChrome already counted groups; reuse count we just got. // loadHostChrome already counted groups; reuse count we just got.
chrome.SourceGroupCount = len(groups) chrome.SourceGroupCount = len(groups)
view := s.baseView(u) view := s.baseView(r, u)
view.Title = host.Name + " sources · restic-manager" view.Title = host.Name + " sources · restic-manager"
view.Page = hostSourcesPage{hostChromeData: chrome, Groups: rows} view.Page = hostSourcesPage{hostChromeData: chrome, Groups: rows}
if err := s.deps.UI.Render(w, "host_sources", view); err != nil { 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 { if !ok {
return return
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = "New source group · " + host.Name + " · restic-manager" view.Title = "New source group · " + host.Name + " · restic-manager"
view.Page = sourceGroupEditPage{ view.Page = sourceGroupEditPage{
hostChromeData: s.loadHostChrome(r, *host, "sources", "new source group"), 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) stdhttp.Error(w, "internal", stdhttp.StatusInternalServerError)
return return
} }
view := s.baseView(u) view := s.baseView(r, u)
view.Title = g.Name + " · " + host.Name + " · restic-manager" view.Title = g.Name + " · " + host.Name + " · restic-manager"
form := formFromGroup(*g) form := formFromGroup(*g)
form.PreHook = s.decryptHookOrFallback(g.PreHook, "", host.ID, "pre") 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 // typed input intact + an error banner. Returns 422 to signal "form
// rejected" while still returning HTML (mirrors handleUIAddHostPost). // 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) { 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" view.Title = "Source group · " + host.Name + " · restic-manager"
saveAction := "/hosts/" + host.ID + "/sources/new" saveAction := "/hosts/" + host.ID + "/sources/new"
crumb := "new source group" crumb := "new source group"