Files
FabledSteward/plugins/host_agent/templates/widget_history.html
T
bvandeusen eefa38cc75
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 7s
CI / integration (push) Successful in 2m20s
CI / publish (push) Successful in 8s
feat(host_agent): in-widget 1h/6h/24h time-range toggle on history graph
Add a live range switch to the history widget that re-requests the fragment
without entering edit mode. It rewrites the parent cell's hx-get so the choice
survives polling (htmx re-reads the attribute each poll), then fetches at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:44:56 -04:00

89 lines
4.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# host_agent history-graph widget — the host-view utilization chart, embedded
on a dashboard. Fills the (resizable) panel; epoch-ms linear axis so no
Chart.js date adapter is needed. #}
{% set has_data = host and (series.cpu_pct or series.mem_used_pct or series.disk_root) %}
{% if not host %}
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.75rem 0;">
No host selected. <strong>Edit</strong> this dashboard and pick a host for this graph.
</div>
{% elif not has_data %}
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.75rem 0;">
No agent metrics for <strong>{{ host.name }}</strong> in the last {{ hours }}h yet.
</div>
{% else %}
<div style="display:flex;flex-direction:column;height:100%;min-height:200px;">
{# Name the host the graph represents (panel title is generic) + a live range
toggle that re-requests this widget without entering edit mode. #}
<div style="flex:0 0 auto;display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;font-size:0.75rem;color:var(--text-muted);margin-bottom:0.3rem;">
{% if session.user_id %}
<a href="/hosts/{{ host.id }}" style="font-weight:600;">{{ host.name }}</a>
{% else %}
<strong style="color:var(--text);">{{ host.name }}</strong>
{% endif %}
<span class="hist-range" style="margin-left:auto;display:inline-flex;border:1px solid var(--border-mid);border-radius:5px;overflow:hidden;">
{% for opt in [1, 6, 24] %}
<button type="button" onclick="histRange({{ wid }}, '{{ host.id }}', {{ opt }})"
style="border:0;cursor:pointer;padding:0.1rem 0.45rem;font-size:0.72rem;
background:{% if opt == hours %}var(--accent){% else %}transparent{% endif %};
color:{% if opt == hours %}#fff{% else %}var(--text-muted){% endif %};">{{ opt }}h</button>
{% endfor %}
</span>
</div>
<div style="flex:1 1 auto;min-height:150px;position:relative;">
<canvas id="host-hist-{{ wid }}"></canvas>
</div>
</div>
<script>
// Live time-range switch. Rewrites the parent cell's hx-get so the choice sticks
// across polls (htmx re-reads the attribute each poll), then fetches immediately.
window.histRange = function (wid, hostId, hours) {
var cell = document.getElementById("widget-" + wid);
if (!cell) return;
var base = (cell.getAttribute("hx-get") || "").split("?")[0];
var url = base + "?host_id=" + encodeURIComponent(hostId) + "&hours=" + hours + "&wid=" + wid;
cell.setAttribute("hx-get", url);
if (window.htmx) window.htmx.ajax("GET", url, { target: cell, swap: "innerHTML" });
};
(function () {
var el = document.getElementById("host-hist-{{ wid }}");
if (!el) return;
var series = {{ series|tojson }};
var fmtTime = function (v) {
var d = new Date(v);
return ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2);
};
var ds = [
{ key: "cpu_pct", label: "CPU %", color: "#c8a840" },
{ key: "mem_used_pct", label: "Mem %", color: "#4aa86a" },
{ key: "disk_root", label: "Disk / %", color: "#c87840" },
];
new Chart(el.getContext("2d"), {
type: "line",
data: { datasets: ds.map(function (d) {
return {
label: d.label,
data: (series[d.key] || []).map(function (p) { return { x: p[0], y: p[1] }; }),
borderColor: d.color, backgroundColor: d.color, borderWidth: 1.5, tension: 0.25,
};
}) },
options: {
responsive: true, maintainAspectRatio: false,
interaction: { mode: "index", intersect: false },
elements: { point: { radius: 0 } },
scales: {
x: { type: "linear", ticks: { callback: function (v) { return fmtTime(v); }, maxTicksLimit: 6, color: "#8a8a92", font: { size: 10 } }, grid: { color: "#30303a" } },
// Fixed 0{{ y_max }} ceiling (server snaps the data peak up to the next
// 10% band). Steady across refreshes — auto-scaling made the zoom jump on
// every poll — while a banded ceiling still keeps the lines filling the panel.
y: { min: 0, max: {{ y_max }}, 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) : ""; } } },
},
},
});
})();
</script>
{% endif %}