from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, desc from datetime import datetime, timedelta from typing import List from app.db.session import get_db from app.models.models import Finding, RiskScore, AuditReport, FindingStatus from app.schemas.schemas import DashboardResponse, RiskCardData from app.core.security import require_executive_or_above from app.services.completion_tracker import CompletionTracker router = APIRouter(prefix="/dashboard", tags=["dashboard"]) @router.get("/{tenant_id}", response_model=DashboardResponse) async def get_dashboard( tenant_id: str, payload: dict = Depends(require_executive_or_above), db: AsyncSession = Depends(get_db), ): # Enforce tenant isolation if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id: raise HTTPException(status_code=403, detail="Access denied") # Current score (most recent) score_result = await db.execute( select(RiskScore) .where(RiskScore.tenant_id == tenant_id) .order_by(desc(RiskScore.score_date)) .limit(1) ) current_score_obj = score_result.scalar_one_or_none() current_score = current_score_obj.overall_score if current_score_obj else 50.0 # Previous score (30 days ago) for delta prev_result = await db.execute( select(RiskScore) .where( RiskScore.tenant_id == tenant_id, RiskScore.score_date <= datetime.utcnow() - timedelta(days=30) ) .order_by(desc(RiskScore.score_date)) .limit(1) ) prev_score_obj = prev_result.scalar_one_or_none() prev_score = prev_score_obj.overall_score if prev_score_obj else None delta = round(current_score - prev_score, 1) if prev_score else None # 90-day trend trend_result = await db.execute( select(RiskScore) .where( RiskScore.tenant_id == tenant_id, RiskScore.score_date >= datetime.utcnow() - timedelta(days=90) ) .order_by(RiskScore.score_date) ) trend_scores = trend_result.scalars().all() score_trend = [ {"date": rs.score_date.strftime("%Y-%m-%d"), "score": rs.overall_score} for rs in trend_scores ] # Top 3 risks top_result = await db.execute( select(Finding) .where( Finding.tenant_id == tenant_id, Finding.is_top_risk == True, Finding.status.in_([FindingStatus.open, FindingStatus.in_progress]) ) .order_by(Finding.created_at) .limit(3) ) top_findings = top_result.scalars().all() # If no manually-flagged top risks, fall back to open criticals if not top_findings: fallback = await db.execute( select(Finding) .where( Finding.tenant_id == tenant_id, Finding.status.in_([FindingStatus.open, FindingStatus.in_progress]) ) .order_by(Finding.created_at) .limit(3) ) top_findings = fallback.scalars().all() top_risks = [ RiskCardData( id=f.id, title=f.title, ai_summary=f.ai_summary, ai_business_impact=f.ai_business_impact, ai_impact_level=f.ai_impact_level, ai_fix_priority=f.ai_fix_priority, severity=f.severity.value, category=f.category.value, ) for f in top_findings ] # Open finding counts counts_result = await db.execute( select(Finding.severity, func.count(Finding.id)) .where( Finding.tenant_id == tenant_id, Finding.status.in_([FindingStatus.open, FindingStatus.in_progress]) ) .group_by(Finding.severity) ) counts = {row[0].value: row[1] for row in counts_result} total_open = sum(counts.values()) # Baseline baseline_result = await db.execute( select(AuditReport) .where(AuditReport.tenant_id == tenant_id, AuditReport.is_baseline == True) .order_by(AuditReport.report_date) .limit(1) ) baseline = baseline_result.scalar_one_or_none() # Tenant name from app.models.models import Tenant t_result = await db.execute(select(Tenant).where(Tenant.id == tenant_id)) tenant = t_result.scalar_one_or_none() return DashboardResponse( tenant_name=tenant.name if tenant else "Unknown", current_score=current_score, previous_score=prev_score, score_delta=delta, score_trend=score_trend, top_risks=top_risks, open_critical=counts.get("critical", 0), open_high=counts.get("high", 0), open_medium=counts.get("medium", 0), total_open=total_open, baseline_score=baseline.baseline_score if baseline else None, baseline_date=baseline.report_date if baseline else None, ) @router.get("/{tenant_id}/completion") async def get_completion_status( tenant_id: str, payload: dict = Depends(require_executive_or_above), db: AsyncSession = Depends(get_db), ): """Get TrustOS platform completion metrics and next steps.""" if payload.get("role") != "trustos_admin" and payload.get("tenant_id") != tenant_id: raise HTTPException(status_code=403, detail="Access denied") tracker = CompletionTracker() metrics = await tracker.get_completion_metrics(tenant_id) return metrics