feat(monitors): a monitor's page reads as a page — response time over 24h, labelled actions, how long, not "ago"; the add form says what each type checks (#5214)
Monitor page: a response-time chart for the last day with failed checks marked; Edit / Pause / Delete as labelled buttons; the state tile says "for 29m" instead of "for at least 29m 25s ago"; the check strip fills its card; no failures is one line, not an empty box; Settings names the type, target and the actual global interval, with readable field names. Add monitor: a line under the fields says what the selected type checks and when it counts as up; Name says it defaults to the target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,13 +101,18 @@ async def test_the_monitor_page_says_since_when_and_why(app):
|
||||
"monitors/detail.html", monitor=m, host=None, latest=down, failures=[down], state="down",
|
||||
since=datetime.now(timezone.utc) - timedelta(minutes=12), since_at_least=False, tls_days=None,
|
||||
uptime={"24h": 93.0, "7d": None, "30d": 99.5},
|
||||
heartbeat=[{"state": "up", "title": "a"}, {"state": "down", "title": "b"}])
|
||||
heartbeat=[{"state": "up", "title": "a"}, {"state": "down", "title": "b"}],
|
||||
history=[{"at": 1, "ms": 20.0, "down": 0}, {"at": 2, "ms": 30.0, "down": 0},
|
||||
{"at": 3, "ms": None, "down": 3}], poll_interval=60)
|
||||
assert '<a href="/monitors/">Monitors</a>' in page and "shop <api>" in page
|
||||
assert "since <time" in page and ">12m ago</time>" in page
|
||||
assert ">for 12m<" in page # how long, not "… ago"
|
||||
assert "Response time · last 24h" in page and '"down": 3' in page
|
||||
assert "Expected status" in page and "1m <span" in page # the global interval, named
|
||||
assert "503 Service Unavailable" in page
|
||||
assert 'class="stat-tile stat-crit"' in page and "93.00%" in page
|
||||
assert "Authorization" in page and "s3cr3t-token" not in page # header names, never their values
|
||||
assert 'aria-label="Delete shop <api>"' in page
|
||||
assert 'data-confirm="Delete monitor shop <api>? Its check history goes with it."' in page
|
||||
assert ">Edit</a>" in page and ">Pause</button>" in page
|
||||
assert not hex_colours(page), hex_colours(page)
|
||||
|
||||
|
||||
@@ -119,6 +124,8 @@ async def test_adding_is_its_own_page_and_keeps_what_was_typed(app):
|
||||
values={"name": "db", "target": "10.0.0.9", "host_id": "", "check_interval_seconds": "30"},
|
||||
error="port must be between 1 and 65535, got 0.")
|
||||
assert '<a href="/monitors/">Monitors</a>' in page and 'action="/monitors/add"' in page
|
||||
for kind in ("Ping", "TCP", "HTTP", "DNS"): # each type says what it checks
|
||||
assert f"<strong>{kind}</strong>" in page
|
||||
assert '<p class="field-error" role="alert">port must be between 1 and 65535, got 0.</p>' in page
|
||||
assert 'value="db"' in page and 'value="10.0.0.9"' in page and 'value="30"' in page
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Integration: a monitor's response-time history averages its checks per
|
||||
bucket and counts the failed ones (#5214). Two checks share a bucket and must
|
||||
merge (lesson #5262). Requires STEWARD_DATABASE_URL."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
if not os.environ.get("STEWARD_DATABASE_URL"):
|
||||
pytest.skip("needs Postgres")
|
||||
from steward.app import create_app
|
||||
return create_app(testing=False)
|
||||
|
||||
|
||||
def test_response_history_averages_a_bucket_and_counts_its_failures(app):
|
||||
from steward.models.monitors import Monitor, MonitorResult
|
||||
from steward.monitors.service import response_history
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
width = 24 * 3600 // 96 # 15 minutes
|
||||
start = datetime.fromtimestamp((int(now.timestamp()) // width - 2) * width, timezone.utc)
|
||||
mid = str(uuid.uuid4())
|
||||
|
||||
def r(secs, up, ms):
|
||||
return MonitorResult(monitor_id=mid, checked_at=start + timedelta(seconds=secs), is_up=up, response_ms=ms)
|
||||
|
||||
async def go():
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
s.add(Monitor(id=mid, name=f"m-{mid[:8]}", type="tcp", target="10.9.9.9"))
|
||||
async with s.begin():
|
||||
s.add_all([r(10, True, 10.0), r(70, True, 30.0), r(130, False, None)])
|
||||
async with s.begin():
|
||||
return await response_history(s, mid, now)
|
||||
|
||||
got = asyncio.run(go())
|
||||
assert got == [{"at": int(start.timestamp() * 1000), "ms": 20.0, "down": 1}]
|
||||
Reference in New Issue
Block a user