"""Dashboard analytics: aggregate counts across every company a user owns, scoped by joining through Company.user_id (none of the source tables carry user_id directly except Alert). Every bucketed dict is pre-seeded with every enum member at 0 so the frontend never has to guess which keys might be missing. """ from __future__ import annotations import uuid from datetime import UTC, datetime, timedelta from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.alert import Alert from app.models.company import Company from app.models.detected_change import DetectedChange from app.models.enums import ChangeType, MonitoringRunStatus, SeverityLevel, SourceStatus from app.models.monitoring_run import MonitoringRun from app.models.source import Source from app.schemas.dashboard import DashboardAnalytics, RecentSignal, RunsByDayPoint async def get_dashboard_analytics( db: AsyncSession, user_id: uuid.UUID, *, days: int = 30, recent_limit: int = 10 ) -> DashboardAnalytics: since = datetime.now(UTC) - timedelta(days=days) changes_by_type = {ct.value: 0 for ct in ChangeType} changes_result = await db.execute( select(DetectedChange.change_type, func.count()) .join(Company, DetectedChange.company_id == Company.id) .where(Company.user_id == user_id, DetectedChange.created_at >= since) .group_by(DetectedChange.change_type) ) for change_type, count in changes_result.all(): changes_by_type[change_type.value] = count alerts_by_severity = {s.value: 0 for s in SeverityLevel} alerts_result = await db.execute( select(Alert.severity, func.count()) .where(Alert.user_id == user_id, Alert.created_at >= since) .group_by(Alert.severity) ) for severity, count in alerts_result.all(): alerts_by_severity[severity.value] = count sources_by_status = {s.value: 0 for s in SourceStatus} sources_result = await db.execute( select(Source.status, func.count()) .join(Company, Source.company_id == Company.id) .where(Company.user_id == user_id) .group_by(Source.status) ) for source_status, count in sources_result.all(): sources_by_status[source_status.value] = count runs_result = await db.execute( select(MonitoringRun.created_at, MonitoringRun.status) .join(Company, MonitoringRun.company_id == Company.id) .where(Company.user_id == user_id, MonitoringRun.created_at >= since) ) day_buckets: dict[str, dict[str, int]] = {} for created_at, run_status in runs_result.all(): day = created_at.date().isoformat() bucket = day_buckets.setdefault(day, {"successful": 0, "failed": 0, "other": 0}) if run_status == MonitoringRunStatus.SUCCESSFUL: bucket["successful"] += 1 elif run_status == MonitoringRunStatus.FAILED: bucket["failed"] += 1 else: bucket["other"] += 1 runs_by_day = [ RunsByDayPoint(date=day, **counts) for day, counts in sorted(day_buckets.items()) ] signals_result = await db.execute( select(DetectedChange, Company.name) .join(Company, DetectedChange.company_id == Company.id) .where(Company.user_id == user_id) .order_by(DetectedChange.created_at.desc()) .limit(recent_limit) ) recent_signals = [ RecentSignal( id=change.id, company_id=change.company_id, company_name=company_name, change_type=change.change_type.value, severity=change.severity.value, confidence_score=change.confidence_score, summary=change.summary, created_at=change.created_at, ) for change, company_name in signals_result.all() ] return DashboardAnalytics( changes_by_type=changes_by_type, alerts_by_severity=alerts_by_severity, sources_by_status=sources_by_status, runs_by_day=runs_by_day, recent_signals=recent_signals, )