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
-
-
-
-
-
-
- | Name |
- ⚠️ |
- 🔴 |
- IPv4 Addr |
- State |
- Latency |
- Last State |
- IPv6 Addr |
- State |
- Latency |
- Last State |
-
-
-
- {% for host in hosts %}
-
- | {{ 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 -%}
- |
- {% for conn in host.connections %}
- {{ 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 '' }} |
- {% endfor %}
- {% if host.connections|length == 0 %}
- | | | |
- | | | |
- {% elif host.connections|length == 1 %}
- | | | |
- {% endif %}
-
- {% endfor %}
-
-
-
-
-
-
-
- {% 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
+
+ | Family | Address | State |
+ RTT | Last Change | Last Packet |
+
`;
+ 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 += `
+ | ${escHtml(c.family)} |
+ ${escHtml(c.addr || '—')} |
+ ${st} |
+ ${rtt} |
+ ${chg} |
+ ${seen} |
+
`;
+ }
+ html += `
`;
+ }
+
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"] == []