"""Web UI routes — Jinja2 + htmx pages.""" import datetime from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from guarddog_nexus.database import get_session from guarddog_nexus.models import Finding, Scan router = APIRouter(tags=["web"]) TEMPLATES: dict[str, str] = {} def _render(name: str, **context) -> HTMLResponse: from jinja2 import Environment, PackageLoader, select_autoescape env = Environment( loader=PackageLoader("guarddog_nexus", "web/templates"), autoescape=select_autoescape(), ) template = env.get_template(name) return HTMLResponse(template.render(**context)) @router.get("/", response_class=HTMLResponse) async def dashboard(request: Request, session: AsyncSession = Depends(get_session)): total_scans = await session.scalar(select(func.count(Scan.id))) flagged_scans = await session.scalar(select(func.count(Scan.id)).where(Scan.flagged == True)) recent_flagged = await session.scalar( select(func.count(Scan.id)).where( Scan.flagged == True, Scan.started_at >= func.datetime("now", "-7 days"), ) ) total_findings = await session.scalar(select(func.count(Finding.id))) latest_scans = ( (await session.execute(select(Scan).order_by(Scan.started_at.desc()).limit(10))) .scalars() .all() ) top_rules = ( await session.execute( select(Finding.rule, func.count(Finding.id).label("cnt")) .group_by(Finding.rule) .order_by(func.count(Finding.id).desc()) .limit(10) ) ).all() return _render( "dashboard.html", total_scans=total_scans, flagged_scans=flagged_scans, recent_flagged=recent_flagged, total_findings=total_findings, latest_scans=latest_scans, top_rules=[(r.rule, r.cnt) for r in top_rules], now=datetime.datetime.now(datetime.timezone.utc), request=request, ) @router.get("/scans", response_class=HTMLResponse) async def scans_list( request: Request, page: int = 1, flagged: str = "", session: AsyncSession = Depends(get_session), ): per_page = 50 offset = (page - 1) * per_page q = select(Scan) if flagged == "1": q = q.where(Scan.flagged == True) q = q.order_by(Scan.started_at.desc()).offset(offset).limit(per_page) scans = (await session.execute(q)).scalars().all() total = await session.scalar(select(func.count(Scan.id))) return _render( "scans_list.html", scans=scans, page=page, per_page=per_page, total=total, flagged_filter=flagged, request=request, ) @router.get("/scans/{scan_id}", response_class=HTMLResponse) async def scan_detail(scan_id: int, request: Request, session: AsyncSession = Depends(get_session)): from sqlalchemy.orm import selectinload scan = await session.scalar( select(Scan).where(Scan.id == scan_id).options(selectinload(Scan.findings)) ) if not scan: return HTMLResponse("