Files
CIAgent/apps/api/app/services/analytics_service.py
T
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
2026-08-05 10:48:20 -04:00

106 lines
4.0 KiB
Python

"""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,
)