alerts: 5s polling cadence + live toggle + severity colour cues
CI / Build (windows/amd64) (pull_request) Successful in 23s
CI / Lint (pull_request) Successful in 38s
CI / Build (linux/amd64) (pull_request) Successful in 21s
CI / Build (linux/arm64) (pull_request) Successful in 23s
CI / Test (linux/amd64) (pull_request) Successful in 2m57s

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 <option> styling: Chrome and Firefox honour inline color:
on options; Safari ignores it. Acceptable degradation — falls back
to plain text, which is what we had.
This commit is contained in:
2026-05-04 23:35:03 +01:00
parent 9860b412f7
commit 8813e93317
2 changed files with 47 additions and 15 deletions
File diff suppressed because one or more lines are too long
+46 -14
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,16 +93,16 @@
</form>
</div>
{{/* 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. */}}
<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-trigger="every 5s [document.visibilityState==='visible' && localStorage.getItem('rm-alerts-live')!=='off']"
hx-select="#alerts-table"
hx-swap="outerHTML">
@@ -111,8 +114,15 @@
<div>Message</div>
<div>Raised</div>
<div>Last seen</div>
<div>
<span class="text-ink-fade" style="font-size: 10px;" title="auto-refresh every 15s">live ●</span>
<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>
@@ -138,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}}