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

Flagged in review of cd38b40: 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 cd38b40516
commit d373d19647
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)
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
+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
// 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,
+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
// 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
+2 -2
View File
@@ -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)
+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.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)
+4 -4
View File
@@ -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),
+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.
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"