feat(dashboard): per-metric sparklines in the Host Agent — Resources widget
Operator ask: show the graphs next to their fields in the fleet widget, like the host page's AGENT panel. Each row now renders cpu/mem/disk/load as a value with a trend sparkline beneath it (1h window), instead of bare numbers. - _fleet_rows fetches a per-host recent series (cpu/mem/load host-level, disk from the root mount) in one 1h query and attaches a sparkline per metric to each row. - widget_table.html lays out a metric cell (label + threshold-coloured value + sparkline) per field, mirroring panel.html. Threshold colour is computed in the loop and passed into the cell macro (keeps Jinja macro scope clean). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -349,6 +349,39 @@ async def _fleet_rows(session) -> list[dict]:
|
||||
continue
|
||||
latest.setdefault(row.resource_name, {})[row.metric_name] = row.value
|
||||
|
||||
# Per-host recent series for the inline sparklines (cpu/mem/load host-level,
|
||||
# disk from the root mount), so each metric shows its trend beside the value —
|
||||
# the host-panel at-a-glance, on the fleet widget. 1h window keeps it cheap.
|
||||
host_names = [h.name for h in hosts.values()]
|
||||
spark_series: dict[str, dict[str, list]] = {
|
||||
n: {"cpu": [], "mem": [], "disk": [], "load": []} for n in host_names
|
||||
}
|
||||
if host_names:
|
||||
since = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
sp_rows = (await session.execute(
|
||||
select(PluginMetric).where(
|
||||
PluginMetric.source_module == SOURCE_MODULE,
|
||||
PluginMetric.recorded_at >= since,
|
||||
or_(
|
||||
and_(PluginMetric.resource_name.in_(host_names),
|
||||
PluginMetric.metric_name.in_(("cpu_pct", "mem_used_pct", "load_1m"))),
|
||||
and_(PluginMetric.resource_name.in_([n + ":/" for n in host_names]),
|
||||
PluginMetric.metric_name == "disk_used_pct"),
|
||||
),
|
||||
).order_by(PluginMetric.recorded_at)
|
||||
)).scalars().all()
|
||||
for r in sp_rows:
|
||||
if r.resource_name.endswith(":/"):
|
||||
name = r.resource_name[:-2]
|
||||
if name in spark_series:
|
||||
spark_series[name]["disk"].append(r.value)
|
||||
else:
|
||||
key = {"cpu_pct": "cpu", "mem_used_pct": "mem", "load_1m": "load"}.get(r.metric_name)
|
||||
if key and r.resource_name in spark_series:
|
||||
spark_series[r.resource_name][key].append(r.value)
|
||||
|
||||
from steward.core.status import sparkline_svg
|
||||
|
||||
stale_after = _stale_after_seconds()
|
||||
now = datetime.now(timezone.utc)
|
||||
rows = []
|
||||
@@ -359,6 +392,11 @@ async def _fleet_rows(session) -> list[dict]:
|
||||
m = latest.get(host.name, {})
|
||||
ls = reg.last_seen_at
|
||||
stale = ls is None or (now - ls).total_seconds() > stale_after
|
||||
ser = spark_series.get(host.name, {})
|
||||
sparks = {
|
||||
k: (sparkline_svg(v[-60:], width=72, height=16) if len(v) >= 2 else "")
|
||||
for k, v in ser.items()
|
||||
}
|
||||
rows.append({
|
||||
"host": host,
|
||||
"reg": reg,
|
||||
@@ -371,6 +409,7 @@ async def _fleet_rows(session) -> list[dict]:
|
||||
"temp_max": m.get("temp_c_max"),
|
||||
"net_rx_bps": m.get("net_rx_bps"),
|
||||
"net_tx_bps": m.get("net_tx_bps"),
|
||||
"sparks": sparks,
|
||||
})
|
||||
rows.sort(key=lambda r: r["host"].name.lower())
|
||||
return rows
|
||||
|
||||
Reference in New Issue
Block a user