From 595656ed5927e8e3ed81c42f84c0719eef27b40f Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Mon, 4 May 2026 23:30:19 +0100 Subject: [PATCH 1/2] 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}} From d46adabeecb5a441749e4ce3fd74f7b3cd2e3824 Mon Sep 17 00:00:00 2001 From: Steve Cliff Date: Mon, 4 May 2026 23:35:03 +0100 Subject: [PATCH 2/2] alerts: 5s polling cadence + live toggle + severity colour cues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two operator-visible changes on /alerts: 1. Polling drops from 15s to 5s and gains a checkbox in the table header to turn live monitoring on/off. Choice is persisted in localStorage so it survives full-page navigations. The toggle state is woven into the htmx hx-trigger predicate, so flipping the checkbox just sets the flag and the next tick (or the absence of one) honours it — no attribute juggling, no htmx.process re-init. The dot dims to 0.3 opacity when paused so operators can see at a glance that they're looking at a stale view. 2. Severity dropdown options pick up the same oklch tints used by the row dots / left borders / kind chips. The kind column shows only the kind text, so without a colour cue the dropdown mentioned a concept (severity) that the table itself didn't render. Now the colours bridge the gap. Note on
- {{/* severity dropdown */}} + {{/* severity dropdown — option text tinted to match the colour + already used in the row (dot, left border, kind chip). The + severity word is otherwise invisible to operators because the + table column shows kind only; the colour bridges the two. */}}
@@ -90,16 +93,16 @@
- {{/* 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. */}} + {{/* alerts table — polled every 5s when the tab is visible AND the + live toggle is on. The localStorage check is part of the htmx + trigger predicate, so flipping the toggle just sets the flag and + the next tick (or the absence of one) honours it. No need to + re-process the element when the toggle changes. + The polling lives on this div (not the page root) so the filter + strip and header don't flash on each tick. */}}
@@ -111,8 +114,15 @@
Message
Raised
Last seen
-
- live ● +
+
@@ -138,4 +148,26 @@
+ + {{end}}