feat: Live Dashboard redesign — 6 columns, hover details, last-alert column
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
This commit is contained in:
@@ -258,12 +258,25 @@ def _build_host_info(host, threshold_checker=None) -> dict:
|
|||||||
key=lambda x: x["metric"],
|
key=lambda x: x["metric"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
connections = [
|
||||||
|
{
|
||||||
|
"family": getattr(conn, "afam", family),
|
||||||
|
"addr": getattr(conn, "addr", ""),
|
||||||
|
"state": getattr(conn, "state", ""),
|
||||||
|
"rtt": (getattr(conn, "rtts", None) or [None])[0],
|
||||||
|
"statetime": getattr(conn, "statetime", None),
|
||||||
|
"lastbeat": getattr(conn, "lastbeat", None),
|
||||||
|
}
|
||||||
|
for family, conn in sorted(host.connections.items())
|
||||||
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"owner": getattr(host, "owner", None),
|
"owner": getattr(host, "owner", None),
|
||||||
"managers": list(getattr(host, "managers", [])),
|
"managers": list(getattr(host, "managers", [])),
|
||||||
"hbc_version": hbc_version,
|
"hbc_version": hbc_version,
|
||||||
"hbc_type": hbc_type,
|
"hbc_type": hbc_type,
|
||||||
"last_packet": last_packet,
|
"last_packet": last_packet,
|
||||||
|
"connections": connections,
|
||||||
"thresholds": thresholds,
|
"thresholds": thresholds,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+453
-664
File diff suppressed because it is too large
Load Diff
@@ -444,6 +444,30 @@
|
|||||||
<span class="info-label">Last Packet</span><span class="info-value">${lastPkt}</span>
|
<span class="info-label">Last Packet</span><span class="info-value">${lastPkt}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
if (data.connections && data.connections.length) {
|
||||||
|
html += `<div class="info-thresholds-title">Connectivity</div>
|
||||||
|
<table class="data-table"><thead><tr>
|
||||||
|
<th>Family</th><th>Address</th><th>State</th>
|
||||||
|
<th class="num">RTT</th><th>Last Change</th><th>Last Packet</th>
|
||||||
|
</tr></thead><tbody>`;
|
||||||
|
for (const c of data.connections) {
|
||||||
|
const st = escHtml(c.state || '—');
|
||||||
|
const stCls = c.state === 'up' ? 'status-up' : (c.state ? 'status-down' : '');
|
||||||
|
const rtt = (c.state === 'up' && c.rtt) ? Math.round(c.rtt) + ' ms' : '—';
|
||||||
|
const chg = c.statetime ? new Date(c.statetime * 1000).toLocaleString() : '—';
|
||||||
|
const seen = c.lastbeat ? new Date(c.lastbeat * 1000).toLocaleString() : '—';
|
||||||
|
html += `<tr>
|
||||||
|
<td class="key">${escHtml(c.family)}</td>
|
||||||
|
<td>${escHtml(c.addr || '—')}</td>
|
||||||
|
<td><span class="${stCls}">${st}</span></td>
|
||||||
|
<td class="num">${rtt}</td>
|
||||||
|
<td>${chg}</td>
|
||||||
|
<td>${seen}</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
html += `</tbody></table>`;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.thresholds === null) {
|
if (data.thresholds === null) {
|
||||||
html += `<div class="info-note">Threshold alerting not configured.</div>`;
|
html += `<div class="info-note">Threshold alerting not configured.</div>`;
|
||||||
} else if (data.thresholds.length === 0) {
|
} else if (data.thresholds.length === 0) {
|
||||||
|
|||||||
@@ -172,3 +172,31 @@ def test_build_host_info_covers_empty_when_exact_matches_only():
|
|||||||
result = _build_host_info(host, threshold_checker=checker)
|
result = _build_host_info(host, threshold_checker=checker)
|
||||||
t = result["thresholds"][0]
|
t = result["thresholds"][0]
|
||||||
assert t["covers"] == []
|
assert t["covers"] == []
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeConnFull:
|
||||||
|
def __init__(self, afam, addr, state, rtt, statetime, lastbeat):
|
||||||
|
self.afam = afam
|
||||||
|
self.addr = addr
|
||||||
|
self.state = state
|
||||||
|
self.rtts = [rtt]
|
||||||
|
self.statetime = statetime
|
||||||
|
self.lastbeat = lastbeat
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_host_info_includes_connections():
|
||||||
|
host = _FakeHost(connections={
|
||||||
|
"IPv4": _FakeConnFull("IPv4", "10.0.0.5", "up", 14.2, 1000.0, 2000.0),
|
||||||
|
"IPv6": _FakeConnFull("IPv6", "fd00::5", "overdue", 0, 1500.0, 1900.0),
|
||||||
|
})
|
||||||
|
result = _build_host_info(host)
|
||||||
|
conns = {c["family"]: c for c in result["connections"]}
|
||||||
|
assert conns["IPv4"] == {"family": "IPv4", "addr": "10.0.0.5", "state": "up",
|
||||||
|
"rtt": 14.2, "statetime": 1000.0, "lastbeat": 2000.0}
|
||||||
|
assert conns["IPv6"]["state"] == "overdue"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_host_info_connections_empty():
|
||||||
|
host = _FakeHost()
|
||||||
|
result = _build_host_info(host)
|
||||||
|
assert result["connections"] == []
|
||||||
|
|||||||
Reference in New Issue
Block a user