"""Router des rapports HTML générés.""" from __future__ import annotations import asyncio from datetime import datetime, timezone from pathlib import Path from fastapi import APIRouter, HTTPException, Query from fastapi.responses import HTMLResponse router = APIRouter() @router.get("/api/reports") async def api_reports( reports_dir: str = Query(default=".", description="Dossier rapports"), ) -> dict: """Liste les rapports HTML disponibles dans ``reports_dir``, ``.`` et ``./rapports``. Le scan disque (``glob``, ``stat``) est exécuté dans un thread pour ne pas bloquer l'event loop si le dossier contient des centaines de rapports. """ return await asyncio.to_thread(_list_reports_sync, reports_dir) def _list_reports_sync(reports_dir: str) -> dict: target = Path(reports_dir).resolve() reports: list[dict] = [] search_dirs = [target, Path(".").resolve(), Path("./rapports").resolve()] seen: set[str] = set() for d in search_dirs: if not d.exists(): continue for f in sorted(d.glob("*.html"), key=lambda x: x.stat().st_mtime, reverse=True): if str(f) not in seen: seen.add(str(f)) stat = f.stat() reports.append({ "filename": f.name, "path": str(f), "size_kb": round(stat.st_size / 1024, 1), "modified": datetime.fromtimestamp( stat.st_mtime, tz=timezone.utc, ).isoformat(), "url": f"/reports/{f.name}", }) return {"reports": reports} @router.get("/reports/{filename}", response_class=HTMLResponse) async def serve_report(filename: str) -> HTMLResponse: """Sert un rapport HTML par son nom de fichier. Lit le contenu et le renvoie en ``text/html`` plutôt que de rediriger vers un fichier statique : indispensable pour que ça fonctionne depuis un Codespace ou tout reverse-proxy distant. La lecture est déléguée à un thread car les rapports peuvent atteindre plusieurs Mo (Chart.js inline, données denses). """ # Sécurité : interdire les path traversal if "/" in filename or "\\" in filename or ".." in filename: raise HTTPException(status_code=400, detail="Nom de fichier invalide") content = await asyncio.to_thread(_read_report_sync, filename) return HTMLResponse(content=content) def _read_report_sync(filename: str) -> str: for d in [Path("."), Path("./rapports")]: f = d / filename if f.exists() and f.suffix == ".html": return f.read_text(encoding="utf-8") raise HTTPException(status_code=404, detail=f"Rapport non trouvé : {filename}")