feat(monitors): Monitors absorbs the Status page; Status is removed (#5270)
Status repeated the Monitors page's counts, check strip and uptime. Monitors now has one header row and every column Status had: the last 30 checks, latest response with its trend, uptime over 24h / 7d / 30d (the range picker goes) and TLS days on every row. /status and /status/rows and the nav link are removed outright; the dashboard Overview (/status/widget) stays and its Details link goes to Monitors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,8 @@ def test_problems_come_first_then_by_name():
|
||||
|
||||
|
||||
def _row(m, state, latest, uptime=None, tls_days=None):
|
||||
return {"monitor": m, "state": state, "latest": latest, "uptime_pct": uptime, "tls_days": tls_days,
|
||||
return {"monitor": m, "state": state, "latest": latest, "uptime": {"24h": uptime} if uptime is not None else {},
|
||||
"tls_days": tls_days, "spark": "",
|
||||
"heartbeat": [{"state": "up" if latest.is_up else "down", "title": "t"}] if latest else []}
|
||||
|
||||
|
||||
@@ -62,14 +63,18 @@ async def test_the_list_counts_then_shows_problems_first(app):
|
||||
counts = {"up": 1, "degraded": 0, "down": 1, "pending": 0, "paused": 1}
|
||||
async with app.test_request_context("/monitors/rows"):
|
||||
session["user_role"] = "operator"
|
||||
page = await render_template("monitors/rows.html", monitor_data=data, counts=counts, range_key="24h")
|
||||
page = await render_template("monitors/rows.html", monitor_data=data, counts=counts)
|
||||
|
||||
assert page.index('<span class="stat-tile-label">Down</span>') < page.index("ping-row")
|
||||
assert page.index('href="/monitors/m2"') < page.index('href="/monitors/m1"') < page.index('href="/monitors/m3"')
|
||||
assert "HTTP · https://web/" in page and "Ping · https://nas/" in page
|
||||
assert '<span class="tone-ok">30 ms</span>' in page
|
||||
assert '<span class="tone-crit">90.0%</span>' in page and '<span class="tone-ok">100.0%</span>' in page
|
||||
assert 'class="tone-crit" title="TLS certificate expires in 2 days">cert 2d' in page
|
||||
assert '<span class="tone-crit">90.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
|
||||
# One header row names every column (#5270, absorbing the Status page).
|
||||
head = page[page.index("mon-head"):page.index('href="/monitors/m2"')]
|
||||
for label in ("Monitor", "Last 30 checks", "Latest", "24h", "7d", "30d", "TLS"):
|
||||
assert f">{label}<" in head, label
|
||||
assert '<span class="badge badge-crit">Down</span>' in page and '<span class="badge badge-dim">Paused</span>' in page
|
||||
assert 'aria-label="Delete api"' in page and 'data-confirm="Delete monitor api?"' in page
|
||||
assert "onclick" not in page and not hex_colours(page), hex_colours(page)
|
||||
@@ -78,9 +83,8 @@ async def test_the_list_counts_then_shows_problems_first(app):
|
||||
async def test_a_viewer_gets_no_row_actions_and_an_empty_board_says_so(app):
|
||||
async with app.test_request_context("/monitors/rows"):
|
||||
page = await render_template("monitors/rows.html", monitor_data=[_row(_mon("web"), "up", _check())],
|
||||
counts={"up": 1, "degraded": 0, "down": 0, "pending": 0, "paused": 0},
|
||||
range_key="24h")
|
||||
empty = await render_template("monitors/rows.html", monitor_data=[], range_key="24h",
|
||||
counts={"up": 1, "degraded": 0, "down": 0, "pending": 0, "paused": 0})
|
||||
empty = await render_template("monitors/rows.html", monitor_data=[],
|
||||
counts=dict.fromkeys(("up", "degraded", "down", "pending", "paused"), 0))
|
||||
assert "row-actions" not in page
|
||||
assert "No monitors yet" in empty and "/monitors/new" not in empty
|
||||
@@ -133,8 +137,7 @@ async def test_a_down_monitor_says_since_when_and_why_in_the_list(app):
|
||||
down_reason="connection refused")
|
||||
async with app.test_request_context("/monitors/rows"):
|
||||
page = await render_template("monitors/rows.html", monitor_data=[row],
|
||||
counts={"up": 0, "degraded": 0, "down": 1, "pending": 0, "paused": 0},
|
||||
range_key="24h")
|
||||
counts={"up": 0, "degraded": 0, "down": 1, "pending": 0, "paused": 0})
|
||||
page = " ".join(page.split())
|
||||
assert re.search(r'Down <time [^>]*>12m</time> · connection refused', page), page
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
"""The Status page says what is wrong, and since when, before anything else
|
||||
(#5120)."""
|
||||
"""How long a monitor has been in its state (#5120). The Status page that
|
||||
first showed it was folded into Monitors (#5270)."""
|
||||
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
|
||||
from steward.core.status import streak_start
|
||||
|
||||
T0 = datetime(2026, 10, 6, 18, 0, tzinfo=timezone.utc)
|
||||
|
||||
@@ -26,45 +24,3 @@ def _checks(*ups):
|
||||
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 "web01 CPU > 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