"""Dashboard API with caching and optimizations""" from datetime import UTC, datetime, timedelta from fastapi import APIRouter, Depends from sqlalchemy import select, func, text from sqlalchemy.ext.asyncio import AsyncSession from app.db.session import get_db from app.models.user import User from app.models.datasource import DataSource from app.models.datasource_config import DataSourceConfig from app.models.alert import Alert, AlertSeverity from app.models.task import CollectionTask from app.core.security import get_current_user from app.core.cache import cache from app.core.time import to_iso8601_utc # Built-in collectors info (mirrored from datasources.py) COLLECTOR_INFO = { "top500": { "id": 1, "name": "TOP500 Supercomputers", "module": "L1", "priority": "P0", "frequency_hours": 4, }, "epoch_ai_gpu": { "id": 2, "name": "Epoch AI GPU Clusters", "module": "L1", "priority": "P0", "frequency_hours": 6, }, "huggingface_models": { "id": 3, "name": "HuggingFace Models", "module": "L2", "priority": "P1", "frequency_hours": 12, }, "huggingface_datasets": { "id": 4, "name": "HuggingFace Datasets", "module": "L2", "priority": "P1", "frequency_hours": 12, }, "huggingface_spaces": { "id": 5, "name": "HuggingFace Spaces", "module": "L2", "priority": "P2", "frequency_hours": 24, }, "peeringdb_ixp": { "id": 6, "name": "PeeringDB IXP", "module": "L2", "priority": "P1", "frequency_hours": 24, }, "peeringdb_network": { "id": 7, "name": "PeeringDB Networks", "module": "L2", "priority": "P2", "frequency_hours": 48, }, "peeringdb_facility": { "id": 8, "name": "PeeringDB Facilities", "module": "L2", "priority": "P2", "frequency_hours": 48, }, "telegeography_cables": { "id": 9, "name": "Submarine Cables", "module": "L2", "priority": "P1", "frequency_hours": 168, }, "telegeography_landing": { "id": 10, "name": "Cable Landing Points", "module": "L2", "priority": "P2", "frequency_hours": 168, }, "telegeography_systems": { "id": 11, "name": "Cable Systems", "module": "L2", "priority": "P2", "frequency_hours": 168, }, } router = APIRouter() @router.get("/stats") async def get_stats( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get dashboard statistics with caching""" cache_key = "dashboard:stats" cached_result = cache.get(cache_key) if cached_result: return cached_result today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) # Count built-in collectors built_in_count = len(COLLECTOR_INFO) built_in_active = built_in_count # Built-in are always "active" for counting purposes # Count custom configs from database result = await db.execute(select(func.count(DataSourceConfig.id))) custom_count = result.scalar() or 0 result = await db.execute( select(func.count(DataSourceConfig.id)).where(DataSourceConfig.is_active == True) ) custom_active = result.scalar() or 0 # Total datasources total_datasources = built_in_count + custom_count active_datasources = built_in_active + custom_active # Tasks today (from database) result = await db.execute( select(func.count(CollectionTask.id)).where(CollectionTask.started_at >= today_start) ) tasks_today = result.scalar() or 0 result = await db.execute( select(func.count(CollectionTask.id)).where( CollectionTask.status == "success", CollectionTask.started_at >= today_start, ) ) success_tasks = result.scalar() or 0 success_rate = (success_tasks / tasks_today * 100) if tasks_today > 0 else 0 # Alerts result = await db.execute( select(func.count(Alert.id)).where( Alert.severity == AlertSeverity.CRITICAL, Alert.status == "active", ) ) critical_alerts = result.scalar() or 0 result = await db.execute( select(func.count(Alert.id)).where( Alert.severity == AlertSeverity.WARNING, Alert.status == "active", ) ) warning_alerts = result.scalar() or 0 result = await db.execute( select(func.count(Alert.id)).where( Alert.severity == AlertSeverity.INFO, Alert.status == "active", ) ) info_alerts = result.scalar() or 0 response = { "total_datasources": total_datasources, "active_datasources": active_datasources, "tasks_today": tasks_today, "success_rate": round(success_rate, 1), "last_updated": to_iso8601_utc(datetime.now(UTC)), "alerts": { "critical": critical_alerts, "warning": warning_alerts, "info": info_alerts, }, } cache.set(cache_key, response, expire_seconds=60) return response @router.get("/summary") async def get_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get dashboard summary by module with caching""" cache_key = "dashboard:summary" cached_result = cache.get(cache_key) if cached_result: return cached_result # Count by module for built-in collectors builtin_by_module = {} for name, info in COLLECTOR_INFO.items(): module = info["module"] if module not in builtin_by_module: builtin_by_module[module] = {"datasources": 0, "sources": []} builtin_by_module[module]["datasources"] += 1 builtin_by_module[module]["sources"].append(info["name"]) # Count custom configs by module (default to L3 for custom) result = await db.execute( select(DataSourceConfig.source_type, func.count(DataSourceConfig.id).label("count")) .where(DataSourceConfig.is_active == True) .group_by(DataSourceConfig.source_type) ) custom_rows = result.fetchall() for row in custom_rows: source_type = row.source_type module = "L3" # Custom configs default to L3 if module not in builtin_by_module: builtin_by_module[module] = {"datasources": 0, "sources": []} builtin_by_module[module]["datasources"] += row.count builtin_by_module[module]["sources"].append(f"自定义 ({source_type})") summary = {} for module, data in builtin_by_module.items(): summary[module] = { "datasources": data["datasources"], "total_records": 0, # Built-in don't track this in dashboard stats "last_updated": to_iso8601_utc(datetime.now(UTC)), } response = {"modules": summary, "last_updated": to_iso8601_utc(datetime.now(UTC))} cache.set(cache_key, response, expire_seconds=300) return response