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)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m3s
CI / unit (push) Successful in 1m22s
CI / publish (push) Successful in 1m22s

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:
2026-10-07 15:57:09 -04:00
co-authored by Claude Opus 5.5
parent a7b280aed9
commit c6fc02def5
7 changed files with 162 additions and 21 deletions
+21 -1
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
import json
from collections.abc import Mapping
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from sqlalchemy import func, select
@@ -255,6 +255,26 @@ async def monitor_states(db, monitors: list[Monitor]) -> dict[str, dict]:
return out
async def response_history(db, monitor_id: str, now: datetime, hours: int = 24,
points: int = 96) -> list[dict]:
"""The monitor's response time and failed checks over the last `hours`,
in about `points` buckets, oldest first: [{at (epoch ms), ms, down}].
`ms` is the bucket's average (None when no check returned a time, as for
DNS); `down` counts the failed checks in it."""
width = max(60, hours * 3600 // points)
bucket = func.date_bin(func.make_interval(0, 0, 0, 0, 0, 0, width), MonitorResult.checked_at,
func.to_timestamp(0)).label("bucket")
rows = (await db.execute(
select(bucket, func.avg(MonitorResult.response_ms),
func.count().filter(MonitorResult.is_up.is_(False)))
.where(MonitorResult.monitor_id == monitor_id,
MonitorResult.checked_at >= now - timedelta(hours=hours))
.group_by(bucket).order_by(bucket)
)).all()
return [{"at": int(at.timestamp() * 1000), "ms": None if ms is None else round(float(ms), 1),
"down": int(down)} for at, ms, down in rows]
async def recent_failures(db, monitor_id: str, limit: int = 10) -> list[MonitorResult]:
"""The monitor's latest failed checks, newest first."""
return list((await db.execute(