"""Phase 13: Executive Dashboard router.""" from typing import List, Optional from uuid import UUID from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from app.database import get_db from app.dependencies.auth import get_current_user, require_any_role from app.schemas.executive_dashboard_schema import ( PostureSnapshotOut, ExecutiveSummary, KpiBlock, CoverageByTactic, PostureHistoryEntry, ActivityEntry, ) import app.services.executive_dashboard_service as svc router = APIRouter(prefix="/dashboard", tags=["Executive Dashboard"]) @router.get("/executive", response_model=ExecutiveSummary) def executive_view( db: Session = Depends(get_db), user=Depends(get_current_user), ): """ Full executive view — snapshot, 30-day trends, top risks, coverage by tactic, and recent activity feed. """ data = svc.get_executive_summary(db) snap = data["snapshot"] return ExecutiveSummary( snapshot=PostureSnapshotOut.model_validate(snap), coverage_trend=data["coverage_trend"], risk_trend=data["risk_trend"], top_risks=data["top_risks"], coverage_by_tactic=data["coverage_by_tactic"], recent_activity=data["recent_activity"], ) @router.get("/kpis", response_model=KpiBlock) def kpis( db: Session = Depends(get_db), user=Depends(get_current_user), ): """Compact KPI block — live aggregation without persisting a snapshot.""" live = svc.get_live_kpis(db) # Try to find today's snapshot id; fall back to None from datetime import date from app.models.executive_dashboard import PostureSnapshot today_snap = db.query(PostureSnapshot).filter( PostureSnapshot.snapshot_date == date.today() ).first() return KpiBlock( coverage_pct=live["coverage_pct"], avg_risk_score=live["avg_risk_score"], critical_count=live["critical_count"], open_queue_items=live["open_queue_items"], orphan_techniques=live["orphan_techniques"], mttd_avg_seconds=live.get("mttd_avg_seconds"), detection_rate_30d=live.get("detection_rate_30d"), playbook_count=live["playbook_count"], lesson_count=live["lesson_count"], snapshot_date=live["snapshot_date"], snapshot_id=today_snap.id if today_snap else None, ) @router.get("/coverage-by-tactic", response_model=List[CoverageByTactic]) def coverage_by_tactic( db: Session = Depends(get_db), user=Depends(get_current_user), ): """Per-tactic validated / partial / not_covered breakdown.""" return svc.get_coverage_by_tactic(db) @router.get("/posture-history", response_model=List[PostureHistoryEntry]) def posture_history( days: int = Query(30, ge=1, le=365), db: Session = Depends(get_db), user=Depends(get_current_user), ): """Historical posture snapshots for trend charts (default last 30 days).""" snaps = svc.get_posture_history(db, days=days) return [ PostureHistoryEntry( snapshot_date=s.snapshot_date, coverage_pct=s.coverage_pct, avg_risk_score=s.avg_risk_score, critical_count=s.critical_count, open_queue_items=s.open_queue_items, ) for s in snaps ] @router.post("/posture-snapshot", response_model=PostureSnapshotOut, status_code=201) def create_posture_snapshot( db: Session = Depends(get_db), user=Depends(require_any_role("admin", "red_lead", "blue_lead")), ): """ Take (or refresh) today's posture snapshot — admin / leads only. Aggregates live data from all phases into a single PostureSnapshot row. """ snap = svc.take_posture_snapshot(db, created_by=user.id) return PostureSnapshotOut.model_validate(snap) @router.get("/activity", response_model=List[ActivityEntry]) def recent_activity( limit: int = Query(20, ge=1, le=100), db: Session = Depends(get_db), user=Depends(get_current_user), ): """Recent activity feed — tests, attack-path executions, OSINT signals.""" return svc.get_recent_activity(db, limit=limit)