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
+2
View File
@@ -114,6 +114,7 @@ async def detail(monitor_id: str):
return redirect(url_for("monitors.index"))
st = (await monitor_svc.monitor_states(db, [m]))[m.id]
failures = await monitor_svc.recent_failures(db, m.id)
history = await monitor_svc.response_history(db, m.id, datetime.now(timezone.utc))
host = await db.get(Host, m.host_id) if m.host_id else None
thresholds = current_app.config.get("THRESHOLDS", {})
latest = st["latest"]
@@ -129,6 +130,7 @@ async def detail(monitor_id: str):
since=since, since_at_least=since_at_least, uptime=st["uptime"], tls_days=tls_days,
heartbeat=[{"state": "up" if r.is_up else "down", "title": _heartbeat_title(m.type, r)}
for r in st["recent"]],
history=history, poll_interval=current_app.config.get("MONITORS_POLL_INTERVAL", 60),
)
+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(