feat: add CPU usage history graph to CPU Monitor section
Renders an SVG line chart above the CPU Usage row using all available history samples (up to 100). Color adapts green/orange/red by load level. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -873,7 +873,7 @@
|
|||||||
let html = '';
|
let html = '';
|
||||||
switch (pluginName) {
|
switch (pluginName) {
|
||||||
case 'os_info': html = renderOsInfoTable(cached.data); break;
|
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 'memory_monitor': html = renderMemoryTable(cached.data); break;
|
||||||
case 'disk_monitor': html = renderDiskTables(cached.data); break;
|
case 'disk_monitor': html = renderDiskTables(cached.data); break;
|
||||||
case 'network_monitor':html = renderNetworkTables(cached.data); break;
|
case 'network_monitor':html = renderNetworkTables(cached.data); break;
|
||||||
@@ -885,6 +885,10 @@
|
|||||||
|
|
||||||
html += `<div class="timestamp">Last updated: ${new Date(cached.timestamp * 1000).toLocaleString()}</div>`;
|
html += `<div class="timestamp">Last updated: ${new Date(cached.timestamp * 1000).toLocaleString()}</div>`;
|
||||||
body.innerHTML = html;
|
body.innerHTML = html;
|
||||||
|
|
||||||
|
if (pluginName === 'cpu_monitor') {
|
||||||
|
fetchCpuHistory(hostname).then(samples => renderCpuChart(hostname, samples)).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Per-plugin renderers ────────────────────────────────────────────────
|
// ── Per-plugin renderers ────────────────────────────────────────────────
|
||||||
@@ -907,7 +911,78 @@
|
|||||||
return html;
|
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 += `<line x1="${PAD.left}" y1="${yy}" x2="${PAD.left + cW}" y2="${yy}" stroke="#e0e0e0" stroke-width="1"/>`;
|
||||||
|
gridLines += `<text x="${(PAD.left - 3).toFixed(1)}" y="${yy}" text-anchor="end" dominant-baseline="middle" font-size="8" fill="#999">${pct}</text>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-axis time labels
|
||||||
|
const fmt = ts => {
|
||||||
|
const d = new Date(ts * 1000);
|
||||||
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
};
|
||||||
|
const xLabels = `
|
||||||
|
<text x="${PAD.left}" y="${H - 2}" text-anchor="start" font-size="8" fill="#999">${fmt(pts[0].t)}</text>
|
||||||
|
<text x="${PAD.left + cW}" y="${H - 2}" text-anchor="end" font-size="8" fill="#999">${fmt(pts[pts.length-1].t)}</text>`;
|
||||||
|
|
||||||
|
el.innerHTML = `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none"
|
||||||
|
style="width:100%;height:${H}px;display:block;">
|
||||||
|
<defs>
|
||||||
|
<clipPath id="cpu-clip-${hostname}">
|
||||||
|
<rect x="${PAD.left}" y="${PAD.top}" width="${cW}" height="${cH}"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
${gridLines}
|
||||||
|
<line x1="${PAD.left}" y1="${PAD.top}" x2="${PAD.left}" y2="${PAD.top + cH}" stroke="#ccc" stroke-width="1"/>
|
||||||
|
<line x1="${PAD.left}" y1="${PAD.top + cH}" x2="${PAD.left + cW}" y2="${PAD.top + cH}" stroke="#ccc" stroke-width="1"/>
|
||||||
|
<g clip-path="url(#cpu-clip-${hostname})">
|
||||||
|
<path d="${areaPath}" fill="${fillColor}" opacity="0.6"/>
|
||||||
|
<polyline points="${linePoints}" fill="none" stroke="${strokeColor}" stroke-width="1.5" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
${xLabels}
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCpuTable(hostname, d) {
|
||||||
const KEYS = [
|
const KEYS = [
|
||||||
['cpu_percent', 'CPU Usage', 'bar'],
|
['cpu_percent', 'CPU Usage', 'bar'],
|
||||||
['load_1min', 'Load (1 min)', 'num'],
|
['load_1min', 'Load (1 min)', 'num'],
|
||||||
@@ -925,7 +1000,8 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handled = new Set(KEYS.map(r => r[0]));
|
const handled = new Set(KEYS.map(r => r[0]));
|
||||||
let html = '<table class="data-table"><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody>';
|
let html = `<div id="cpu-chart-${hostname}" style="margin-bottom:8px;"></div>`;
|
||||||
|
html += '<table class="data-table"><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody>';
|
||||||
for (const [k, label, fmt] of KEYS) {
|
for (const [k, label, fmt] of KEYS) {
|
||||||
if (!(k in d)) continue;
|
if (!(k in d)) continue;
|
||||||
const v = d[k];
|
const v = d[k];
|
||||||
|
|||||||
Reference in New Issue
Block a user