Files
FabledSteward/tests/core/test_dashboard_defaults.py
T
bvandeusenandClaude Opus 5.5 17abe450cc
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m19s
CI / unit (push) Failing after 1m31s
CI / publish (push) Skipped
feat(dashboard): Hosts widget worst first, load against cores, one home per fact (#5251)
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>
2026-10-07 13:15:01 -04:00

164 lines
8.7 KiB
Python

"""The dashboard starts from a curated layout, the duplicate widgets are gone,
and the Overview says what each number counts (#5213)."""
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from quart import render_template, session
from steward.core.status import StatusEntry
from steward.core.widgets import RECOMMENDED, RETIRED, WIDGET_REGISTRY, recommended_layout
from steward.status.routes import overview_alerts
from tests.test_style_tokens import hex_colours
ALL_ON = {p: {"enabled": True} for p in ("docker", "host_agent", "traefik", "unifi", "snmp")}
def _overlap(a, b):
return (a["x"] < b["x"] + b["w"] and b["x"] < a["x"] + a["w"]
and a["y"] < b["y"] + b["h"] and b["y"] < a["y"] + a["h"])
def test_retired_widgets_point_at_the_one_that_replaced_them():
for old, new in RETIRED.items():
assert old not in WIDGET_REGISTRY and new in WIDGET_REGISTRY
assert all(key in WIDGET_REGISTRY for key, *_ in RECOMMENDED)
def test_the_recommended_layout_fills_rows_without_overlap():
items = recommended_layout(ALL_ON)
assert [i["key"] for i in items] == [key for key, *_ in RECOMMENDED]
assert items[0] == {"key": "status_overview", "x": 0, "y": 0, "w": 12, "h": 3, "config": {}}
assert (items[1]["x"], items[1]["y"], items[2]["x"], items[2]["y"]) == (0, 3, 6, 3)
for i, a in enumerate(items):
assert a["x"] + a["w"] <= 12
assert not any(_overlap(a, b) for b in items[i + 1:]), a
def test_the_recommended_layout_skips_what_is_not_enabled():
items = recommended_layout({})
assert [i["key"] for i in items] == ["status_overview", "hosts_overview", "monitors"]
assert [(i["x"], i["y"]) for i in items] == [(0, 0), (0, 3), (6, 3)]
def test_overview_alerts_put_the_breaching_first():
from steward.models.alerts import AlertStateEnum
ack = SimpleNamespace(state=AlertStateEnum.acknowledged, last_transition_at=None, no_data_since=None)
silent = SimpleNamespace(state=AlertStateEnum.inactive, last_transition_at=None,
no_data_since=datetime(2026, 10, 7, tzinfo=timezone.utc))
firing = SimpleNamespace(state=AlertStateEnum.firing, last_transition_at=datetime(2026, 10, 6, tzinfo=timezone.utc),
no_data_since=None)
rows = overview_alerts([(ack, "r-ack"), (silent, "r-silent"), (firing, "r-firing")])
assert [(r["rule"], r["label"]) for r in rows] == [
("r-firing", "firing"), ("r-silent", "no_data"), ("r-ack", "acknowledged")]
assert rows[1]["since"] == silent.no_data_since # silent since the series stopped
assert rows[0]["since"] == firing.last_transition_at
async def test_the_overview_labels_its_counts_and_lists_what_is_wrong(app):
now = datetime.now(timezone.utc)
rule = SimpleNamespace(id="r1", name="nas disk full")
alerts = [{"rule": rule, "label": "firing", "since": now - timedelta(minutes=12)}]
entries = [
StatusEntry(kind="http", key="1", name="web", status="up", detail_url="/monitors/1"),
StatusEntry(kind="tcp", key="2", name="ssh-nas", status="down", detail_url="/monitors/2",
since=now - timedelta(hours=2)),
]
async with app.test_request_context("/status/widget"):
session["user_id"] = "u1"
page = await render_template("status/widget.html", entries=entries, up=1, down=1, pending=0,
total_hosts=3, alerts=alerts, widget_id="1")
assert ">Monitors<" in page and ">Alerts<" in page and ">firing<" in page and ">hosts<" in page
assert '<span class="badge badge-crit">Firing</span>' in page
assert 'href="/alerts/history?rule=r1"' in page and "12m ago" in page
assert '<span class="badge badge-crit">Down</span>' in page and "2h ago" in page
assert page.index("nas disk full") < page.index("ssh-nas") # alerts, then monitors
assert ">pending<" not in page # a zero pending count is not shown
assert not hex_colours(page), hex_colours(page)
async def test_a_quiet_overview_says_so(app):
async with app.test_request_context("/status/widget"):
page = await render_template("status/widget.html", entries=[], up=0, down=0, pending=0,
total_hosts=1, alerts=[], widget_id="1")
assert "No monitors configured, and no alert is firing." in page
assert ">host<" in page and 'href="/alerts/"' not in page # the share view has no links
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"):
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):
defn = {"poll": True, "poll_seconds": 300, "detail_url": "/x"}
item = SimpleNamespace(db=SimpleNamespace(id=7, grid_x=0, grid_y=0, grid_w=6, grid_h=5),
title="Slow", defn=defn, hx_url="/slow/widget")
async with app.test_request_context("/"):
page = await render_template("dashboard/_grid_item.html", item=item, can_edit=False, poll_interval=30)
assert "every 300s" in page
defn.pop("poll_seconds")
async with app.test_request_context("/"):
page = await render_template("dashboard/_grid_item.html", item=item, can_edit=False, poll_interval=30)
assert "every 30s" in page
async def test_a_grid_item_carries_its_saved_layout(app):
"""#5249: the saved 12-column place and height, which the page fits to
content in view mode and restores in edit mode."""
item = SimpleNamespace(db=SimpleNamespace(id=7, grid_x=6, grid_y=3, grid_w=6, grid_h=5),
title="Uptime", defn={"poll": False, "detail_url": "/x"}, hx_url="/uptime/widget")
async with app.test_request_context("/"):
page = await render_template("dashboard/_grid_item.html", item=item, can_edit=False, poll_interval=30)
assert 'data-x="6" data-y="3"' in page and 'data-w="6" data-h="5"' in page
assert 'gs-x="6" gs-y="3"' in page