""" Agent History Routes — persist and retrieve scan results. GET /api/compliance/agent/scans — list recent scans GET /api/compliance/agent/scans/{id} — get single scan POST /api/compliance/agent/scans — save a scan result """ import json import logging import os import uuid from datetime import datetime, timezone from fastapi import APIRouter, Query from fastapi.responses import Response from pydantic import BaseModel from compliance.services.agent_pdf_export import generate_scan_pdf logger = logging.getLogger(__name__) router = APIRouter(prefix="/compliance/agent", tags=["agent"]) DATABASE_URL = os.environ.get( "COMPLIANCE_DATABASE_URL", os.environ.get("DATABASE_URL", ""), ) class SaveScanRequest(BaseModel): url: str scan_type: str = "scan" analysis_mode: str = "post_launch" result: dict # Full scan result JSON class ScanHistoryItem(BaseModel): id: str url: str scan_type: str analysis_mode: str risk_level: str | None = None risk_score: float = 0 findings_count: int = 0 pages_scanned: int = 0 email_sent: bool = False created_at: str class ScanDetail(BaseModel): id: str url: str scan_type: str analysis_mode: str result: dict created_at: str async def _get_pool(): """Get or create database connection pool.""" import asyncpg if not DATABASE_URL: return None try: return await asyncpg.create_pool(DATABASE_URL, min_size=1, max_size=3) except Exception as e: logger.warning("DB connection failed: %s", e) return None @router.post("/scans") async def save_scan(req: SaveScanRequest): """Save a scan result to the database.""" pool = await _get_pool() if not pool: return {"status": "skipped", "reason": "no database"} scan_id = str(uuid.uuid4()) result = req.result try: async with pool.acquire() as conn: await conn.execute(""" INSERT INTO compliance_agent_scans (id, url, scan_type, analysis_mode, classification, risk_level, risk_score, escalation_level, responsible_role, services, findings, summary_html, pages_scanned, pages_list, email_sent, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) """, uuid.UUID(scan_id), req.url, req.scan_type, req.analysis_mode, result.get("classification", ""), result.get("risk_level", ""), result.get("risk_score", 0), result.get("escalation_level", ""), result.get("responsible_role", ""), json.dumps(result.get("services", [])), json.dumps(result.get("findings", [])), result.get("summary", result.get("summary_html", "")), result.get("pages_scanned", 0), json.dumps(result.get("pages_list", [])), result.get("email_status") == "sent", datetime.now(timezone.utc), ) return {"status": "saved", "id": scan_id} except Exception as e: logger.error("Failed to save scan: %s", e) return {"status": "error", "error": str(e)} finally: await pool.close() @router.get("/scans", response_model=list[ScanHistoryItem]) async def list_scans( limit: int = Query(20, le=100), scan_type: str | None = None, ): """List recent scans.""" pool = await _get_pool() if not pool: return [] try: async with pool.acquire() as conn: query = """ SELECT id, url, scan_type, analysis_mode, risk_level, risk_score, findings, pages_scanned, email_sent, created_at FROM compliance_agent_scans """ params = [] if scan_type: query += " WHERE scan_type = $1" params.append(scan_type) query += " ORDER BY created_at DESC LIMIT " + str(limit) rows = await conn.fetch(query, *params) return [ ScanHistoryItem( id=str(r["id"]), url=r["url"], scan_type=r["scan_type"], analysis_mode=r["analysis_mode"], risk_level=r["risk_level"], risk_score=r["risk_score"] or 0, findings_count=len(json.loads(r["findings"] or "[]")), pages_scanned=r["pages_scanned"] or 0, email_sent=r["email_sent"] or False, created_at=r["created_at"].isoformat() if r["created_at"] else "", ) for r in rows ] except Exception as e: logger.error("Failed to list scans: %s", e) return [] finally: await pool.close() @router.get("/scans/{scan_id}", response_model=ScanDetail) async def get_scan(scan_id: str): """Get a single scan result.""" pool = await _get_pool() if not pool: return ScanDetail(id=scan_id, url="", scan_type="", analysis_mode="", result={}, created_at="") try: async with pool.acquire() as conn: row = await conn.fetchrow(""" SELECT * FROM compliance_agent_scans WHERE id = $1 """, uuid.UUID(scan_id)) if not row: return ScanDetail(id=scan_id, url="", scan_type="", analysis_mode="", result={}, created_at="") return ScanDetail( id=str(row["id"]), url=row["url"], scan_type=row["scan_type"], analysis_mode=row["analysis_mode"], result={ "classification": row["classification"], "risk_level": row["risk_level"], "risk_score": row["risk_score"], "services": json.loads(row["services"] or "[]"), "findings": json.loads(row["findings"] or "[]"), "summary": row["summary_html"], "pages_scanned": row["pages_scanned"], "pages_list": json.loads(row["pages_list"] or "[]"), }, created_at=row["created_at"].isoformat() if row["created_at"] else "", ) except Exception as e: logger.error("Failed to get scan: %s", e) return ScanDetail(id=scan_id, url="", scan_type="", analysis_mode="", result={}, created_at="") finally: await pool.close() @router.post("/scans/pdf") async def export_scan_pdf(req: SaveScanRequest): """Generate a PDF report from scan results (no DB required).""" try: pdf_bytes = generate_scan_pdf({ "url": req.url, "scan_type": req.scan_type, "analysis_mode": req.analysis_mode, **req.result, }) return Response( content=pdf_bytes, media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="compliance-report-{req.url.split("/")[2][:30]}.pdf"'}, ) except Exception as e: logger.error("PDF generation failed: %s", e) return {"error": str(e)}