From 595656ed5927e8e3ed81c42f84c0719eef27b40f Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Mon, 4 May 2026 23:30:19 +0100 Subject: [PATCH] feat(alerts): live-refresh the table every 15s while the tab is visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The alerts list is the one screen where staleness is genuinely harmful — an operator can be looking at an Open tab that's already been resolved by another admin or auto-resolved by the engine, and take action on a row that no longer exists. Add an htmx poll on just the table panel: hx-get same URL with current querystring (filters preserved) hx-trigger every 15s, only when document is visible (no idle CPU) hx-select #alerts-table — pull this element out of the response hx-swap outerHTML Polling lives on the table div, not the page root, so the filter strip and header don't flash on each tick. Header gains a small 'live ●' label so the polling is discoverable. RefreshURL is r.URL.RequestURI() on the server side — keeps any status/severity/host_id/q params intact across refreshes. Other screens (dashboard, hosts, jobs) deliberately stay manual- refresh per the project's anti-flicker stance. --- internal/server/http/ui_alerts.go | 16 +++++++++++----- web/templates/pages/alerts.html | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/internal/server/http/ui_alerts.go b/internal/server/http/ui_alerts.go index 06c82fb..8fc76ac 100644 --- a/internal/server/http/ui_alerts.go +++ b/internal/server/http/ui_alerts.go @@ -14,10 +14,11 @@ import ( ) type alertsPage struct { - Filter store.AlertFilter - Alerts []store.Alert - Counts alertCounts - HostNames map[string]string // host_id → name for table rendering + Filter store.AlertFilter + Alerts []store.Alert + Counts alertCounts + HostNames map[string]string // host_id → name for table rendering + RefreshURL string // self-URL for the live-refresh poll } type alertCounts struct { @@ -51,7 +52,12 @@ func (s *Server) handleUIAlerts(w stdhttp.ResponseWriter, r *stdhttp.Request) { return } - page := alertsPage{Filter: f, Alerts: alerts, HostNames: map[string]string{}} + page := alertsPage{ + Filter: f, + Alerts: alerts, + HostNames: map[string]string{}, + RefreshURL: r.URL.RequestURI(), + } if hosts, err := s.deps.Store.ListHosts(r.Context()); err == nil { for _, h := range hosts { page.HostNames[h.ID] = h.Name diff --git a/web/templates/pages/alerts.html b/web/templates/pages/alerts.html index c059e24..7b3fd26 100644 --- a/web/templates/pages/alerts.html +++ b/web/templates/pages/alerts.html @@ -90,8 +90,18 @@ - {{/* alerts table */}} -
+ {{/* alerts table — polled every 15s while the tab is visible. + hx-get re-fetches the same URL (so filter querystring is preserved) + and hx-select pulls just this element out of the full response, + replacing the live one. Pauses automatically when the tab is + backgrounded so we're not burning CPU on inactive tabs. + The polling lives here (not on the page root) so the filter strip + and header don't flash on each tick. */}} +
{{/* header row */}}
@@ -101,7 +111,9 @@
Message
Raised
Last seen
-
+
+ live ● +
{{if eq (len $page.Alerts) 0}}