# plugins/traefik/routes.py from __future__ import annotations from quart import Blueprint, current_app, render_template from sqlalchemy import select from fablednetmon.auth.middleware import require_role from fablednetmon.models.users import UserRole from .models import TraefikMetric traefik_bp = Blueprint("traefik", __name__, template_folder="templates") def _sparkline(values: list[float], width: int = 80, height: int = 20) -> str: """Generate an inline SVG sparkline from a list of float values.""" if len(values) < 2: return f'' mn, mx = min(values), max(values) if mx == mn: mx = mn + 1.0 step = width / (len(values) - 1) pts = [] for i, v in enumerate(values): x = i * step y = height - (v - mn) / (mx - mn) * (height - 2) - 1 pts.append(f"{x:.1f},{y:.1f}") poly = " ".join(pts) return ( f'' f'' f'' ) @traefik_bp.get("/") @require_role(UserRole.viewer) async def index(): # Last 20 data points per router for sparklines history_limit = 20 async with current_app.db_sessionmaker() as db: # Get distinct router names with a recent row result = await db.execute( select(TraefikMetric.router_name) .distinct() .order_by(TraefikMetric.router_name) ) routers = [row[0] for row in result.all()] router_data = [] for router in routers: result = await db.execute( select(TraefikMetric) .where(TraefikMetric.router_name == router) .order_by(TraefikMetric.scraped_at.desc()) .limit(history_limit) ) rows = list(reversed(result.scalars().all())) if not rows: continue latest = rows[-1] router_data.append({ "name": router, "latest": latest, "sparkline_req": _sparkline([r.request_rate for r in rows]), "sparkline_p95": _sparkline([r.latency_p95_ms for r in rows]), "sparkline_5xx": _sparkline([r.error_rate_5xx_pct for r in rows]), }) return await render_template("traefik/index.html", router_data=router_data)