"""The dashboard starts from a curated layout, the duplicate widgets are gone, and the Overview says what each number counts (#5213).""" import re 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] # The Overview and the Alerts side by side first (#5510), then the fleet. assert items[0] == {"key": "status_overview", "x": 0, "y": 0, "w": 6, "h": 4, "config": {}} assert items[1] == {"key": "alerts", "x": 6, "y": 0, "w": 6, "h": 4, "config": {}} assert [(i["key"], i["x"], i["y"]) for i in items[2:4]] == [ ("hosts_overview", 0, 4), ("monitors", 6, 4)] 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", "alerts", "hosts_overview", "monitors"] assert [(i["x"], i["y"]) for i in items] == [(0, 0), (6, 0), (0, 4), (6, 4)] 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 'Firing' in page assert 'href="/alerts/history?rule=r1"' in page and "12m ago" in page assert 'Down' 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 # #5810: a verdict says what is wrong before the list says where. assert '
1 alert firing, 1 monitor down.
' in page assert page.index("1 alert firing, 1 monitor down.") < page.index("nas disk full") assert not hex_colours(page), hex_colours(page) async def test_the_overviews_overflow_links_to_alerts_and_to_monitors(app): """#5510: what doesn't fit goes to its own list; /status/ is gone.""" now = datetime.now(timezone.utc) alerts = [{"rule": SimpleNamespace(id=f"r{i}", name=f"rule {i}"), "label": "firing", "since": now} for i in range(7)] entries = [StatusEntry(kind="tcp", key=str(i), name=f"m{i}", status="down", detail_url=f"/monitors/{i}") for i in range(10)] async with app.test_request_context("/status/widget"): session["user_id"] = "u1" page = await render_template("status/widget.html", entries=entries, up=0, down=10, pending=0, total_hosts=1, alerts=alerts, widget_id="1") assert '+1 more alerts ยท +2 more monitors' in page assert "/status/" not in page async with app.test_request_context("/status/widget"): page = await render_template("status/widget.html", entries=entries[:3], up=0, down=3, pending=0, total_hosts=1, alerts=alerts, widget_id="1") assert "+1 more alerts" in page and "more monitors" not in page assert 'href="/alerts/"' not in page # the share view has no links 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")] went = datetime(2026, 10, 15, 9, 0, tzinfo=timezone.utc) status = {"d": {"state": "down", "down_since": went}, "a": {"state": "up", "down_since": None}} 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 assert rows[0]["down_since"] == went and rows[4]["down_since"] is None # #5810 async def test_a_down_host_says_for_how_long(app): """#5810: the problem row carries its since, as the Hosts list does.""" rows = [{"host": _host("d", "dead"), "state": "down", "worst": None, "agent": {}, "down_since": datetime.now(timezone.utc) - timedelta(hours=3)}] async with app.test_request_context("/hosts/overview/widget"): page = await render_template("hosts/overview_widget.html", rows=rows) page = " ".join(page.split()) assert "a monitor is down for" in page and ">3h<" in page and " ago" not in page 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 len(re.findall(r"]", page)) == 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