"""Monitor queries shared by the monitors pages and the MCP tools. Each function takes an open session, with no Quart request context, so the routes and the /mcp tools (which run outside Quart's pipeline) call the same code. """ from __future__ import annotations from datetime import datetime, timezone from sqlalchemy import select from steward.core.status import _heartbeat_title, _last_n_results, _uptime_by_monitor from steward.core.time_range import parse_range from steward.models.monitors import Monitor async def monitor_rows(db, range_key: str): """Per-monitor state: latest result, heartbeat strip, uptime and TLS days. Returns (rows, up, down, pending, range_key). `range_key` picks the uptime window surfaced (24h/7d/30d; anything else falls back to 24h) and comes back normalised. """ _since, range_key = parse_range(range_key) monitors = list((await db.execute(select(Monitor).order_by(Monitor.name))).scalars()) ids = [m.id for m in monitors] recent = await _last_n_results(db, ids) uptime = await _uptime_by_monitor(db, ids) # Map the selected range to the uptime bucket we surface in the list. bucket = {"24h": "24h", "7d": "7d", "30d": "30d"}.get(range_key, "24h") now = datetime.now(timezone.utc) data = [] for m in monitors: rows = recent.get(m.id, []) latest = rows[-1] if rows else None tls_days = None if latest and latest.tls_expires_at: tls_days = (latest.tls_expires_at - now).total_seconds() / 86400.0 data.append({ "monitor": m, "latest": latest, "heartbeat": [{"state": "up" if r.is_up else "down", "title": _heartbeat_title(m.type, r)} for r in rows], "uptime_pct": uptime.get(m.id, {}).get(bucket), "tls_days": tls_days, }) up = sum(1 for d in data if d["latest"] and d["latest"].is_up) down = sum(1 for d in data if d["latest"] and not d["latest"].is_up) pending = sum(1 for d in data if not d["latest"]) return data, up, down, pending, range_key