9e4f1983f8
The full-metrics page rendered once server-side with the history query inline, so the charts blocked first paint and nothing refreshed without a reload (it reads the latest snapshot the server holds — the agent pushes ~every 30s). Split it into a shell + two HTMX fragments: - Shell (host_detail.html): header + shared time-range toggle + two containers; paints instantly. - Current state (/<id>/metrics → _host_metrics.html): identity + gauges + per-core + filesystems + interfaces/disks + temps, from the DISTINCT ON latest query. hx-trigger "load, every 15s" → live numbers at ~the agent cadence. - History charts (/<id>/charts → _host_charts.html): the 3 charts + Chart.js, from the date_bin history query. hx-trigger "load, every 60s, rangeChange" so they lazy-load (never block paint), refresh slowly, and follow the range selector. Leak-safe: previous Chart instances are destroyed before re-render. - Range toggle switched from full-reload links to the shared _time_range.html (setTimeRange + rangeChange), so only the fragments refetch. - routes: host_detail (shell) + host_detail_metrics + host_detail_charts, with a _split_host_metrics helper. - tests/test_templates_parse.py now also parses plugin templates (these fragments aren't rendered in the unit lane). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
71 lines
3.3 KiB
HTML
71 lines
3.3 KiB
HTML
{# History-charts fragment for the full-metrics page — lazy-loaded via HTMX so
|
|
the date_bin history query never blocks the page shell, and refreshed on a
|
|
slow cadence / range change. #}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:1rem;">
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Utilization % — last {{ range_key }}</div>
|
|
<div style="height:220px;"><canvas id="chart-util"></canvas></div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Throughput (B/s) — last {{ range_key }}</div>
|
|
<div style="height:220px;"><canvas id="chart-net"></canvas></div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Load & pressure — last {{ range_key }}</div>
|
|
<div style="height:220px;"><canvas id="chart-pressure"></canvas></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const series = {{ series|tojson }};
|
|
// This fragment is re-swapped on poll / range change. Destroy the previous
|
|
// chart instances first so Chart.js doesn't leak detached canvases.
|
|
(window._hostCharts || []).forEach(function (c) { try { c.destroy(); } catch (e) {} });
|
|
window._hostCharts = [];
|
|
const fmtTime = (v) => { const d = new Date(v); return ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2); };
|
|
const baseOpts = (ymax) => ({
|
|
responsive: true, maintainAspectRatio: false,
|
|
interaction: { mode: "index", intersect: false },
|
|
elements: { point: { radius: 0 } },
|
|
scales: {
|
|
x: { type: "linear", ticks: { callback: (v) => fmtTime(v), maxTicksLimit: 8, color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } },
|
|
y: { beginAtZero: true, max: ymax, ticks: { color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } },
|
|
},
|
|
plugins: {
|
|
legend: { labels: { color: "#b8b8b0", boxWidth: 10, font: { size: 11 } } },
|
|
tooltip: { callbacks: { title: function (items) { return items.length ? fmtTime(items[0].parsed.x) : ""; } } },
|
|
},
|
|
});
|
|
const mk = (id, datasets, ymax) => {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
window._hostCharts.push(new Chart(el.getContext("2d"), {
|
|
type: "line",
|
|
data: { datasets: datasets.map((d) => ({
|
|
label: d.label,
|
|
data: (series[d.key] || []).map((p) => ({ x: p[0], y: p[1] })),
|
|
borderColor: d.color, backgroundColor: d.color, borderWidth: 1.5, tension: 0.25,
|
|
})) },
|
|
options: baseOpts(ymax),
|
|
}));
|
|
};
|
|
mk("chart-util", [
|
|
{ key: "cpu_pct", label: "CPU %", color: "#c8a840" },
|
|
{ key: "mem_used_pct", label: "Mem %", color: "#4aa86a" },
|
|
{ key: "disk_used_pct_worst", label: "Disk % (worst)", color: "#c87840" },
|
|
], 100);
|
|
mk("chart-net", [
|
|
{ key: "net_rx_bps", label: "Net RX", color: "#4aa86a" },
|
|
{ key: "net_tx_bps", label: "Net TX", color: "#c84048" },
|
|
{ key: "disk_read_bps", label: "Disk read", color: "#6aa0d0" },
|
|
{ key: "disk_write_bps", label: "Disk write", color: "#c8a840" },
|
|
], undefined);
|
|
mk("chart-pressure", [
|
|
{ key: "load_1m", label: "Load 1m", color: "#c8a840" },
|
|
{ key: "psi_mem_some_avg10", label: "Mem PSI %", color: "#c84048" },
|
|
{ key: "temp_c_max", label: "Temp °C", color: "#c87840" },
|
|
], undefined);
|
|
})();
|
|
</script>
|