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