From b29b160baad21da434bca34cf2e421aabc93fd25 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 15 Oct 2026 10:11:37 -0400 Subject: [PATCH] feat(widgets): Overview verdict, pending monitors' age, fullest filesystems first (#5810) - Overview: a verdict line when something is wrong ('1 alert firing, 1 monitor down.'); its picker description now says what it lists (alerts, monitors down, anomalies). Host figures have their home in Hosts. Comments no longer name the Status page that was folded into Monitors. - Monitors widget: a monitor not checked yet says when it was added. - Host page: filesystems fullest first, by the worse of space and inodes. Co-Authored-By: Claude Opus 5.5 --- plugins/host_agent/routes.py | 4 ++++ steward/core/status.py | 8 ++++---- steward/core/widgets.py | 2 +- steward/templates/monitors/widget.html | 2 +- steward/templates/status/widget.html | 7 +++++++ tests/core/test_dashboard_defaults.py | 3 +++ tests/core/test_monitors_page.py | 11 +++++++++++ tests/plugins/host_agent/test_expand_metrics.py | 14 ++++++++++++++ 8 files changed, 45 insertions(+), 6 deletions(-) diff --git a/plugins/host_agent/routes.py b/plugins/host_agent/routes.py index 813b62a..17037e0 100644 --- a/plugins/host_agent/routes.py +++ b/plugins/host_agent/routes.py @@ -864,6 +864,10 @@ def _split_host_metrics(host_name: str, latest: dict) -> tuple: else: mounts[suffix] = metrics cores.sort(key=lambda c: c[0]) + # Fullest first, by the worse of space and inodes (#5810): a full + # /var/lib/docker must not sort after /boot. + mounts = dict(sorted(mounts.items(), key=lambda kv: ( + -max(kv[1].get("disk_used_pct") or 0, kv[1].get("inodes_used_pct") or 0), kv[0]))) return hostlvl, cores, nets, disks_io, temps, mounts diff --git a/steward/core/status.py b/steward/core/status.py index bf12894..186c1f8 100644 --- a/steward/core/status.py +++ b/steward/core/status.py @@ -1,11 +1,11 @@ # steward/core/status.py """Unified monitor-status aggregation. -A single readable "is everything up?" surface (the Status page + dashboard +A single readable "is everything up?" surface (the dashboard Overview widget) is assembled from heterogeneous monitors. The core unified Monitor entity (ping/dns/http) contributes via `monitor_status_source`; plugins may register their own sources from setup(). Each source is an async callable(db) -returning normalised StatusEntry objects so the Status page never imports +returning normalised StatusEntry objects so the Overview never imports plugin tables directly. """ from __future__ import annotations @@ -62,7 +62,7 @@ async def collect_status(db) -> list[StatusEntry]: """Gather entries from every registered source, down-first then by name. A failing source is logged and skipped so one broken plugin can't blank - the whole Status page. + the whole Overview. """ entries: list[StatusEntry] = [] for src in list(_SOURCES): @@ -210,7 +210,7 @@ def _heartbeat_title(mtype: str, r) -> str: async def monitor_status_source(db) -> list[StatusEntry]: - """Contribute every enabled Monitor (all types) to the Status page.""" + """Contribute every enabled Monitor (all types) to the Overview.""" from steward.models.monitors import Monitor monitors = list((await db.execute( diff --git a/steward/core/widgets.py b/steward/core/widgets.py index 1be750b..ed1b710 100644 --- a/steward/core/widgets.py +++ b/steward/core/widgets.py @@ -65,7 +65,7 @@ WIDGET_REGISTRY: dict[str, dict] = { "status_overview": { "key": "status_overview", "label": "Overview", - "description": "What needs attention across Steward: hosts, monitors and firing alerts, problems listed with since-times", + "description": "What needs attention across Steward: firing alerts, monitors down and metric anomalies, each with since when", "hx_url": "/status/widget", "detail_url": "/monitors/", "plugin": None, diff --git a/steward/templates/monitors/widget.html b/steward/templates/monitors/widget.html index 35c56ff..d96064d 100644 --- a/steward/templates/monitors/widget.html +++ b/steward/templates/monitors/widget.html @@ -27,7 +27,7 @@ {% if session.user_id %}{{ m.name }}{% else %}{{ m.name }}{% endif %} {%- if d.state == 'down' %}went down {{ d.down_since|ago if d.down_since else '' }}{% if d.down_reason %} · {{ d.down_reason }}{% endif %} {%- elif d.state == 'degraded' %}{% if (d.latency or status_class(latest.response_ms, 'latency') if latest else none) == 'crit' %}slow replies{% else %}checks failing now and then{% endif %} - {%- elif d.state == 'pending' %}not checked yet{% endif %} + {%- elif d.state == 'pending' %}not checked yet{% if m.created_at %} · added {{ m.created_at|ago }}{% endif %}{% endif %} {% set lat = (d.latency or status_class(latest.response_ms, 'latency')) if latest and latest.is_up and latest.response_ms is not none else none %} diff --git a/steward/templates/status/widget.html b/steward/templates/status/widget.html index fb6bd0e..c1c5e40 100644 --- a/steward/templates/status/widget.html +++ b/steward/templates/status/widget.html @@ -30,6 +30,13 @@ {% if entries %}✓ All {{ up }} monitors are up and no alert is firing.{% else %}No monitors configured, and no alert is firing.{% endif %} {% else %} +{# The verdict when something is wrong (#5810): what, in counts, worst first. #} +{% set said = [] %} +{% if firing %}{% set _ = said.append(firing|length ~ " alert" ~ ('' if firing|length == 1 else 's') ~ " firing") %}{% endif %} +{% if down %}{% set _ = said.append(down ~ " monitor" ~ ('' if down == 1 else 's') ~ " down") %}{% endif %} +{% if anomalies %}{% set _ = said.append(anomalies|length ~ " metric" ~ ('' if anomalies|length == 1 else 's') ~ " unusual") %}{% endif %} +{% if pending %}{% set _ = said.append(pending ~ " monitor" ~ ('' if pending == 1 else 's') ~ " waiting for a first check") %}{% endif %} +{% if said %}
{{ said|join(', ')|capitalize }}.
{% endif %}
{% for a in alerts[:6] %}
diff --git a/tests/core/test_dashboard_defaults.py b/tests/core/test_dashboard_defaults.py index cfa4305..57c190e 100644 --- a/tests/core/test_dashboard_defaults.py +++ b/tests/core/test_dashboard_defaults.py @@ -77,6 +77,9 @@ async def test_the_overview_labels_its_counts_and_lists_what_is_wrong(app): 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) diff --git a/tests/core/test_monitors_page.py b/tests/core/test_monitors_page.py index 52b8f0c..4a5961f 100644 --- a/tests/core/test_monitors_page.py +++ b/tests/core/test_monitors_page.py @@ -181,3 +181,14 @@ async def test_the_widget_says_when_all_is_well(app): up = {**_row(_mon("web", mid="m1"), "up", _check()), "uptime": {"24h": 100.0}} page = await _widget(app, [], [up]) assert "All 1 monitor up." in page and "tone-ok" in page + + +async def test_a_pending_monitor_says_when_it_was_added(app): + """#5810: a monitor not yet checked says how long it has waited.""" + m = _mon("new-api", mid="m7") + m.created_at = datetime.now(timezone.utc) - timedelta(minutes=6) + pending = {**_row(m, "pending", None), "uptime": {}} + page = await _widget(app, [pending], [], counts={"up": 0, "degraded": 0, "down": 0, + "pending": 1, "paused": 0}) + page = " ".join(page.split()) + assert "not checked yet · added