Replaces the 11-column table with severity-sorted record rows: host, combined status chip, latency, alert chips, time-in-state, and the most recent alert message. Per-family address/state/latency/last-change move to a hover card on the status chip and permanently to a new Connectivity table in the Host Overview info section (host info API now includes connections). Last-alert is seeded from /api/0/alerts and kept fresh from the event stream; the event log below uses the same row idiom with its filters intact. WS reconnect state shows in the toolbar instead of a modal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
489 lines
22 KiB
HTML
489 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
{% include 'head.html' %}
|
||
<link rel="stylesheet" href="/static/hbd-ui.css">
|
||
<script src="/static/hbd-ui.js"></script>
|
||
|
||
<style>
|
||
html, body { height: auto; overflow: visible; }
|
||
body { background: var(--st-paper); padding: 60px 0 0; }
|
||
|
||
.live-main { max-width: 1180px; margin: 0 auto; padding: 0 20px 70px; }
|
||
|
||
.st-toolbar input {
|
||
font: 12px var(--st-mono); color: var(--st-ink);
|
||
background: var(--st-surface); border: 1px solid var(--st-line); border-radius: 999px;
|
||
padding: 5px 12px; min-width: 160px;
|
||
}
|
||
.live-dot { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--st-ok); }
|
||
.live-dot::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--st-ok); }
|
||
.live-dot.reconnecting { color: var(--st-crit); }
|
||
.live-dot.reconnecting::before { background: var(--st-crit); }
|
||
|
||
/* ── host rows: 6 columns ── */
|
||
.hrow, .hhead {
|
||
display: grid;
|
||
grid-template-columns: 175px 105px 70px 125px 85px minmax(0,1fr);
|
||
gap: 4px 14px; align-items: center;
|
||
padding: 6px 14px; border-bottom: 1px solid var(--st-line-soft);
|
||
}
|
||
.hrow:last-child { border-bottom: none; }
|
||
.hrow:hover { background: var(--st-surface-2); }
|
||
.hhead {
|
||
font-family: var(--st-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em;
|
||
color: var(--st-faint); background: var(--st-surface-2); border-radius: 8px 8px 0 0;
|
||
}
|
||
.hhead .num, .hrow .num { text-align: right; }
|
||
|
||
.hrow.sev-warn { border-left: 3px solid var(--st-warn); padding-left: 11px; }
|
||
.hrow.sev-crit { border-left: 3px solid var(--st-crit); padding-left: 11px; }
|
||
|
||
.host { font-family: var(--st-mono); font-size: 13px; font-weight: 400; display: flex; align-items: center; gap: 7px; min-width: 0; }
|
||
.host.watched { font-weight: 700; }
|
||
.host a { color: inherit; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.host a:hover { color: var(--st-accent); }
|
||
.dot.down { background: var(--st-crit); box-shadow: 0 0 0 3px var(--st-crit-soft); }
|
||
.dot.overdue { background: transparent; border: 2px solid var(--st-warn); }
|
||
.stale { color: var(--st-faint); font-size: 10px; border: 1px solid var(--st-line); border-radius: 4px; padding: 1px 4px; font-family: var(--st-mono); white-space: nowrap; }
|
||
.chip.outline { background: transparent; border: 1px solid var(--st-line); }
|
||
|
||
.lat { font-family: var(--st-mono); font-size: 12px; color: var(--st-muted); font-variant-numeric: tabular-nums; }
|
||
.since { font-family: var(--st-mono); font-size: 11.5px; color: var(--st-faint); font-variant-numeric: tabular-nums; }
|
||
|
||
.lastalert { font-family: var(--st-mono); font-size: 11.5px; color: var(--st-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.lastalert .lvl { font-weight: 700; }
|
||
.lastalert .lvl.crit { color: var(--st-crit); }
|
||
.lastalert .lvl.warn { color: var(--st-warn); }
|
||
.lastalert .ago { color: var(--st-faint); }
|
||
.lastalert.quiet { color: var(--st-faint); }
|
||
|
||
/* ── hover card on the status chip ── */
|
||
.tip { position: relative; display: inline-flex; }
|
||
.tipbox {
|
||
display: none; position: absolute; z-index: 40; top: calc(100% + 6px); left: 0;
|
||
width: 340px; background: var(--st-surface); border: 1px solid var(--st-line); border-radius: 8px;
|
||
box-shadow: 0 8px 28px var(--shadow); padding: 10px 12px;
|
||
}
|
||
.tip:hover .tipbox, .tip:focus-within .tipbox { display: block; }
|
||
.tipbox table { border-collapse: collapse; width: 100%; font-family: var(--st-mono); font-size: 11.5px; }
|
||
.tipbox th { text-align: left; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--st-faint); padding: 2px 6px 4px; border-bottom: 1px solid var(--st-line-soft); }
|
||
.tipbox td { padding: 3px 6px; color: var(--st-muted); }
|
||
.tipbox td:first-child { color: var(--st-faint); }
|
||
.tipbox .up { color: var(--st-ok); font-weight: 700; }
|
||
.tipbox .dn { color: var(--st-crit); font-weight: 700; }
|
||
.tipbox .ov { color: var(--st-warn); font-weight: 700; }
|
||
.tipbox .meta { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--st-line-soft); font-size: 11px; color: var(--st-faint); }
|
||
|
||
/* ── event log ── */
|
||
.logrow {
|
||
display: grid; grid-template-columns: 130px 74px 150px minmax(0,1fr);
|
||
gap: 4px 14px; align-items: baseline;
|
||
padding: 4px 14px; border-bottom: 1px solid var(--st-line-soft);
|
||
font-family: var(--st-mono); font-size: 11.5px;
|
||
}
|
||
.logrow:last-child { border-bottom: none; }
|
||
.logrow .t { color: var(--st-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||
.logrow .h { color: var(--st-ink); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.logrow .m { color: var(--st-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.filters { display: flex; gap: 6px; margin-left: auto; flex-wrap: wrap; }
|
||
.filters input, .filters select {
|
||
font: 11px var(--st-mono); color: var(--st-ink); background: var(--st-surface);
|
||
border: 1px solid var(--st-line); border-radius: 999px; padding: 3px 10px;
|
||
}
|
||
|
||
.empty { padding: 24px 14px; text-align: center; color: var(--st-faint); font-size: 12.5px; }
|
||
|
||
@media (max-width: 760px) {
|
||
.hhead { display: none; }
|
||
.hrow { grid-template-columns: minmax(0,1fr) auto auto; padding: 8px 12px; }
|
||
.hrow .num { text-align: left; }
|
||
.hrow .since { display: none; }
|
||
.lastalert { grid-column: 1 / -1; }
|
||
.logrow { grid-template-columns: 66px minmax(0,1fr); }
|
||
.logrow .lvlc, .logrow .h { display: none; }
|
||
.tipbox { left: auto; right: 0; }
|
||
}
|
||
</style>
|
||
|
||
<body>
|
||
{% include 'nav.html' %}
|
||
|
||
<div class="st-toolbar">
|
||
<span class="brand">hbd<span class="tld">·live</span></span>
|
||
<span class="hintline">every host, one line — details on hover, history in the overview</span>
|
||
<span class="spacer"></span>
|
||
<input type="text" id="host-filter" placeholder="host filter (regex)">
|
||
<span class="live-dot reconnecting" id="ws-status">connecting…</span>
|
||
</div>
|
||
<svg class="pulse" viewBox="0 0 1200 14" preserveAspectRatio="none" aria-hidden="true">
|
||
<polyline points="0,10 340,10 352,10 358,3 364,13 370,1 378,12 384,10 560,10 572,10 578,3 584,13 590,1 598,12 604,10 1200,10"/>
|
||
</svg>
|
||
|
||
<div class="live-main">
|
||
<section class="st">
|
||
<div class="sec-head">
|
||
<h2>hosts<span class="colon">:</span></h2>
|
||
<span class="count" id="host-counts"></span>
|
||
<span class="sub">sorted by severity, then name</span>
|
||
</div>
|
||
<div class="list">
|
||
<div class="hhead">
|
||
<span>host</span><span>status</span><span class="num">latency</span>
|
||
<span>alerts</span><span>since</span><span>last alert</span>
|
||
</div>
|
||
<div id="host-rows"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="st">
|
||
<div class="sec-head">
|
||
<h2>log<span class="colon">:</span></h2>
|
||
<span class="sub">connectivity and alert events</span>
|
||
<span class="filters">
|
||
<input type="text" id="filter-host" placeholder="host">
|
||
<select id="filter-level">
|
||
<option value="">level</option>
|
||
<option value="info">INFO</option>
|
||
<option value="warning">WARNING</option>
|
||
<option value="critical">CRITICAL</option>
|
||
<option value="recover">RECOVER</option>
|
||
<option value="unknown">UNKNOWN</option>
|
||
</select>
|
||
<input type="text" id="filter-msg" placeholder="message">
|
||
</span>
|
||
</div>
|
||
<div class="list" id="messages">
|
||
<div class="empty">Waiting for events…</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<script>
|
||
var HBD_VERSION = "{{ hbd_version }}";
|
||
var INITIAL_HOSTS = {{ hosts | tojson }};
|
||
var MAX_LOG_ROWS = 400;
|
||
|
||
var hostData = {}; // name -> latest stateinfo payload
|
||
var lastAlert = {}; // name -> {level, message, ts}
|
||
var hostFilterRe = null;
|
||
|
||
function escHtml(s) {
|
||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||
}
|
||
|
||
// ---- derived per-host facts ----------------------------------------
|
||
var STATE_RANK = { down: 3, overdue: 2, up: 1 };
|
||
|
||
function worstConnection(data) {
|
||
var worst = null;
|
||
(data.connections || []).forEach(function (c) {
|
||
if (!c.state || c.state === 'unknown') return;
|
||
if (!worst || (STATE_RANK[c.state] || 0) > (STATE_RANK[worst.state] || 0)) worst = c;
|
||
});
|
||
return worst;
|
||
}
|
||
|
||
function severity(data) {
|
||
var w = worstConnection(data);
|
||
if ((data.alert_critical_unacked || 0) > 0 || (data.alert_critical_acked || 0) > 0) return 'crit';
|
||
if (w && w.state === 'down') return 'crit';
|
||
if ((data.alert_warning_unacked || 0) > 0 || (data.alert_warning_acked || 0) > 0) return 'warn';
|
||
if (w && w.state === 'overdue') return 'warn';
|
||
return 'ok';
|
||
}
|
||
var SEV_ORDER = { crit: 0, warn: 1, ok: 2 };
|
||
|
||
function relTime(ts) {
|
||
if (!ts) return '—';
|
||
var s = Math.max(0, Math.floor(Date.now() / 1000 - ts));
|
||
if (s < 60) return s + 's';
|
||
if (s < 3600) return Math.floor(s / 60) + 'm';
|
||
if (s < 86400) return Math.floor(s / 3600) + 'h ' + Math.floor((s % 3600) / 60) + 'm';
|
||
if (s < 86400 * 30) return Math.floor(s / 86400) + 'd ' + Math.floor((s % 86400) / 3600) + 'h';
|
||
return Math.floor(s / 86400) + 'd';
|
||
}
|
||
|
||
function fmtClock(ts) {
|
||
var d = new Date(ts * 1000);
|
||
function p(n) { return n < 10 ? '0' + n : '' + n; }
|
||
var t = p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
||
return (d.toDateString() === new Date().toDateString())
|
||
? t
|
||
: d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + t;
|
||
}
|
||
|
||
// ---- host row rendering ---------------------------------------------
|
||
function statusChip(data) {
|
||
var w = worstConnection(data);
|
||
if (!w) return '<span class="chip outline">—</span>';
|
||
var cls = w.state === 'up' ? 'ok' : (w.state === 'down' ? 'crit' : 'warn');
|
||
return '<span class="chip ' + cls + '">' + escHtml(w.state) + '</span>';
|
||
}
|
||
|
||
function tipCard(data) {
|
||
var rows = '';
|
||
(data.connections || []).forEach(function (c) {
|
||
if (!c.state || c.state === 'unknown') return;
|
||
var cls = c.state === 'up' ? 'up' : (c.state === 'down' ? 'dn' : 'ov');
|
||
var fam = (c.afam || '').replace('IPv', 'v');
|
||
var rtt = (c.state === 'up' && c.rtts && c.rtts[0]) ? Math.round(c.rtts[0]) + ' ms' : '—';
|
||
rows += '<tr><td>' + escHtml(fam) + '</td><td>' + escHtml(c.addr || '—') + '</td>'
|
||
+ '<td class="' + cls + '">' + escHtml(c.state) + '</td>'
|
||
+ '<td>' + rtt + '</td><td>' + (c.statetime ? fmtClock(c.statetime) : '—') + '</td></tr>';
|
||
});
|
||
if (!rows) rows = '<tr><td colspan="5">no connections yet</td></tr>';
|
||
var meta = [];
|
||
meta.push('agent ' + (data.hbc_version ? 'v' + escHtml(String(data.hbc_version)) : 'unknown'));
|
||
if (data.dyn) meta.push('dyndns');
|
||
if (data.watched) meta.push('watched');
|
||
if (data.interval) meta.push('interval ' + escHtml(String(data.interval)) + ' s');
|
||
return '<span class="tipbox" role="tooltip">'
|
||
+ '<table><thead><tr><th></th><th>address</th><th>state</th><th>rtt</th><th>since</th></tr></thead>'
|
||
+ '<tbody>' + rows + '</tbody></table>'
|
||
+ '<div class="meta">' + meta.join(' · ') + '</div></span>';
|
||
}
|
||
|
||
function alertChips(data) {
|
||
var cu = data.alert_critical_unacked || 0, ca = data.alert_critical_acked || 0;
|
||
var wu = data.alert_warning_unacked || 0, wa = data.alert_warning_acked || 0;
|
||
var out = '';
|
||
if (cu) out += '<span class="chip crit"><b>' + cu + '</b> crit</span> ';
|
||
if (wu) out += '<span class="chip warn"><b>' + wu + '</b> warn</span> ';
|
||
var acked = ca + wa;
|
||
if (acked) out += '<span class="chip outline">' + acked + ' ack’d</span>';
|
||
return out;
|
||
}
|
||
|
||
function lastAlertHtml(name) {
|
||
var a = lastAlert[name];
|
||
if (!a) return '<span class="lastalert quiet">—</span>';
|
||
var lvl = (a.level || '').toUpperCase();
|
||
var quiet = (lvl !== 'WARNING' && lvl !== 'CRITICAL');
|
||
var lvlCls = lvl === 'CRITICAL' ? 'crit' : (lvl === 'WARNING' ? 'warn' : '');
|
||
var short = lvl === 'CRITICAL' ? 'CRIT' : (lvl === 'WARNING' ? 'WARN' : escHtml(lvl));
|
||
return '<span class="lastalert' + (quiet ? ' quiet' : '') + '" title="' + escHtml(a.message) + '">'
|
||
+ '<span class="lvl ' + lvlCls + '">' + short + '</span> '
|
||
+ escHtml(a.message)
|
||
+ (a.ts ? ' <span class="ago">· ' + relTime(a.ts) + ' ago</span>' : '')
|
||
+ '</span>';
|
||
}
|
||
|
||
function renderRow(data) {
|
||
var name = data.name;
|
||
var sev = severity(data);
|
||
var row = document.querySelector('.hrow[data-host="' + CSS.escape(name) + '"]');
|
||
var isNew = false;
|
||
if (!row) {
|
||
row = document.createElement('div');
|
||
row.dataset.host = name;
|
||
isNew = true;
|
||
}
|
||
row.className = 'hrow' + (sev !== 'ok' ? ' sev-' + sev : '');
|
||
row.dataset.sev = sev;
|
||
|
||
var w = worstConnection(data);
|
||
var up = (data.connections || []).find(function (c) { return c.state === 'up' && c.rtts && c.rtts.length; });
|
||
var lat = up ? Math.round(up.rtts[0]) + ' ms' : '—';
|
||
var since = w ? relTime(w.statetime) : '—';
|
||
var staleTag = '';
|
||
if (!data.hbc_version) staleTag = '<span class="stale" title="agent version unknown">agent?</span>';
|
||
else if (data.hbc_version !== HBD_VERSION) {
|
||
staleTag = '<span class="stale" title="agent v' + escHtml(String(data.hbc_version)) + ' ≠ server v' + HBD_VERSION + '">v' + escHtml(String(data.hbc_version)) + '</span>';
|
||
}
|
||
var dotCls = w ? (w.state === 'up' ? sev : (w.state === 'down' ? 'down' : 'overdue')) : 'off';
|
||
|
||
row.innerHTML =
|
||
'<span class="host' + (data.watched ? ' watched' : '') + '">'
|
||
+ '<span class="dot ' + dotCls + '"></span>'
|
||
+ '<a href="/plugins#' + encodeURIComponent(name) + '">' + escHtml(name) + '</a>' + staleTag + '</span>'
|
||
+ '<span class="tip" tabindex="0">' + statusChip(data) + tipCard(data) + '</span>'
|
||
+ '<span class="lat num">' + lat + '</span>'
|
||
+ '<span>' + alertChips(data) + '</span>'
|
||
+ '<span class="since">' + since + '</span>'
|
||
+ lastAlertHtml(name);
|
||
|
||
if (isNew) {
|
||
document.getElementById('host-rows').appendChild(row);
|
||
sortRows();
|
||
}
|
||
applyHostFilter(row);
|
||
}
|
||
|
||
function sortRows() {
|
||
var container = document.getElementById('host-rows');
|
||
var rows = [...container.children];
|
||
rows.sort(function (a, b) {
|
||
var d = (SEV_ORDER[a.dataset.sev] ?? 2) - (SEV_ORDER[b.dataset.sev] ?? 2);
|
||
return d !== 0 ? d : a.dataset.host.localeCompare(b.dataset.host);
|
||
});
|
||
rows.forEach(function (r) { container.appendChild(r); });
|
||
}
|
||
|
||
function updateCounts() {
|
||
var total = 0, up = 0, overdue = 0, down = 0;
|
||
Object.values(hostData).forEach(function (d) {
|
||
total++;
|
||
var w = worstConnection(d);
|
||
if (!w) return;
|
||
if (w.state === 'up') up++;
|
||
else if (w.state === 'overdue') overdue++;
|
||
else if (w.state === 'down') down++;
|
||
});
|
||
var parts = [total + ' hosts', up + ' up'];
|
||
if (overdue) parts.push(overdue + ' overdue');
|
||
if (down) parts.push(down + ' down');
|
||
document.getElementById('host-counts').textContent = parts.join(' · ');
|
||
}
|
||
|
||
function updateHost(data) {
|
||
var prev = hostData[data.name];
|
||
var prevSev = prev ? severity(prev) : null;
|
||
hostData[data.name] = data;
|
||
renderRow(data);
|
||
if (prevSev !== severity(data)) sortRows();
|
||
updateCounts();
|
||
}
|
||
|
||
// ---- host filter -----------------------------------------------------
|
||
function applyHostFilter(row) {
|
||
var rows = row ? [row] : [...document.querySelectorAll('.hrow')];
|
||
rows.forEach(function (r) {
|
||
r.style.display = (!hostFilterRe || hostFilterRe.test(r.dataset.host)) ? '' : 'none';
|
||
});
|
||
}
|
||
document.getElementById('host-filter').addEventListener('input', function () {
|
||
var v = this.value.trim();
|
||
try {
|
||
hostFilterRe = v ? new RegExp(v, 'i') : null;
|
||
this.style.borderColor = '';
|
||
} catch (_) {
|
||
hostFilterRe = null;
|
||
this.style.borderColor = 'var(--st-crit)';
|
||
}
|
||
applyHostFilter();
|
||
});
|
||
|
||
// ---- last alert tracking ---------------------------------------------
|
||
var ALERT_LEVELS = { WARNING: 1, CRITICAL: 1, RECOVER: 1 };
|
||
|
||
function noteAlert(host, level, message, ts) {
|
||
if (!host || !ALERT_LEVELS[(level || '').toUpperCase()]) return;
|
||
var cur = lastAlert[host];
|
||
if (cur && cur.ts && ts && ts < cur.ts) return; // keep the newest
|
||
lastAlert[host] = { level: level, message: message, ts: ts };
|
||
if (hostData[host]) renderRow(hostData[host]);
|
||
}
|
||
|
||
async function seedActiveAlerts() {
|
||
try {
|
||
var r = await fetch('/api/0/alerts');
|
||
if (!r.ok) return;
|
||
var data = await r.json();
|
||
(data.alerts || []).forEach(function (a) {
|
||
var metric = (a.metric_path.includes('.')
|
||
? a.metric_path.slice(a.metric_path.indexOf('.') + 1)
|
||
: a.metric_path).replace(/_status_code$/, '');
|
||
var msg = metric + ' = ' + a.last_value
|
||
+ (a.formatted_message ? ' ' + a.formatted_message
|
||
: (a.operator && a.threshold_value != null ? ' (' + a.operator + ' ' + a.threshold_value + ')' : ''));
|
||
noteAlert(a.hostname, a.level, msg, a.since);
|
||
});
|
||
} catch (_) { /* dashboard still works without the seed */ }
|
||
}
|
||
|
||
// ---- event log ---------------------------------------------------------
|
||
function logRowHtml(msg) {
|
||
var lvl = (msg.level || 'INFO');
|
||
var lc = lvl.toLowerCase();
|
||
var chipCls = lc === 'critical' ? 'crit' : (lc === 'warning' ? 'warn' : (lc === 'recover' ? 'ok' : ''));
|
||
var short = lc === 'critical' ? 'CRIT' : (lc === 'warning' ? 'WARN' : escHtml(lvl));
|
||
return '<div class="logrow" data-level="' + escHtml(lc) + '" data-host="' + escHtml(msg.host || '') + '">'
|
||
+ '<span class="t">' + fmtClock(msg.ts) + '</span>'
|
||
+ '<span class="lvlc"><span class="chip ' + chipCls + '">' + short + '</span></span>'
|
||
+ '<span class="h">' + escHtml(msg.host || '') + (msg.service ? ' · ' + escHtml(msg.service) : '') + '</span>'
|
||
+ '<span class="m">' + escHtml(msg.message || '') + '</span>'
|
||
+ '</div>';
|
||
}
|
||
|
||
function addLogRow(msg, isHistory) {
|
||
var box = document.getElementById('messages');
|
||
var placeholder = box.querySelector('.empty');
|
||
if (placeholder) placeholder.remove();
|
||
box.insertAdjacentHTML(isHistory ? 'beforeend' : 'afterbegin', logRowHtml(msg));
|
||
while (box.children.length > MAX_LOG_ROWS) box.lastElementChild.remove();
|
||
applyLogFilters();
|
||
}
|
||
|
||
function applyLogFilters() {
|
||
var hostF = document.getElementById('filter-host').value.toLowerCase().trim();
|
||
var levelF = document.getElementById('filter-level').value;
|
||
var msgF = document.getElementById('filter-msg').value.toLowerCase().trim();
|
||
document.querySelectorAll('#messages .logrow').forEach(function (entry) {
|
||
var show = true;
|
||
if (hostF && !(entry.dataset.host || '').toLowerCase().includes(hostF)) show = false;
|
||
if (levelF && entry.dataset.level !== levelF) show = false;
|
||
if (msgF) {
|
||
var m = entry.querySelector('.m');
|
||
if (!m || !m.textContent.toLowerCase().includes(msgF)) show = false;
|
||
}
|
||
entry.style.display = show ? '' : 'none';
|
||
});
|
||
}
|
||
document.getElementById('filter-host').addEventListener('input', applyLogFilters);
|
||
document.getElementById('filter-level').addEventListener('change', applyLogFilters);
|
||
document.getElementById('filter-msg').addEventListener('input', applyLogFilters);
|
||
|
||
// ---- websocket -----------------------------------------------------------
|
||
function setWsStatus(ok) {
|
||
var el = document.getElementById('ws-status');
|
||
el.classList.toggle('reconnecting', !ok);
|
||
el.textContent = ok ? 'live' : 'reconnecting…';
|
||
}
|
||
|
||
function WS_Connect() {
|
||
if (!("WebSocket" in window)) {
|
||
console.log("WebSocket NOT supported by your Browser!");
|
||
return;
|
||
}
|
||
var ws_hbd = new WebSocket("{{ heartbeat_ws_url }}");
|
||
|
||
ws_hbd.onopen = function () {
|
||
setWsStatus(true);
|
||
ws_hbd.send("heartbeat_web");
|
||
};
|
||
|
||
ws_hbd.onerror = function (event) { console.log(event); };
|
||
|
||
ws_hbd.onmessage = function (event) {
|
||
var state = JSON.parse(event.data);
|
||
if (state.type === "host") {
|
||
updateHost(state.data);
|
||
} else if (state.type === "message") {
|
||
var msg = state.data;
|
||
addLogRow(msg, !!state.history);
|
||
noteAlert(msg.host, msg.level, msg.message, msg.ts);
|
||
}
|
||
};
|
||
|
||
ws_hbd.onclose = function () {
|
||
setWsStatus(false);
|
||
setTimeout(WS_Connect, 3000);
|
||
};
|
||
}
|
||
|
||
// ---- boot -----------------------------------------------------------------
|
||
INITIAL_HOSTS.forEach(function (h) { hostData[h.name] = h; });
|
||
Object.values(hostData).forEach(renderRow);
|
||
sortRows();
|
||
updateCounts();
|
||
seedActiveAlerts();
|
||
WS_Connect();
|
||
|
||
// refresh relative times once a minute
|
||
setInterval(function () {
|
||
Object.values(hostData).forEach(renderRow);
|
||
}, 60000);
|
||
</script>
|
||
</body>
|
||
</html>
|