fed9973899
The agent reports every few seconds, so multi-hour history series were hundreds– thousands of points — a dense, noisy line (esp. CPU). Bucket-average server-side to ~120 points (keeps the shape, drops the noise) for both the history-graph widget and the host-detail charts. Also fix the chart tooltip title showing the raw epoch-ms (e.g. 1,781,720,471,459) — format it as HH:MM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
257 lines
14 KiB
HTML
257 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs %}
|
|
{% block title %}{{ host.name }} — Host Agent — Steward{% endblock %}
|
|
{% block breadcrumb %}{{ crumbs([("Hosts", "/hosts/"), (host.name, "/hosts/" ~ host.id), ("Metrics", "")]) }}{% endblock %}
|
|
|
|
{% macro fmt_bps(v) %}
|
|
{%- if v is none -%}—
|
|
{%- elif v >= 1048576 -%}{{ "%.1f"|format(v / 1048576) }} MB/s
|
|
{%- elif v >= 1024 -%}{{ "%.1f"|format(v / 1024) }} KB/s
|
|
{%- else -%}{{ "%.0f"|format(v) }} B/s
|
|
{%- endif -%}
|
|
{% endmacro %}
|
|
{% macro fmt_bytes(v) %}
|
|
{%- if v is none -%}—
|
|
{%- elif v >= 1073741824 -%}{{ "%.1f"|format(v / 1073741824) }} GB
|
|
{%- elif v >= 1048576 -%}{{ "%.0f"|format(v / 1048576) }} MB
|
|
{%- else -%}{{ "%.0f"|format(v / 1024) }} KB
|
|
{%- endif -%}
|
|
{% endmacro %}
|
|
{% macro bar(pct) %}
|
|
{%- set p = pct if pct is not none else 0 -%}
|
|
<div style="height:8px;background:var(--bg-elevated);border-radius:4px;overflow:hidden;border:1px solid var(--border);">
|
|
<div style="height:100%;width:{{ p|round(1) }}%;background:{% if pct is none %}var(--text-dim){% elif pct < 70 %}var(--green){% elif pct < 90 %}var(--yellow){% else %}var(--red){% endif %};"></div>
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% block content %}
|
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-bottom:0.75rem;">
|
|
<div style="display:flex;align-items:center;gap:0.6rem;">
|
|
<a href="/hosts/{{ host.id }}" class="btn btn-ghost btn-sm">← Host</a>
|
|
<h1 class="page-title" style="margin-bottom:0;">{{ host.name }}</h1>
|
|
{% if stale %}<span class="badge badge-red">stale</span>{% else %}<span class="badge badge-green">live</span>{% endif %}
|
|
</div>
|
|
<div style="display:flex;gap:0.3rem;">
|
|
{% for r in range_options %}
|
|
<a class="range-btn {% if r == range_key %}active{% endif %}" href="?range={{ r }}">{{ r }}</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Identity / metadata ─────────────────────────────────────────────────── #}
|
|
<div class="card" style="display:flex;flex-wrap:wrap;gap:0.5rem 1.25rem;align-items:center;font-size:0.82rem;color:var(--text-muted);">
|
|
<span><span style="color:var(--text-dim);">Address</span> {{ host.address or "—" }}</span>
|
|
{% if reg %}
|
|
{% if reg.distro %}<span><span style="color:var(--text-dim);">OS</span> {{ reg.distro }}</span>{% endif %}
|
|
{% if reg.kernel %}<span><span style="color:var(--text-dim);">Kernel</span> {{ reg.kernel }}</span>{% endif %}
|
|
{% if reg.arch %}<span><span style="color:var(--text-dim);">Arch</span> {{ reg.arch }}</span>{% endif %}
|
|
{% if reg.agent_version %}<span><span style="color:var(--text-dim);">Agent</span> v{{ reg.agent_version }}</span>{% endif %}
|
|
{% set up = hostlvl.get('uptime_secs') %}
|
|
{% if up %}<span><span style="color:var(--text-dim);">Uptime</span> {{ (up // 86400)|int }}d {{ ((up % 86400) // 3600)|int }}h</span>{% endif %}
|
|
<span><span style="color:var(--text-dim);">Last seen</span> {{ reg.last_seen_at.strftime("%Y-%m-%d %H:%M:%S") ~ " UTC" if reg.last_seen_at else "never" }}</span>
|
|
{% else %}
|
|
<span>No agent registration found for this host.</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if not hostlvl %}
|
|
<div class="card empty">No metrics reported yet. Once the agent checks in, CPU, memory, network, disk I/O, temperatures, and pressure will appear here.</div>
|
|
{% else %}
|
|
|
|
{# ── Current headline gauges ─────────────────────────────────────────────── #}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:0.75rem;margin-bottom:1rem;">
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">CPU</div>
|
|
{% set c = hostlvl.get('cpu_pct') %}
|
|
<span class="stat-val" style="font-size:1.6rem;color:{% if c is none %}var(--text-dim){% elif c < 70 %}var(--green){% elif c < 90 %}var(--yellow){% else %}var(--red){% endif %};">{% if c is not none %}{{ "%.0f"|format(c) }}%{% else %}—{% endif %}</span>
|
|
<div style="margin-top:0.5rem;">{{ bar(c) }}</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Memory</div>
|
|
{% set mp = hostlvl.get('mem_used_pct') %}
|
|
<span class="stat-val" style="font-size:1.6rem;color:{% if mp is none %}var(--text-dim){% elif mp < 70 %}var(--green){% elif mp < 90 %}var(--yellow){% else %}var(--red){% endif %};">{% if mp is not none %}{{ "%.0f"|format(mp) }}%{% else %}—{% endif %}</span>
|
|
<div style="margin-top:0.5rem;">{{ bar(mp) }}</div>
|
|
<div style="margin-top:0.45rem;font-size:0.72rem;color:var(--text-dim);">
|
|
avail {{ fmt_bytes(hostlvl.get('mem_available_bytes')) }} · cache {{ fmt_bytes(hostlvl.get('mem_cached_bytes')) }} · swap {{ fmt_bytes(hostlvl.get('swap_used_bytes')) }}
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Load</div>
|
|
<span class="stat-val" style="font-size:1.6rem;">{% if hostlvl.get('load_1m') is not none %}{{ "%.2f"|format(hostlvl.get('load_1m')) }}{% else %}—{% endif %}</span>
|
|
<div style="margin-top:0.45rem;font-size:0.72rem;color:var(--text-dim);">
|
|
5m {% if hostlvl.get('load_5m') is not none %}{{ "%.2f"|format(hostlvl.get('load_5m')) }}{% else %}—{% endif %} ·
|
|
15m {% if hostlvl.get('load_15m') is not none %}{{ "%.2f"|format(hostlvl.get('load_15m')) }}{% else %}—{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Network</div>
|
|
<div style="font-size:0.95rem;font-variant-numeric:tabular-nums;">
|
|
<div><span style="color:var(--green);">↓</span> {{ fmt_bps(hostlvl.get('net_rx_bps')) }}</div>
|
|
<div><span style="color:var(--red);">↑</span> {{ fmt_bps(hostlvl.get('net_tx_bps')) }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Disk I/O</div>
|
|
<div style="font-size:0.95rem;font-variant-numeric:tabular-nums;">
|
|
<div><span style="color:var(--text-dim);">rd</span> {{ fmt_bps(hostlvl.get('disk_read_bps')) }}</div>
|
|
<div><span style="color:var(--text-dim);">wr</span> {{ fmt_bps(hostlvl.get('disk_write_bps')) }}</div>
|
|
</div>
|
|
</div>
|
|
{% if hostlvl.get('temp_c_max') is not none %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Temp (max)</div>
|
|
{% set t = hostlvl.get('temp_c_max') %}
|
|
<span class="stat-val" style="font-size:1.6rem;color:{% if t < 70 %}var(--green){% elif t < 85 %}var(--yellow){% else %}var(--red){% endif %};">{{ "%.0f"|format(t) }}°C</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if hostlvl.get('psi_mem_some_avg10') is not none %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;" title="Pressure stall — % of time stalled, 10s avg">Pressure (10s)</div>
|
|
<div style="font-size:0.78rem;font-variant-numeric:tabular-nums;color:var(--text-muted);">
|
|
<div>mem {{ "%.1f"|format(hostlvl.get('psi_mem_some_avg10')) }}%</div>
|
|
{% if hostlvl.get('psi_cpu_some_avg10') is not none %}<div>cpu {{ "%.1f"|format(hostlvl.get('psi_cpu_some_avg10')) }}%</div>{% endif %}
|
|
{% if hostlvl.get('psi_io_some_avg10') is not none %}<div>io {{ "%.1f"|format(hostlvl.get('psi_io_some_avg10')) }}%</div>{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ── Per-core CPU ─────────────────────────────────────────────────────────── #}
|
|
{% if cores %}
|
|
<div class="card">
|
|
<div class="section-title" style="margin-bottom:0.6rem;">Per-core CPU</div>
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.5rem 0.9rem;">
|
|
{% for idx, pct in cores %}
|
|
<div>
|
|
<div style="display:flex;justify-content:space-between;font-size:0.72rem;color:var(--text-dim);margin-bottom:0.2rem;">
|
|
<span>core {{ idx }}</span><span>{% if pct is not none %}{{ "%.0f"|format(pct) }}%{% else %}—{% endif %}</span>
|
|
</div>
|
|
{{ bar(pct) }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Filesystems ──────────────────────────────────────────────────────────── #}
|
|
{% if mounts %}
|
|
<div class="card">
|
|
<div class="section-title" style="margin-bottom:0.6rem;">Filesystems</div>
|
|
{% for mount, m in mounts.items() %}
|
|
<div style="margin-bottom:0.6rem;">
|
|
<div style="display:flex;justify-content:space-between;font-size:0.8rem;margin-bottom:0.2rem;">
|
|
<span style="font-family:ui-monospace,monospace;">{{ mount }}</span>
|
|
<span style="color:var(--text-muted);">{{ fmt_bytes(m.get('disk_used_bytes')) }} / {{ fmt_bytes(m.get('disk_total_bytes')) }}{% if m.get('disk_used_pct') is not none %} · {{ "%.0f"|format(m.get('disk_used_pct')) }}%{% endif %}</span>
|
|
</div>
|
|
{{ bar(m.get('disk_used_pct')) }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Interfaces / disks / sensors current detail ──────────────────────────── #}
|
|
{% if nets or disks_io or temps %}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem;">
|
|
{% if nets %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Interfaces</div>
|
|
{% for iface, m in nets.items() %}
|
|
<div style="display:flex;justify-content:space-between;font-size:0.8rem;padding:0.15rem 0;">
|
|
<span style="font-family:ui-monospace,monospace;">{{ iface }}</span>
|
|
<span style="color:var(--text-muted);font-variant-numeric:tabular-nums;">↓ {{ fmt_bps(m.get('net_rx_bps')) }} · ↑ {{ fmt_bps(m.get('net_tx_bps')) }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% if disks_io %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Disks</div>
|
|
{% for dev, m in disks_io.items() %}
|
|
<div style="display:flex;justify-content:space-between;font-size:0.8rem;padding:0.15rem 0;">
|
|
<span style="font-family:ui-monospace,monospace;">{{ dev }}</span>
|
|
<span style="color:var(--text-muted);font-variant-numeric:tabular-nums;">rd {{ fmt_bps(m.get('disk_read_bps')) }} · wr {{ fmt_bps(m.get('disk_write_bps')) }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% if temps %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.5rem;">Temperatures</div>
|
|
{% for label, c in temps.items() %}
|
|
<div style="display:flex;justify-content:space-between;font-size:0.8rem;padding:0.15rem 0;">
|
|
<span>{{ label }}</span>
|
|
<span style="color:{% if c is none %}var(--text-dim){% elif c < 70 %}var(--green){% elif c < 85 %}var(--yellow){% else %}var(--red){% endif %};font-variant-numeric:tabular-nums;">{% if c is not none %}{{ "%.0f"|format(c) }}°C{% else %}—{% endif %}</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── History charts ───────────────────────────────────────────────────────── #}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:1rem;margin-top: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 }};
|
|
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;
|
|
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>
|
|
{% endif %}
|
|
{% endblock %}
|