"""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("

Not found

", status_code=404) return _render("scan_detail.html", scan=scan, request=request) @router.get("/packages", response_class=HTMLResponse) async def packages_list( request: Request, page: int = 1, flagged: str = "", session: AsyncSession = Depends(get_session), ): per_page = 50 offset = (page - 1) * per_page subq = select( Scan.package_name.label("pkg_name"), Scan.package_version.label("pkg_ver"), Scan.ecosystem, Scan.repository, func.max(Scan.started_at).label("last_scan"), func.max(Scan.flagged).label("is_flagged"), func.sum(Scan.total_findings).label("findings_sum"), func.max(Scan.id).label("sid"), ).group_by(Scan.package_name, Scan.package_version) if flagged == "1": subq = subq.having(func.max(Scan.flagged) == True) subq = subq.subquery() total = await session.scalar(select(func.count()).select_from(subq)) rows = ( await session.execute( select(subq).order_by(subq.c.last_scan.desc()).offset(offset).limit(per_page) ) ).all() return _render( "packages_list.html", packages=rows, page=page, per_page=per_page, total=total, flagged_filter=flagged, request=request, ) @router.get("/packages/{name}/{version}", response_class=HTMLResponse) async def package_detail( name: str, version: str, request: Request, session: AsyncSession = Depends(get_session), ): from sqlalchemy.orm import selectinload scans = ( ( await session.execute( select(Scan) .where(Scan.package_name == name, Scan.package_version == version) .options(selectinload(Scan.findings)) .order_by(Scan.started_at.desc()) ) ) .scalars() .all() ) if not scans: return HTMLResponse("

Not found

", status_code=404) all_findings = [] for s in scans: all_findings.extend(s.findings) return _render( "package_detail.html", pkg_name=name, pkg_version=version, scans=scans, findings=all_findings, request=request, )