from __future__ import annotations import logging from quart import Blueprint, current_app, render_template, request, redirect, url_for from sqlalchemy import select, func from fablednetmon.auth.middleware import require_role from fablednetmon.models.users import UserRole from fablednetmon.models.hosts import Host from fablednetmon.models.monitors import PingResult from fablednetmon.core.settings import get_all_settings, set_setting, DEFAULTS ping_bp = Blueprint("ping", __name__, url_prefix="/ping") logger = logging.getLogger(__name__) async def _last_n_pings(db, host_ids: list[str], n: int = 30) -> dict[str, list]: """Return {host_id: [PingResult]} for last n pings per host, oldest first.""" if not host_ids: return {} rn = func.row_number().over( partition_by=PingResult.host_id, order_by=PingResult.probed_at.desc(), ).label("rn") subq = ( select(PingResult.id, rn) .where(PingResult.host_id.in_(host_ids)) .subquery() ) pr = await db.execute( select(PingResult) .join(subq, PingResult.id == subq.c.id) .where(subq.c.rn <= n) .order_by(PingResult.host_id, PingResult.probed_at.asc()) ) result: dict[str, list] = {hid: [] for hid in host_ids} for row in pr.scalars(): result[row.host_id].append(row) return result async def _thresholds(db) -> tuple[int, int]: settings = await get_all_settings(db) good = int(settings.get("ping.threshold.good_ms", DEFAULTS["ping.threshold.good_ms"])) warn = int(settings.get("ping.threshold.warn_ms", DEFAULTS["ping.threshold.warn_ms"])) return good, warn @ping_bp.get("/") @require_role(UserRole.viewer) async def index(): async with current_app.db_sessionmaker() as db: result = await db.execute( select(Host).where(Host.ping_enabled.is_(True)).order_by(Host.name) ) hosts = result.scalars().all() pings_by_host = await _last_n_pings(db, [h.id for h in hosts]) good_ms, warn_ms = await _thresholds(db) poll_interval = current_app.config.get("MONITORS_POLL_INTERVAL", 60) return await render_template( "ping/index.html", hosts=hosts, pings_by_host=pings_by_host, good_ms=good_ms, warn_ms=warn_ms, poll_interval=poll_interval, ) @ping_bp.get("/rows") @require_role(UserRole.viewer) async def rows(): """HTMX fragment — host rows with pills, no page shell.""" async with current_app.db_sessionmaker() as db: result = await db.execute( select(Host).where(Host.ping_enabled.is_(True)).order_by(Host.name) ) hosts = result.scalars().all() pings_by_host = await _last_n_pings(db, [h.id for h in hosts]) good_ms, warn_ms = await _thresholds(db) return await render_template( "ping/rows.html", hosts=hosts, pings_by_host=pings_by_host, good_ms=good_ms, warn_ms=warn_ms, ) @ping_bp.post("/settings") @require_role(UserRole.admin) async def save_settings(): form = await request.form try: good = max(1, int(form.get("good_ms", 50))) warn = max(good + 1, int(form.get("warn_ms", 200))) except (ValueError, TypeError): good, warn = 50, 200 async with current_app.db_sessionmaker() as db: async with db.begin(): await set_setting(db, "ping.threshold.good_ms", good) await set_setting(db, "ping.threshold.warn_ms", warn) return redirect(url_for("ping.index"))