feat(alerts): live-refresh the table every 15s while the tab is visible

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.
This commit is contained in:
2026-05-04 23:30:19 +01:00
parent 1618094a26
commit 9860b412f7
2 changed files with 26 additions and 8 deletions
+11 -5
View File
@@ -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
+15 -3
View File
@@ -90,8 +90,18 @@
</form>
</div>
{{/* alerts table */}}
<div class="panel mt-3.5 rounded-[7px] overflow-hidden">
{{/* 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. */}}
<div id="alerts-table" class="panel mt-3.5 rounded-[7px] overflow-hidden"
hx-get="{{$page.RefreshURL}}"
hx-trigger="every 15s [document.visibilityState==='visible']"
hx-select="#alerts-table"
hx-swap="outerHTML">
{{/* header row */}}
<div class="alert-row head">
@@ -101,7 +111,9 @@
<div>Message</div>
<div>Raised</div>
<div>Last seen</div>
<div></div>
<div>
<span class="text-ink-fade" style="font-size: 10px;" title="auto-refresh every 15s">live ●</span>
</div>
</div>
{{if eq (len $page.Alerts) 0}}