212 lines
9.3 KiB
Python
212 lines
9.3 KiB
Python
"""
|
|
Dashboard endpoints for comprehensive usage monitoring.
|
|
"""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from typing import Dict, Any
|
|
from datetime import datetime
|
|
from loguru import logger
|
|
import sqlite3
|
|
|
|
from services.database import get_db
|
|
from services.subscription import UsageTrackingService, PricingService
|
|
from services.subscription.schema_utils import ensure_subscription_plan_columns, ensure_usage_summaries_columns
|
|
from models.subscription_models import UsageAlert
|
|
from middleware.auth_middleware import get_current_user
|
|
from ..dependencies import verify_user_access
|
|
from ..cache import get_cached_dashboard, set_cached_dashboard
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/dashboard/{user_id}")
|
|
async def get_dashboard_data(
|
|
user_id: str,
|
|
billing_period: str = None,
|
|
db: Session = Depends(get_db),
|
|
current_user: Dict[str, Any] = Depends(get_current_user)
|
|
) -> Dict[str, Any]:
|
|
"""Get comprehensive dashboard data for usage monitoring."""
|
|
|
|
verify_user_access(user_id, current_user)
|
|
|
|
try:
|
|
ensure_subscription_plan_columns(db)
|
|
ensure_usage_summaries_columns(db)
|
|
|
|
# Check cache first (skip if billing_period is specified)
|
|
if not billing_period:
|
|
cached_data = get_cached_dashboard(user_id)
|
|
if cached_data:
|
|
return cached_data
|
|
|
|
usage_service = UsageTrackingService(db)
|
|
pricing_service = PricingService(db)
|
|
|
|
# Get current usage stats (for the requested period)
|
|
current_usage = usage_service.get_user_usage_stats(user_id, billing_period)
|
|
|
|
# Get usage trends (last 6 months)
|
|
trends = usage_service.get_usage_trends(user_id, 6)
|
|
|
|
# Get user limits
|
|
limits = pricing_service.get_user_limits(user_id)
|
|
|
|
# Get unread alerts
|
|
alerts_query = db.query(UsageAlert).filter(
|
|
UsageAlert.user_id == user_id,
|
|
UsageAlert.is_read == False
|
|
)
|
|
if billing_period:
|
|
alerts_query = alerts_query.filter(UsageAlert.billing_period == billing_period)
|
|
|
|
alerts = alerts_query.order_by(UsageAlert.created_at.desc()).limit(5).all()
|
|
|
|
alerts_data = [
|
|
{
|
|
"id": alert.id,
|
|
"type": alert.alert_type,
|
|
"title": alert.title,
|
|
"message": alert.message,
|
|
"severity": alert.severity,
|
|
"created_at": alert.created_at.isoformat()
|
|
}
|
|
for alert in alerts
|
|
]
|
|
|
|
# Calculate cost projections (only relevant for current month)
|
|
current_cost = current_usage.get('total_cost', 0)
|
|
days_in_period = 30
|
|
current_day = datetime.now().day
|
|
|
|
# Only project costs if viewing current month
|
|
is_current_month = not billing_period or billing_period == datetime.now().strftime("%Y-%m")
|
|
if is_current_month:
|
|
projected_cost = (current_cost / current_day) * days_in_period if current_day > 0 else 0
|
|
else:
|
|
projected_cost = current_cost # For past months, projected is actual
|
|
|
|
response_payload = {
|
|
"success": True,
|
|
"data": {
|
|
"current_usage": current_usage,
|
|
"trends": trends,
|
|
"limits": limits,
|
|
"alerts": alerts_data,
|
|
"projections": {
|
|
"projected_monthly_cost": round(projected_cost, 2),
|
|
"cost_limit": limits.get('limits', {}).get('monthly_cost', 0) if limits else 0,
|
|
"projected_usage_percentage": (projected_cost / max(limits.get('limits', {}).get('monthly_cost', 1), 1)) * 100 if limits else 0
|
|
},
|
|
"summary": {
|
|
"total_api_calls_this_month": current_usage.get('total_calls', 0),
|
|
"total_cost_this_month": current_usage.get('total_cost', 0),
|
|
"usage_status": current_usage.get('usage_status', 'active'),
|
|
"unread_alerts": len(alerts_data)
|
|
}
|
|
}
|
|
}
|
|
|
|
# Cache the response only for default view
|
|
if not billing_period:
|
|
set_cached_dashboard(user_id, response_payload)
|
|
|
|
return response_payload
|
|
|
|
except (sqlite3.OperationalError, Exception) as e:
|
|
error_str = str(e).lower()
|
|
if 'no such column' in error_str and ('exa_calls' in error_str or 'exa_cost' in error_str or 'video_calls' in error_str or 'video_cost' in error_str or 'image_edit_calls' in error_str or 'image_edit_cost' in error_str or 'audio_calls' in error_str or 'audio_cost' in error_str):
|
|
logger.warning("Missing column detected in dashboard query, attempting schema fix...")
|
|
try:
|
|
import services.subscription.schema_utils as schema_utils
|
|
schema_utils._checked_usage_summaries_columns = False
|
|
schema_utils._checked_subscription_plan_columns = False
|
|
# Use the already imported functions from top of file
|
|
ensure_usage_summaries_columns(db)
|
|
ensure_subscription_plan_columns(db)
|
|
db.expire_all()
|
|
|
|
# Retry the query
|
|
usage_service = UsageTrackingService(db)
|
|
pricing_service = PricingService(db)
|
|
|
|
current_usage = usage_service.get_user_usage_stats(user_id)
|
|
trends = usage_service.get_usage_trends(user_id, 6)
|
|
limits = pricing_service.get_user_limits(user_id)
|
|
|
|
alerts = db.query(UsageAlert).filter(
|
|
UsageAlert.user_id == user_id,
|
|
UsageAlert.is_read == False
|
|
).order_by(UsageAlert.created_at.desc()).limit(5).all()
|
|
|
|
alerts_data = [
|
|
{
|
|
"id": alert.id,
|
|
"type": alert.alert_type,
|
|
"title": alert.title,
|
|
"message": alert.message,
|
|
"severity": alert.severity,
|
|
"created_at": alert.created_at.isoformat()
|
|
}
|
|
for alert in alerts
|
|
]
|
|
|
|
current_cost = current_usage.get('total_cost', 0)
|
|
days_in_period = 30
|
|
current_day = datetime.now().day
|
|
projected_cost = (current_cost / current_day) * days_in_period if current_day > 0 else 0
|
|
|
|
response_payload = {
|
|
"success": True,
|
|
"data": {
|
|
"current_usage": current_usage,
|
|
"trends": trends,
|
|
"limits": limits,
|
|
"alerts": alerts_data,
|
|
"projections": {
|
|
"projected_monthly_cost": round(projected_cost, 2),
|
|
"cost_limit": limits.get('limits', {}).get('monthly_cost', 0) if limits else 0,
|
|
"projected_usage_percentage": (projected_cost / max(limits.get('limits', {}).get('monthly_cost', 1), 1)) * 100 if limits else 0
|
|
},
|
|
"summary": {
|
|
"total_api_calls_this_month": current_usage.get('total_calls', 0),
|
|
"total_cost_this_month": current_usage.get('total_cost', 0),
|
|
"usage_status": current_usage.get('usage_status', 'active'),
|
|
"unread_alerts": len(alerts_data)
|
|
}
|
|
}
|
|
}
|
|
|
|
# Cache the response after successful retry
|
|
set_cached_dashboard(user_id, response_payload)
|
|
return response_payload
|
|
except Exception as retry_err:
|
|
logger.error(f"Schema fix and retry failed: {retry_err}")
|
|
return {
|
|
"success": False,
|
|
"error": str(retry_err),
|
|
"data": {
|
|
"current_usage": {"total_calls": 0, "total_cost": 0, "usage_status": "error", "provider_breakdown": {}},
|
|
"trends": [],
|
|
"limits": {"limits": {"monthly_cost": 0}},
|
|
"alerts": [],
|
|
"projections": {"projected_monthly_cost": 0, "cost_limit": 0, "projected_usage_percentage": 0},
|
|
"summary": {"total_api_calls_this_month": 0, "total_cost_this_month": 0, "usage_status": "error", "unread_alerts": 0}
|
|
}
|
|
}
|
|
|
|
logger.error(f"Error getting dashboard data: {e}")
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"data": {
|
|
"current_usage": {"total_calls": 0, "total_cost": 0, "usage_status": "error", "provider_breakdown": {}},
|
|
"trends": [],
|
|
"limits": {"limits": {"monthly_cost": 0}},
|
|
"alerts": [],
|
|
"projections": {"projected_monthly_cost": 0, "cost_limit": 0, "projected_usage_percentage": 0},
|
|
"summary": {"total_api_calls_this_month": 0, "total_cost_this_month": 0, "usage_status": "error", "unread_alerts": 0}
|
|
}
|
|
}
|