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).
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user