"""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(): keys = [i["key"] for i in recommended_layout({})] assert keys == ["status_overview", "hosts_overview", "monitors", "uptime_summary"] # uptime no longer fits beside the monitors, so it starts the next row items = recommended_layout({}) assert (items[3]["x"], items[3]["y"]) == (0, 10) 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 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 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}} 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 0.73" in page async def test_a_widget_polls_at_its_own_pace(app): defn = {**WIDGET_REGISTRY["uptime_summary"]} item = SimpleNamespace(db=SimpleNamespace(id=7, grid_x=0, grid_y=0, grid_w=6, grid_h=5), title="Uptime", defn=defn, 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 "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