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:
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user