""" 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 pydantic import BaseModel 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()