Merge pull request 'feat(alerts): live refresh table with toggle + severity colour cues' (#11) from alerts-live-refresh into main

Reviewed-on: #11
This commit is contained in:
2026-05-05 06:42:21 +00:00
3 changed files with 63 additions and 13 deletions
+7 -1
View File
@@ -18,6 +18,7 @@ type alertsPage struct {
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
File diff suppressed because one or more lines are too long
+51 -7
View File
@@ -56,14 +56,17 @@
{{end}}
</div>
{{/* 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. */}}
<div>
<select class="field" style="padding: 6px 10px; font-size: 11.5px; min-width: 130px;"
onchange="window.location='/alerts?status={{$filter.Status}}&severity='+this.value+'{{if $filter.HostID}}&host_id={{$filter.HostID}}{{end}}{{if $filter.Search}}&q={{$filter.Search}}{{end}}'">
<option value="" {{if eq $filter.Severity ""}}selected{{end}}>Severity · any</option>
<option value="info" {{if eq $filter.Severity "info"}}selected{{end}}>info</option>
<option value="warning" {{if eq $filter.Severity "warning"}}selected{{end}}>warning</option>
<option value="critical" {{if eq $filter.Severity "critical"}}selected{{end}}>critical</option>
<option value="info" style="color: oklch(0.78 0.005 250);" {{if eq $filter.Severity "info"}}selected{{end}}>info</option>
<option value="warning" style="color: oklch(0.82 0.13 80);" {{if eq $filter.Severity "warning"}}selected{{end}}>warning</option>
<option value="critical" style="color: oklch(0.70 0.20 25);" {{if eq $filter.Severity "critical"}}selected{{end}}>critical</option>
</select>
</div>
@@ -90,8 +93,18 @@
</form>
</div>
{{/* alerts table */}}
<div class="panel mt-3.5 rounded-[7px] overflow-hidden">
{{/* 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. */}}
<div id="alerts-table" class="panel mt-3.5 rounded-[7px] overflow-hidden"
hx-get="{{$page.RefreshURL}}"
hx-trigger="every 5s [document.visibilityState==='visible' && localStorage.getItem('rm-alerts-live')!=='off']"
hx-select="#alerts-table"
hx-swap="outerHTML">
{{/* header row */}}
<div class="alert-row head">
@@ -101,7 +114,16 @@
<div>Message</div>
<div>Raised</div>
<div>Last seen</div>
<div></div>
<div style="display: flex; align-items: center; gap: 6px; justify-content: flex-end;">
<label style="display: inline-flex; align-items: center; gap: 5px; cursor: pointer; font-size: 10px;"
class="text-ink-fade" title="auto-refresh every 5s">
<input type="checkbox" id="alerts-live-toggle" checked
onchange="localStorage.setItem('rm-alerts-live', this.checked ? 'on' : 'off'); document.getElementById('alerts-live-dot').style.opacity = this.checked ? '1' : '0.3';"
style="width: 11px; height: 11px; cursor: pointer; margin: 0;" />
<span>live</span>
<span id="alerts-live-dot" class="text-accent"></span>
</label>
</div>
</div>
{{if eq (len $page.Alerts) 0}}
@@ -126,4 +148,26 @@
</div>
</div>
<script>
// Restore the live-refresh toggle from localStorage so the operator's
// last choice survives full-page navigations. Re-runs after every htmx
// swap so the freshly-rendered checkbox + dot stay in sync.
(function syncLiveToggle() {
var on = localStorage.getItem('rm-alerts-live') !== 'off';
var cb = document.getElementById('alerts-live-toggle');
var dot = document.getElementById('alerts-live-dot');
if (cb) cb.checked = on;
if (dot) dot.style.opacity = on ? '1' : '0.3';
})();
document.body.addEventListener('htmx:afterSwap', function(e) {
if (e.detail.target && e.detail.target.id === 'alerts-table') {
var on = localStorage.getItem('rm-alerts-live') !== 'off';
var cb = document.getElementById('alerts-live-toggle');
var dot = document.getElementById('alerts-live-dot');
if (cb) cb.checked = on;
if (dot) dot.style.opacity = on ? '1' : '0.3';
}
});
</script>
{{end}}