From 8d268adf70bc101b5f89fb859897f8c8b6415e25 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Fri, 10 Jul 2026 13:45:03 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20Live=20Dashboard=20redesign=20=E2=80=94?= =?UTF-8?q?=206=20columns,=20hover=20details,=20last-alert=20column?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU --- hbd/server/http.py | 13 + hbd/server/templates/live.html | 1129 ++++++++++++----------------- hbd/server/templates/plugins.html | 24 + tests/test_http_host_info.py | 28 + 4 files changed, 524 insertions(+), 670 deletions(-) diff --git a/hbd/server/http.py b/hbd/server/http.py index ca3d0cf..6108282 100644 --- a/hbd/server/http.py +++ b/hbd/server/http.py @@ -258,12 +258,25 @@ def _build_host_info(host, threshold_checker=None) -> dict: 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 { "owner": getattr(host, "owner", None), "managers": list(getattr(host, "managers", [])), "hbc_version": hbc_version, "hbc_type": hbc_type, "last_packet": last_packet, + "connections": connections, "thresholds": thresholds, } diff --git a/hbd/server/templates/live.html b/hbd/server/templates/live.html index 2f2c2b8..001bf46 100644 --- a/hbd/server/templates/live.html +++ b/hbd/server/templates/live.html @@ -1,699 +1,488 @@ {% include 'head.html' %} + + - {% include 'nav.html' %} - {% include 'menu.html' %} +
+ hbd·live + every host, one line — details on hover, history in the overview + + + connecting… +
+ -
-
-

{{ header }}

-

Real-time host monitoring and event log

-
- -
- - - - - - - - - - - - - - - - - - {% for host in hosts %} - - - - - {% for conn in host.connections %} - - - - - {% endfor %} - {% if host.connections|length == 0 %} - - - {% elif host.connections|length == 1 %} - - {% endif %} - - {% endfor %} - -
Name⚠️🔴IPv4 AddrStateLatencyLast StateIPv6 AddrStateLatencyLast State
{{ host.name }}{% if not host.hbc_version or host.hbc_version != hbd_version %} 🥀{% endif %} - {%- set warning_unacked = host.alert_warning_unacked -%} - {%- set warning_acked = host.alert_warning_acked -%} - {%- if warning_unacked > 0 or warning_acked > 0 -%} - {{ warning_unacked }}{% if warning_acked > 0 %}/{{ warning_acked }}{% endif %} - {%- endif -%} - - {%- set critical_unacked = host.alert_critical_unacked -%} - {%- set critical_acked = host.alert_critical_acked -%} - {%- if critical_unacked > 0 or critical_acked > 0 -%} - {{ critical_unacked }}{% if critical_acked > 0 %}/{{ critical_acked }}{% endif %} - {%- endif -%} - {{ conn.addr if conn.addr else '' }}{{ conn.state if conn.state else '' }}{{ conn.latency if conn.latency else '' }}{{ conn.last_state_ts if conn.last_state_ts else '' }}
-
- -
-
- Log of Events -
- - + - -
+ +
-
-
+
+
Waiting for events…
+
+
- - {% include 'foot.html' %} - - -
-
-

⚠️ Connection is closed, reopening...

-
-
- + diff --git a/hbd/server/templates/plugins.html b/hbd/server/templates/plugins.html index 5503732..917d82b 100644 --- a/hbd/server/templates/plugins.html +++ b/hbd/server/templates/plugins.html @@ -444,6 +444,30 @@ Last Packet${lastPkt} `; + if (data.connections && data.connections.length) { + html += `
Connectivity
+ + + + `; + 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 += ` + + + + + + + `; + } + html += `
FamilyAddressStateRTTLast ChangeLast Packet
${escHtml(c.family)}${escHtml(c.addr || '—')}${st}${rtt}${chg}${seen}
`; + } + if (data.thresholds === null) { html += `
Threshold alerting not configured.
`; } else if (data.thresholds.length === 0) { diff --git a/tests/test_http_host_info.py b/tests/test_http_host_info.py index 9a5df61..c38915d 100644 --- a/tests/test_http_host_info.py +++ b/tests/test_http_host_info.py @@ -172,3 +172,31 @@ def test_build_host_info_covers_empty_when_exact_matches_only(): result = _build_host_info(host, threshold_checker=checker) t = result["thresholds"][0] 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"] == []