feat(status): what is wrong first — down monitors and active alerts, and since when (#5120)
#5120 "Status page: what is wrong first". - A "Needs attention" card heads the Status board: each down monitor with how long it has been down, and each active alert (firing, acknowledged, pending, no data) with its condition and how long, linking to its history. - StatusEntry.since / since_at_least: when the current up/down run began, from the recent checks (streak_start); "for at least" when every check we have agrees. - Counts as stat tiles (Down, Active alerts, Pending, Up); names wrap instead of being cut off; dots carry their label. - Uptime and TLS are coloured by status_class against Settings → Thresholds instead of literal cutoffs; heartbeat pills and DOWN/UP use tokens and status_badge rather than hex and bold. - tests/core/test_status_page.py: streak_start cases, and the rows render with the problem card before the list, each since-time, threshold tones and no hex colour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""The Status page says what is wrong, and since when, before anything else
|
||||
(#5120)."""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from quart import render_template
|
||||
|
||||
from steward.core.status import StatusEntry, streak_start
|
||||
from tests.test_style_tokens import hex_colours
|
||||
|
||||
T0 = datetime(2026, 10, 6, 18, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _checks(*ups):
|
||||
return [SimpleNamespace(is_up=up, checked_at=T0 + timedelta(minutes=i)) for i, up in enumerate(ups)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ups, since, at_least", [
|
||||
((), None, False),
|
||||
((True,), 0, True),
|
||||
((False, False, False), 0, True), # down for every check we have
|
||||
((True, True, False, False), 2, False), # went down at the third check
|
||||
((False, True, True, False), 3, False),
|
||||
])
|
||||
def test_streak_start(ups, since, at_least):
|
||||
checks = _checks(*ups)
|
||||
assert streak_start(checks) == ((checks[since].checked_at if since is not None else None), at_least)
|
||||
|
||||
|
||||
async def _rows(app, entries, alerts):
|
||||
async with app.test_request_context("/status/rows"):
|
||||
return await render_template(
|
||||
"status/rows.html", entries=entries, alerts=alerts,
|
||||
problems=[e for e in entries if e.status == "down"],
|
||||
up=sum(e.status == "up" for e in entries), down=sum(e.status == "down" for e in entries), pending=0)
|
||||
|
||||
|
||||
async def test_the_problems_come_first_and_say_since_when(app):
|
||||
now = datetime.now(timezone.utc)
|
||||
entries = [
|
||||
StatusEntry(kind="http", key="1", name="shop-api", target="https://shop/x", status="down",
|
||||
since=now - timedelta(minutes=12), uptime={"24h": 93.0}, tls_days=2.0),
|
||||
StatusEntry(kind="icmp", key="2", name="nas", target="10.0.0.5", status="up",
|
||||
since=now - timedelta(days=3), since_at_least=True, uptime={"24h": 100.0}),
|
||||
]
|
||||
state = SimpleNamespace(state=SimpleNamespace(value="firing"), no_data_since=None,
|
||||
last_transition_at=now - timedelta(minutes=4))
|
||||
rule = SimpleNamespace(id="r1", name="CPU high", resource_name="web01", metric_name="cpu_pct",
|
||||
operator=SimpleNamespace(value=">"), threshold=90)
|
||||
page = await _rows(app, entries, [(state, rule)])
|
||||
|
||||
attention = page.index("Needs attention")
|
||||
assert attention < page.index("Recent checks") # before the full list
|
||||
problem_card = page[attention:page.index("</section>")]
|
||||
assert "shop-api" in problem_card and "nas" not in problem_card
|
||||
assert "down since <time" in problem_card and ">12m ago</time>" in problem_card
|
||||
assert "CPU high" in problem_card and "cpu_pct > 90" in problem_card and ">4m ago</time>" in problem_card
|
||||
assert '<span class="badge badge-crit">Firing</span>' in problem_card
|
||||
# Uptime and TLS coloured by Settings → Thresholds, not by literals.
|
||||
assert '<span class="tone-crit">93.00%</span>' in page and '<span class="tone-ok">100.00%</span>' in page
|
||||
assert 'class="tone-crit" title="TLS certificate expires in 2 days">2d' in page
|
||||
assert not hex_colours(page), hex_colours(page)
|
||||
|
||||
|
||||
async def test_a_quiet_board_has_no_problem_card(app):
|
||||
entries = [StatusEntry(kind="icmp", key="2", name="nas", target="10.0.0.5", status="up")]
|
||||
page = await _rows(app, entries, [])
|
||||
assert "Needs attention" not in page and "nas" in page
|
||||
assert "No monitors yet" in await _rows(app, [], [])
|
||||
Reference in New Issue
Block a user