feat(ui): show host status, simplex and absent protocols

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uc9V1v7iXruubpoLiSE24M
This commit is contained in:
2026-09-21 09:36:25 -04:00
co-authored by Claude Sonnet 5
parent 3970f4cdde
commit 8122c8d7dc
2 changed files with 37 additions and 16 deletions
+18 -15
View File
@@ -73,6 +73,7 @@
.tipbox .up { color: var(--st-ok); font-weight: 700; } .tipbox .up { color: var(--st-ok); font-weight: 700; }
.tipbox .dn { color: var(--st-crit); font-weight: 700; } .tipbox .dn { color: var(--st-crit); font-weight: 700; }
.tipbox .ov { color: var(--st-warn); font-weight: 700; } .tipbox .ov { color: var(--st-warn); font-weight: 700; }
.tipbox .ab { color: var(--st-faint); 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); } .tipbox .meta { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--st-line-soft); font-size: 11px; color: var(--st-faint); }
@media (max-width: 760px) { @media (max-width: 760px) {
@@ -134,18 +135,18 @@
function worstConnection(data) { function worstConnection(data) {
var worst = null; var worst = null;
(data.connections || []).forEach(function (c) { (data.connections || []).forEach(function (c) {
if (!c.state || c.state === 'unknown') return; if (!c.state || c.state === 'unknown' || c.state === 'absent') return;
if (!worst || (STATE_RANK[c.state] || 0) > (STATE_RANK[worst.state] || 0)) worst = c; if (!worst || (STATE_RANK[c.state] || 0) > (STATE_RANK[worst.state] || 0)) worst = c;
}); });
return worst; return worst;
} }
function severity(data) { function severity(data) {
var w = worstConnection(data); var st = data.status;
if ((data.alert_critical_unacked || 0) > 0 || (data.alert_critical_acked || 0) > 0) return 'crit'; if ((data.alert_critical_unacked || 0) > 0 || (data.alert_critical_acked || 0) > 0) return 'crit';
if (w && w.state === 'down') return 'crit'; if (st === 'down') return 'crit';
if ((data.alert_warning_unacked || 0) > 0 || (data.alert_warning_acked || 0) > 0) return 'warn'; if ((data.alert_warning_unacked || 0) > 0 || (data.alert_warning_acked || 0) > 0) return 'warn';
if (w && w.state === 'overdue') return 'warn'; if (st === 'overdue') return 'warn';
return 'ok'; return 'ok';
} }
var SEV_ORDER = { crit: 0, warn: 1, ok: 2 }; var SEV_ORDER = { crit: 0, warn: 1, ok: 2 };
@@ -171,17 +172,17 @@
// ---- host row rendering --------------------------------------------- // ---- host row rendering ---------------------------------------------
function statusChip(data) { function statusChip(data) {
var w = worstConnection(data); var st = data.status;
if (!w) return '<span class="chip outline">—</span>'; if (!st || st === 'unknown') return '<span class="chip outline">—</span>';
var cls = w.state === 'up' ? 'ok' : (w.state === 'down' ? 'crit' : 'warn'); var cls = (st === 'up' || st === 'simplex') ? 'ok' : (st === 'down' ? 'crit' : 'warn');
return '<span class="chip ' + cls + '">' + escHtml(w.state) + '</span>'; return '<span class="chip ' + cls + '">' + escHtml(st) + '</span>';
} }
function tipCard(data) { function tipCard(data) {
var rows = ''; var rows = '';
(data.connections || []).forEach(function (c) { (data.connections || []).forEach(function (c) {
if (!c.state || c.state === 'unknown') return; if (!c.state || c.state === 'unknown') return;
var cls = c.state === 'up' ? 'up' : (c.state === 'down' ? 'dn' : 'ov'); var cls = c.state === 'up' ? 'up' : (c.state === 'down' ? 'dn' : (c.state === 'absent' ? 'ab' : 'ov'));
var fam = (c.afam || '').replace('IPv', 'v'); var fam = (c.afam || '').replace('IPv', 'v');
var rtt = (c.state === 'up' && c.rtts && c.rtts[0]) ? Math.round(c.rtts[0]) + ' ms' : '—'; 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>' rows += '<tr><td>' + escHtml(fam) + '</td><td>' + escHtml(c.addr || '—') + '</td>'
@@ -247,7 +248,9 @@
else if (data.hbc_version !== HBD_VERSION) { 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>'; 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'; var st = data.status;
var dotCls = (!st || st === 'unknown') ? 'off'
: ((st === 'up' || st === 'simplex') ? sev : (st === 'down' ? 'down' : 'overdue'));
var flapTag = ''; var flapTag = '';
var flapping = data.flapping || []; var flapping = data.flapping || [];
if (flapping.length) { if (flapping.length) {
@@ -286,11 +289,11 @@
var total = 0, up = 0, overdue = 0, down = 0; var total = 0, up = 0, overdue = 0, down = 0;
Object.values(hostData).forEach(function (d) { Object.values(hostData).forEach(function (d) {
total++; total++;
var w = worstConnection(d); var st = d.status;
if (!w) return; if (!st || st === 'unknown') return;
if (w.state === 'up') up++; if (st === 'up' || st === 'simplex') up++;
else if (w.state === 'overdue') overdue++; else if (st === 'overdue') overdue++;
else if (w.state === 'down') down++; else if (st === 'down') down++;
}); });
var parts = [total + ' hosts', up + ' up']; var parts = [total + ' hosts', up + ' up'];
if (overdue) parts.push(overdue + ' overdue'); if (overdue) parts.push(overdue + ' overdue');
+19 -1
View File
@@ -456,7 +456,7 @@
</tr></thead><tbody>`; </tr></thead><tbody>`;
for (const c of data.connections) { for (const c of data.connections) {
const st = escHtml(c.state || '—'); const st = escHtml(c.state || '—');
const stCls = c.state === 'up' ? 'status-up' : (c.state ? 'status-down' : ''); const stCls = c.state === 'up' ? 'status-up' : (c.state === 'absent' ? '' : (c.state ? 'status-down' : ''));
const rtt = (c.state === 'up' && c.rtt) ? Math.round(c.rtt) + ' ms' : '—'; const rtt = (c.state === 'up' && c.rtt) ? Math.round(c.rtt) + ' ms' : '—';
const chg = c.statetime ? new Date(c.statetime * 1000).toLocaleString() : '—'; const chg = c.statetime ? new Date(c.statetime * 1000).toLocaleString() : '—';
const seen = c.lastbeat ? new Date(c.lastbeat * 1000).toLocaleString() : '—'; const seen = c.lastbeat ? new Date(c.lastbeat * 1000).toLocaleString() : '—';
@@ -1143,6 +1143,24 @@
function renderNetworkTables(d) { function renderNetworkTables(d) {
let html = ''; let html = '';
if (d.connectivity) {
let t = '<table class="data-table"><thead><tr><th>Protocol</th><th>Internet</th>'
+ '<th>Address</th><th>Gateway</th><th class="num">RTT</th></tr></thead><tbody>';
for (const fam of ['ipv4', 'ipv6']) {
const c = d.connectivity[fam];
if (!c) continue;
const known = c.present === true || c.present === false;
const cls = c.present === true ? 'status-up' : '';
const txt = c.present === true ? '✓ present' : (c.present === false ? 'absent' : '?');
t += `<tr><td>${fam === 'ipv4' ? 'IPv4' : 'IPv6'}</td>
<td class="${cls}">${txt}</td>
<td>${escHtml(c.addr || '—')}</td><td>${escHtml(c.gateway || '—')}</td>
<td class="num">${known && c.rtt != null ? Math.round(c.rtt) + ' ms' : '—'}</td></tr>`;
}
t += '</tbody></table>';
html += `<div class="table-section-label">Connectivity</div>${t}`;
}
if (d.interfaces && Object.keys(d.interfaces).length) { if (d.interfaces && Object.keys(d.interfaces).length) {
const sample = Object.values(d.interfaces)[0]; const sample = Object.values(d.interfaces)[0];
const hasDelta = 'bytes_sent_delta' in sample; const hasDelta = 'bytes_sent_delta' in sample;