feat(dashboard): Hosts widget worst first, load against cores, one home per fact (#5251)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m19s
CI / unit (push) Failing after 1m31s
CI / publish (push) Skipped

The Hosts — Overview widget opens with a verdict ("VDNT-Util01: CPU high."
or "All 3 hosts healthy."), then one row per host under one header: CPU,
memory, disk / and load. Rows sort by host_glance: a down monitor, a
reading past critical, a quiet agent, a warning, then the healthy. Load
reads "16.8 / 8 cores", graded per core as Settings → Thresholds intends
(cores counted from the per-core CPU sub-resources, as on the host page).
A quiet agent's last figures are shown uncoloured, with when it last reported.

Ping and 24h uptime are gone from this widget: they belong to Monitors. So
are the unlabelled CPU sparkline and the 30-day uptime query it no longer
needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 13:15:01 -04:00
co-authored by Claude Opus 5.5
parent b5fcccba5f
commit 17abe450cc
5 changed files with 170 additions and 92 deletions
+52 -6
View File
@@ -84,13 +84,59 @@ async def test_a_quiet_overview_says_so(app):
assert ">host<" in page and 'href="/alerts/"' not in page # the share view has no links
async def test_the_hosts_widget_shows_load(app):
host = SimpleNamespace(id="h1", name="web-1")
agent = {"web-1": {"cpu_pct": 12.0, "mem_used_pct": 40.0, "load_1m": 0.734, "fresh": True}}
def _host(hid, name):
return SimpleNamespace(id=hid, name=name)
def test_hosts_sort_worst_first():
"""#5251: a down monitor, then a reading past critical, then a quiet agent,
then a warning, then the healthy, by name; a host with nothing last."""
from steward.core.settings import to_thresholds_cfg
from steward.hosts.service import host_glance
th = to_thresholds_cfg({})
hosts = [_host("a", "alpha"), _host("b", "busy"), _host("d", "dead"), _host("q", "quiet"),
_host("w", "warm"), _host("n", "nothing")]
status = {"d": {"state": "down"}, "a": {"state": "up"}}
agent = {"alpha": {"cpu_pct": 10.0, "fresh": True}, "busy": {"cpu_pct": 99.0, "fresh": True},
"quiet": {"cpu_pct": 99.0, "fresh": False},
"warm": {"cpu_pct": 10.0, "load_per_core": 85, "fresh": True}}
rows = host_glance(hosts, status, agent, th)
assert [(r["host"].name, r["state"]) for r in rows] == [
("dead", "down"), ("busy", "crit"), ("quiet", "stale"), ("warm", "warn"),
("alpha", "up"), ("nothing", "unknown")]
assert rows[1]["worst"][0] == "CPU" and rows[3]["worst"][0] == "Load"
assert rows[2]["worst"] is None # a quiet agent's old figures aren't graded
async def test_the_hosts_widget_gives_a_verdict_and_load_against_cores(app):
from datetime import datetime, timezone
rows = [
{"host": _host("h2", "VDNT-Util01"), "state": "crit", "worst": ("CPU", 99.0, "crit"),
"agent": {"cpu_pct": 99.0, "mem_used_pct": 28.0, "disk_root": 52.0, "load_1m": 16.77,
"cores": 8, "load_per_core": 210, "fresh": True}},
{"host": _host("h1", "VDNT-Docker02"), "state": "up", "worst": None,
"agent": {"cpu_pct": 1.0, "mem_used_pct": 4.0, "disk_root": 44.0, "load_1m": 0.92,
"cores": 4, "load_per_core": 23, "fresh": True}},
{"host": _host("h3", "nas"), "state": "stale", "worst": None,
"agent": {"cpu_pct": 5.0, "fresh": False, "last_at": datetime(2026, 10, 7, tzinfo=timezone.utc)}},
]
async with app.test_request_context("/hosts/overview/widget"):
page = await render_template("hosts/overview_widget.html", hosts=[host], host_status={},
uptime={}, agent=agent, cpu_sparks={})
assert ">load</span> 0.73" in page
session["user_id"] = "u1"
page = await render_template("hosts/overview_widget.html", rows=rows)
assert "2 of 3 hosts need a look." in page
assert page.index("VDNT-Util01") < page.index("VDNT-Docker02")
assert "16.8 / 8 cores" in page and "0.9 / 4 cores" in page
assert "agent last reported" in page
assert page.count("<th>") == 5 and ">ping<" not in page and "24h" not in page
assert not hex_colours(page), hex_colours(page)
async def test_one_busy_host_is_named_in_the_verdict(app):
rows = [{"host": _host("h2", "web-1"), "state": "crit", "worst": ("CPU", 99.0, "crit"),
"agent": {"cpu_pct": 99.0, "fresh": True}}]
async with app.test_request_context("/hosts/overview/widget"):
page = await render_template("hosts/overview_widget.html", rows=rows)
assert "web-1:" in page and "CPU high." in page and "tone-crit" in page
async def test_a_widget_polls_at_its_own_pace(app):