104 lines
3.5 KiB
Python
104 lines
3.5 KiB
Python
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"))
|