diff --git a/hbd/server/templates/plugins.html b/hbd/server/templates/plugins.html
index 6f164d0..12165c5 100644
--- a/hbd/server/templates/plugins.html
+++ b/hbd/server/templates/plugins.html
@@ -873,7 +873,7 @@
let html = '';
switch (pluginName) {
case 'os_info': html = renderOsInfoTable(cached.data); break;
- case 'cpu_monitor': html = renderCpuTable(cached.data); break;
+ case 'cpu_monitor': html = renderCpuTable(hostname, cached.data); break;
case 'memory_monitor': html = renderMemoryTable(cached.data); break;
case 'disk_monitor': html = renderDiskTables(cached.data); break;
case 'network_monitor':html = renderNetworkTables(cached.data); break;
@@ -885,6 +885,10 @@
html += `
Last updated: ${new Date(cached.timestamp * 1000).toLocaleString()}
`;
body.innerHTML = html;
+
+ if (pluginName === 'cpu_monitor') {
+ fetchCpuHistory(hostname).then(samples => renderCpuChart(hostname, samples)).catch(() => {});
+ }
}
// ── Per-plugin renderers ────────────────────────────────────────────────
@@ -907,7 +911,78 @@
return html;
}
- function renderCpuTable(d) {
+ async function fetchCpuHistory(hostname) {
+ const r = await fetch(`/api/0/hosts/${encodeURIComponent(hostname)}/plugins/cpu_monitor?limit=100`);
+ if (!r.ok) return [];
+ const json = await r.json();
+ return json.samples || [];
+ }
+
+ function renderCpuChart(hostname, samples) {
+ const el = document.getElementById(`cpu-chart-${hostname}`);
+ if (!el || !samples.length) return;
+
+ const pts = samples
+ .filter(s => s.data.cpu_percent != null)
+ .map(s => ({ t: s.timestamp, v: s.data.cpu_percent }));
+ if (pts.length < 2) { el.style.display = 'none'; return; }
+
+ const W = 600, H = 80, PAD = { top: 6, right: 8, bottom: 18, left: 28 };
+ const cW = W - PAD.left - PAD.right;
+ const cH = H - PAD.top - PAD.bottom;
+
+ const tMin = pts[0].t, tMax = pts[pts.length - 1].t;
+ const tRange = tMax - tMin || 1;
+ const x = t => PAD.left + ((t - tMin) / tRange) * cW;
+ const y = v => PAD.top + cH - (Math.min(v, 100) / 100) * cH;
+
+ // Build polyline points and filled area path
+ const linePoints = pts.map(p => `${x(p.t).toFixed(1)},${y(p.v).toFixed(1)}`).join(' ');
+ const areaPath = `M${x(pts[0].t).toFixed(1)},${(PAD.top + cH).toFixed(1)} ` +
+ pts.map(p => `L${x(p.t).toFixed(1)},${y(p.v).toFixed(1)}`).join(' ') +
+ ` L${x(pts[pts.length-1].t).toFixed(1)},${(PAD.top + cH).toFixed(1)} Z`;
+
+ // Color based on latest value
+ const latest = pts[pts.length - 1].v;
+ const strokeColor = latest > 90 ? '#e53935' : latest > 70 ? '#fb8c00' : '#43a047';
+ const fillColor = latest > 90 ? '#ffcdd2' : latest > 70 ? '#ffe0b2' : '#c8e6c9';
+
+ // Y-axis grid lines at 25, 50, 75, 100
+ let gridLines = '';
+ for (const pct of [25, 50, 75, 100]) {
+ const yy = y(pct).toFixed(1);
+ gridLines += ` `;
+ gridLines += `${pct} `;
+ }
+
+ // X-axis time labels
+ const fmt = ts => {
+ const d = new Date(ts * 1000);
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ };
+ const xLabels = `
+ ${fmt(pts[0].t)}
+ ${fmt(pts[pts.length-1].t)} `;
+
+ el.innerHTML = `
+
+
+
+
+
+ ${gridLines}
+
+
+
+
+
+
+ ${xLabels}
+ `;
+ }
+
+ function renderCpuTable(hostname, d) {
const KEYS = [
['cpu_percent', 'CPU Usage', 'bar'],
['load_1min', 'Load (1 min)', 'num'],
@@ -925,7 +1000,8 @@
];
const handled = new Set(KEYS.map(r => r[0]));
- let html = 'Metric Value ';
+ let html = `
`;
+ html += 'Metric Value ';
for (const [k, label, fmt] of KEYS) {
if (!(k in d)) continue;
const v = d[k];