"""Web UI routes — Jinja2 + htmx pages.""" from urllib.parse import unquote from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse from jinja2 import Environment, PackageLoader, select_autoescape from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ..constants import ( APP_PACKAGE, DEFAULT_SORT_BY_PACKAGES, DEFAULT_SORT_BY_SCANS, DEFAULT_SORT_DIR, WEB_PER_PAGE, ) from ..db.engine import get_session from ..db.models import Finding, Scan from ..db.queries import ( build_package_list_query, build_scan_list_query, get_dashboard_stats, ) from ..i18n import t as _t router = APIRouter(tags=["web"]) _jinja_env = Environment( loader=PackageLoader(APP_PACKAGE, "web/templates"), autoescape=select_autoescape(), ) _jinja_env.globals["t"] = _t def _render(name: str, **context) -> HTMLResponse: template = _jinja_env.get_template(name) return HTMLResponse(template.render(**context)) @router.get("/", response_class=HTMLResponse) async def dashboard(request: Request, session: AsyncSession = Depends(get_session)): ctx = await get_dashboard_stats(session) return _render("dashboard.html", **ctx, request=request) @router.get("/dashboard/stats", response_class=HTMLResponse) async def dashboard_stats_fragment(request: Request, session: AsyncSession = Depends(get_session)): ctx = await get_dashboard_stats(session) return _render("dashboard_stats.html", request=request, **ctx) @router.get("/scans", response_class=HTMLResponse) async def scans_list( request: Request, page: int = 1, flagged: str = "", search: str = "", status: str = "", sort_by: str = DEFAULT_SORT_BY_SCANS, sort_dir: str = DEFAULT_SORT_DIR, session: AsyncSession = Depends(get_session), ): per_page = WEB_PER_PAGE offset = (page - 1) * per_page flagged_bool = None if flagged == "1": flagged_bool = True q, count_q = build_scan_list_query( flagged=flagged_bool, status=status or None, search=search or None, sort_by=sort_by, sort_dir=sort_dir, limit=per_page, offset=offset, ) scans = (await session.execute(q)).scalars().all() total = await session.scalar(count_q) template = "_scans_table.html" if request.headers.get("HX-Request") else "scans_list.html" return _render( template, scans=scans, page=page, per_page=per_page, total=total, flagged_filter=flagged, search=search, status_filter=status, sort_by=sort_by, sort_dir=sort_dir, 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("