diff --git a/hbd/server/templates/plugins.html b/hbd/server/templates/plugins.html
index 94f3659..ab6d577 100644
--- a/hbd/server/templates/plugins.html
+++ b/hbd/server/templates/plugins.html
@@ -445,8 +445,12 @@
`;
if (data.connections && data.connections.length) {
- html += `
Connectivity
-
+ html += `Connectivity
`;
+ for (const c of data.connections) {
+ html += `${escHtml(c.family)} · ${escHtml(c.addr || '—')} RTT
+ `;
+ }
+ html += `
| Family | Address | State |
RTT | Last Change | Last Packet |
`;
@@ -495,6 +499,15 @@
}
el.innerHTML = html;
+
+ if (data.connections && data.connections.length) {
+ const rttThresholds = (data.thresholds || []).find(t => t.metric === 'rtt') || null;
+ for (const c of data.connections) {
+ fetchRttHistory(hostname, c.family)
+ .then(samples => renderRttChart(hostname, c.family, samples, rttThresholds))
+ .catch(() => {});
+ }
+ }
}
async function fetchHostGlance(hostname) {
@@ -899,6 +912,35 @@
});
}
+ async function fetchRttHistory(hostname, family) {
+ const plugin = `rtt_${family.toLowerCase()}`;
+ const r = await fetch(`/api/0/hosts/${encodeURIComponent(hostname)}/plugins/${plugin}?limit=100`);
+ if (!r.ok) return [];
+ const json = await r.json();
+ return json.samples || [];
+ }
+
+ function renderRttChart(hostname, family, samples, rttThresholds) {
+ const pts = samples
+ .filter(s => s.data.rtt != null && s.data.rtt > 0)
+ .map(s => ({ t: s.timestamp, v: s.data.rtt }));
+
+ renderTimeSeriesChart(`rtt-chart-${hostname}-${family}`, pts, {
+ yDomain: 'auto',
+ clipId: `rtt-clip-${hostname}-${family}`,
+ unitSuffix: ' ms',
+ colorFor: (latest) => {
+ if (rttThresholds?.critical != null && latest > rttThresholds.critical) {
+ return { stroke: '#e53935', fill: '#ffcdd2' };
+ }
+ if (rttThresholds?.warning != null && latest > rttThresholds.warning) {
+ return { stroke: '#fb8c00', fill: '#ffe0b2' };
+ }
+ return { stroke: '#1976d2', fill: '#bbdefb' };
+ },
+ });
+ }
+
function renderCpuTable(hostname, d) {
const KEYS = [
['cpu_percent', 'CPU Usage', 'bar'],