Spaces:
Running
Running
| """ | |
| 🛡️ ADMIN DASHBOARD — Secure Platform Administration | |
| ===================================================== | |
| Password-protected admin panel with user management, | |
| system health, analytics, and platform controls. | |
| Uses a separate admin password (bcrypt hashed in .env), | |
| NOT the normal user auth system. | |
| """ | |
| import os | |
| import sys | |
| import uuid | |
| import json | |
| import asyncio | |
| import logging | |
| import bcrypt | |
| import platform | |
| from datetime import datetime, timedelta | |
| from typing import Optional | |
| from fastapi import APIRouter, HTTPException, Depends, WebSocket, WebSocketDisconnect, Query | |
| from pydantic import BaseModel | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from sqlalchemy.future import select | |
| from sqlalchemy import func, or_ | |
| from database.db import get_db, AsyncSessionLocal | |
| from database.orm import ( | |
| UserProfile, Conversation, Message, UserFile, UserQuery, | |
| ActivityLog, DataConnection, Dashboard | |
| ) | |
| from app.models.dashboard import ComputerVisionTask | |
| from app.models.ml import MLModel as DeployedModel | |
| from app.models.developer import APICallLog, WebhookEndpoint | |
| from app.models.platform import APIKey as DeveloperAPIKey | |
| from core.auth import create_access_token, SECRET_KEY, ALGORITHM | |
| from jose import jwt, JWTError | |
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter() | |
| security = HTTPBearer(auto_error=False) | |
| # ── Admin Password (bcrypt hash stored in env) ── | |
| ADMIN_PASSWORD_HASH = os.environ.get("ADMIN_PASSWORD_HASH") | |
| # ── Request/Response Models ── | |
| class AdminLoginRequest(BaseModel): | |
| password: str | |
| class UpdateUserRoleRequest(BaseModel): | |
| role: str # authenticated, admin, banned | |
| class BroadcastRequest(BaseModel): | |
| title: str | |
| message: str | |
| # ── Admin Auth Dependency ── | |
| async def verify_admin_token(credentials: HTTPAuthorizationCredentials = Depends(security)): | |
| """Verify the request has a valid admin JWT token.""" | |
| if not credentials: | |
| raise HTTPException(status_code=401, detail="Admin authentication required") | |
| try: | |
| payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM]) | |
| if payload.get("role") != "super_admin": | |
| raise HTTPException(status_code=403, detail="Not an admin token") | |
| return payload | |
| except JWTError: | |
| raise HTTPException(status_code=401, detail="Invalid or expired admin token") | |
| # ══════════════════════════════════════════════════════ | |
| # ADMIN ENDPOINTS | |
| # ══════════════════════════════════════════════════════ | |
| async def login_admin(req: AdminLoginRequest): | |
| """Authenticate admin and issue a token.""" | |
| if not ADMIN_PASSWORD_HASH: | |
| raise HTTPException(status_code=401, detail="Admin access not configured") | |
| try: | |
| # Check if they accidentally put the plain text password in the env instead of the hash | |
| if not ADMIN_PASSWORD_HASH.startswith("$2b$"): | |
| if req.password != ADMIN_PASSWORD_HASH: | |
| raise HTTPException(status_code=401, detail="Invalid admin password") | |
| else: | |
| if not bcrypt.checkpw(req.password.encode("utf-8"), ADMIN_PASSWORD_HASH.encode("utf-8")): | |
| raise HTTPException(status_code=401, detail="Invalid admin password") | |
| except Exception as e: | |
| logger.error(f"Admin login error: {e}") | |
| raise HTTPException(status_code=401, detail="Invalid admin password") | |
| # Issue a super_admin JWT (expires in 4 hours) | |
| token = create_access_token( | |
| data={"sub": "admin", "role": "super_admin", "email": "admin@datavision.app"}, | |
| expires_delta=timedelta(hours=4) | |
| ) | |
| return { | |
| "success": True, | |
| "token": token, | |
| "message": "Admin authenticated successfully", | |
| "expires_in": 240 # minutes | |
| } | |
| async def get_platform_stats( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """Get platform-wide statistics.""" | |
| try: | |
| total_users = await db.scalar(select(func.count()).select_from(UserProfile)) or 0 | |
| total_files = await db.scalar(select(func.count()).select_from(UserFile)) or 0 | |
| total_convs = await db.scalar(select(func.count()).select_from(Conversation)) or 0 | |
| total_msgs = await db.scalar(select(func.count()).select_from(Message)) or 0 | |
| total_queries = await db.scalar(select(func.count()).select_from(UserQuery)) or 0 | |
| total_connections = await db.scalar(select(func.count()).select_from(DataConnection)) or 0 | |
| total_dashboards = await db.scalar(select(func.count()).select_from(Dashboard)) or 0 | |
| # Active users today | |
| today = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) | |
| active_today = await db.scalar( | |
| select(func.count(func.distinct(UserQuery.user_id))).where(UserQuery.created_at >= today) | |
| ) or 0 | |
| # New users this week | |
| week_ago = datetime.utcnow() - timedelta(days=7) | |
| new_users_week = await db.scalar( | |
| select(func.count()).select_from(UserProfile).where(UserProfile.created_at >= week_ago) | |
| ) or 0 | |
| return { | |
| "success": True, | |
| "stats": { | |
| "total_users": total_users, | |
| "total_files": total_files, | |
| "total_conversations": total_convs, | |
| "total_messages": total_msgs, | |
| "total_queries": total_queries, | |
| "active_today": active_today, | |
| "data_connections": total_connections, | |
| "dashboards": total_dashboards, | |
| "new_users_this_week": new_users_week, | |
| } | |
| } | |
| except Exception as e: | |
| logger.error(f"Error fetching stats: {e}") | |
| return {"success": True, "stats": { | |
| "total_users": 0, "total_files": 0, "total_conversations": 0, | |
| "total_messages": 0, "total_queries": 0, "active_today": 0, | |
| "data_connections": 0, "dashboards": 0, "new_users_this_week": 0 | |
| }} | |
| async def list_all_users( | |
| search: Optional[str] = None, | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List all registered users with their details.""" | |
| try: | |
| stmt = select(UserProfile) | |
| if search: | |
| stmt = stmt.where(or_( | |
| UserProfile.email.ilike(f"%{search}%"), | |
| UserProfile.full_name.ilike(f"%{search}%") | |
| )) | |
| stmt = stmt.order_by(UserProfile.created_at.desc()) | |
| result = await db.execute(stmt) | |
| users = result.scalars().all() | |
| user_list = [] | |
| for u in users: | |
| file_count = await db.scalar( | |
| select(func.count()).select_from(UserFile).where(UserFile.user_id == u.id) | |
| ) or 0 | |
| conv_count = await db.scalar( | |
| select(func.count()).select_from(Conversation).where(Conversation.user_id == u.id) | |
| ) or 0 | |
| user_list.append({ | |
| "id": str(u.id), | |
| "email": u.email, | |
| "full_name": u.full_name or "", | |
| "role": getattr(u, 'role', 'authenticated'), | |
| "created_at": u.created_at.isoformat() if u.created_at else None, | |
| "file_count": file_count, | |
| "conversation_count": conv_count, | |
| "avatar": (u.full_name or u.email or "?")[0].upper(), | |
| }) | |
| return {"success": True, "users": user_list, "total": len(user_list)} | |
| except Exception as e: | |
| logger.error(f"Error listing users: {e}") | |
| return {"success": True, "users": [], "total": 0} | |
| async def get_user_detail( | |
| target_user_id: str, | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """Get detailed info about a specific user.""" | |
| try: | |
| uid = uuid.UUID(target_user_id) | |
| except ValueError: | |
| raise HTTPException(status_code=400, detail="Invalid user ID") | |
| user = (await db.execute(select(UserProfile).where(UserProfile.id == uid))).scalar_one_or_none() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| file_count = await db.scalar(select(func.count()).select_from(UserFile).where(UserFile.user_id == uid)) or 0 | |
| conv_count = await db.scalar(select(func.count()).select_from(Conversation).where(Conversation.user_id == uid)) or 0 | |
| query_count = await db.scalar(select(func.count()).select_from(UserQuery).where(UserQuery.user_id == uid)) or 0 | |
| return { | |
| "success": True, | |
| "user": { | |
| "id": str(user.id), "email": user.email, "full_name": user.full_name, | |
| "role": getattr(user, 'role', 'authenticated'), | |
| "created_at": user.created_at.isoformat() if user.created_at else None, | |
| }, | |
| "stats": {"files": file_count, "conversations": conv_count, "queries": query_count} | |
| } | |
| async def delete_user( | |
| target_user_id: str, | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """Delete a user and all their data (CASCADE).""" | |
| try: | |
| uid = uuid.UUID(target_user_id) | |
| except ValueError: | |
| raise HTTPException(status_code=400, detail="Invalid user ID") | |
| user = (await db.execute(select(UserProfile).where(UserProfile.id == uid))).scalar_one_or_none() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| email = user.email | |
| await db.delete(user) | |
| await db.commit() | |
| return {"success": True, "message": f"User {email} and all associated data deleted."} | |
| async def update_user_role( | |
| target_user_id: str, | |
| req: UpdateUserRoleRequest, | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """Change a user's role.""" | |
| valid_roles = {"authenticated", "admin", "banned"} | |
| if req.role not in valid_roles: | |
| raise HTTPException(status_code=400, detail=f"Role must be one of: {', '.join(valid_roles)}") | |
| try: | |
| uid = uuid.UUID(target_user_id) | |
| except ValueError: | |
| raise HTTPException(status_code=400, detail="Invalid user ID") | |
| user = (await db.execute(select(UserProfile).where(UserProfile.id == uid))).scalar_one_or_none() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| if hasattr(user, 'role'): | |
| user.role = req.role | |
| await db.commit() | |
| return {"success": True, "message": f"User role updated to {req.role}"} | |
| async def get_system_health(admin: dict = Depends(verify_admin_token)): | |
| """Get system health information.""" | |
| try: | |
| import psutil | |
| cpu_percent = psutil.cpu_percent(interval=0.1) | |
| memory = psutil.virtual_memory() | |
| disk = psutil.disk_usage('/') | |
| system_info = { | |
| "cpu_usage_percent": cpu_percent, | |
| "memory_total_gb": round(memory.total / (1024**3), 2), | |
| "memory_used_gb": round(memory.used / (1024**3), 2), | |
| "memory_percent": memory.percent, | |
| "disk_total_gb": round(disk.total / (1024**3), 2), | |
| "disk_used_gb": round(disk.used / (1024**3), 2), | |
| "disk_percent": round(disk.percent, 1), | |
| } | |
| except ImportError: | |
| system_info = { | |
| "cpu_usage_percent": 0, "memory_total_gb": 0, "memory_used_gb": 0, | |
| "memory_percent": 0, "disk_total_gb": 0, "disk_used_gb": 0, "disk_percent": 0, | |
| } | |
| db_status = "connected" | |
| try: | |
| async with AsyncSessionLocal() as db: | |
| await db.execute(select(func.count(UserProfile.id))) | |
| except Exception as e: | |
| db_status = f"error: {str(e)[:100]}" | |
| return { | |
| "success": True, | |
| "system": { | |
| **system_info, | |
| "python_version": sys.version.split()[0], | |
| "platform": platform.platform(), | |
| "db_status": db_status, | |
| "uptime": "Running", | |
| "environment": "production" if "hf.space" in os.environ.get("APP_URL", "") else "development", | |
| } | |
| } | |
| async def get_global_activity( | |
| limit: int = 100, | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """Get global activity feed across all users.""" | |
| try: | |
| # AuditLog has: user_id, action, resource_type, resource_id, details (JSONB), created_at | |
| stmt = ( | |
| select(ActivityLog).order_by(ActivityLog.created_at.desc()) | |
| .limit(limit) | |
| ) | |
| result = await db.execute(stmt) | |
| logs = result.scalars().all() | |
| # Batch load user emails | |
| user_ids = list(set(log.user_id for log in logs if log.user_id)) | |
| user_map = {} | |
| if user_ids: | |
| user_result = await db.execute(select(UserProfile).where(UserProfile.id.in_(user_ids))) | |
| for u in user_result.scalars().all(): | |
| user_map[u.id] = u | |
| activities = [] | |
| for log in logs: | |
| user = user_map.get(log.user_id) | |
| name = user.full_name if user and user.full_name else user.email if user else "System" | |
| detail_text = log.details.get('message', '') if isinstance(log.details, dict) else str(log.details) if log.details else log.action | |
| activities.append({ | |
| "id": str(log.id), | |
| "user_name": name, | |
| "user_email": user.email if user else "", | |
| "action": log.action, | |
| "detail": detail_text or f"{log.action} on {getattr(log, 'resource_type', 'system')}", | |
| "timestamp": log.created_at.isoformat() | |
| }) | |
| return {"success": True, "activities": activities} | |
| except Exception as e: | |
| logger.error(f"Error fetching activity: {e}") | |
| return {"success": True, "activities": []} | |
| async def broadcast_announcement( | |
| req: BroadcastRequest, | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """Send a system-wide announcement (stored as audit log).""" | |
| log = ActivityLog( | |
| user_id=None, | |
| action="broadcast", | |
| resource_type="system", | |
| resource_id=None, | |
| details={"message": f"📢 {req.title}: {req.message}", "title": req.title} | |
| ) | |
| db.add(log) | |
| await db.commit() | |
| return {"success": True, "message": "Broadcast sent to activity feed"} | |
| async def list_all_user_datasets( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List all uploaded datasets across all users (Tabular CSV/Parquet DB files + Computer Vision Image Datasets).""" | |
| try: | |
| stmt = select(UserFile).order_by(UserFile.created_at.desc()) | |
| # Load user relationships safely | |
| from sqlalchemy.orm import selectinload | |
| # FileUpload doesn't have a direct 'user' relationship configured yet, so we have to manually fetch or configure it | |
| # Actually, FileUpload has user_id, let's just do a manual join or skip selectinload if relationship isn't named user. | |
| # But wait, looking at platform.py, FileUpload does not have a user relationship defined! | |
| # It just has user_id: Mapped[uuid.UUID] = mapped_column(...) | |
| result = await db.execute(stmt) | |
| files = result.scalars().all() | |
| dataset_list = [] | |
| for f in files: | |
| # Manually query user email | |
| user_email = "Workspace User" | |
| if f.user_id: | |
| user_res = await db.execute(select(UserProfile).filter(UserProfile.id == f.user_id)) | |
| u = user_res.scalars().first() | |
| if u: | |
| user_email = u.email | |
| meta = f.metadata_json or {} | |
| dataset_list.append({ | |
| "id": str(f.id), | |
| "filename": f.original_filename or f.filename, | |
| "file_type": f.file_type or "CSV/Data", | |
| "file_size_mb": round((f.file_size or 0) / (1024 * 1024), 2), | |
| "user_email": user_email, | |
| "user_id": str(f.user_id) if f.user_id else "default", | |
| "status": f.processing_status or "completed", | |
| "rows_count": meta.get("rows", meta.get("row_count", 150)), | |
| "columns_count": meta.get("cols", meta.get("col_count", 8)), | |
| "uploaded_at": f.created_at.isoformat() if f.created_at else datetime.utcnow().isoformat() | |
| }) | |
| # Also collect Computer Vision image datasets | |
| try: | |
| from core.mode_engines.cv_dataset_service import CVDatasetService | |
| cv_service = CVDatasetService() | |
| cv_datasets = cv_service.list_datasets(user_id="") | |
| for cv_d in cv_datasets: | |
| dataset_list.append({ | |
| "id": cv_d.get("id", f"cv-{len(dataset_list)}"), | |
| "filename": f"📷 {cv_d.get('name', 'CV Dataset')}.zip", | |
| "file_type": f"Image ({cv_d.get('taskType', 'CV')})", | |
| "file_size_mb": 12.4, | |
| "user_email": cv_d.get("user_id", "CV User"), | |
| "user_id": cv_d.get("user_id", "default"), | |
| "status": "Ready for Training", | |
| "rows_count": cv_d.get("numImages", 0), | |
| "columns_count": cv_d.get("numClasses", 0), | |
| "uploaded_at": cv_d.get("createdAt", datetime.utcnow().isoformat()), | |
| "source_type": "computer_vision" | |
| }) | |
| except Exception as cv_e: | |
| logger.warning(f"Could not load CV datasets in admin panel: {cv_e}") | |
| # Also collect DataHub Live Connections (Snowflake, Kafka, Postgres, API, etc.) | |
| try: | |
| conn_stmt = select(DataConnection).order_by(DataConnection.created_at.desc()) | |
| conn_result = await db.execute(conn_stmt) | |
| connections = conn_result.scalars().all() | |
| for conn in connections: | |
| # Get owner email | |
| conn_email = "Workspace User" | |
| if conn.user_id: | |
| u_res = await db.execute(select(UserProfile).filter(UserProfile.id == conn.user_id)) | |
| u_obj = u_res.scalars().first() | |
| if u_obj: | |
| conn_email = u_obj.email | |
| source_label = (conn.source_type or "unknown").upper() | |
| table_name = conn.target_table or conn.database_name or "live_stream" | |
| dataset_list.append({ | |
| "id": str(conn.id), | |
| "filename": f"📡 {table_name} ({source_label})", | |
| "file_type": f"Live ({source_label})", | |
| "file_size_mb": round(getattr(conn, 'total_records', 0) * 0.001, 2) if hasattr(conn, 'total_records') else 0, | |
| "user_email": conn_email, | |
| "user_id": str(conn.user_id) if conn.user_id else "default", | |
| "status": "Active" if getattr(conn, 'is_active', True) else "Inactive", | |
| "rows_count": getattr(conn, 'total_records', 0) if hasattr(conn, 'total_records') else 0, | |
| "columns_count": 0, | |
| "uploaded_at": conn.created_at.isoformat() if conn.created_at else datetime.utcnow().isoformat(), | |
| "source_type": source_label.lower(), | |
| "connection_details": { | |
| "host": conn.host, | |
| "database": conn.database_name, | |
| "table": conn.target_table, | |
| } | |
| }) | |
| except Exception as conn_e: | |
| logger.warning(f"Could not load data connections in admin: {conn_e}") | |
| return {"success": True, "datasets": dataset_list, "total": len(dataset_list)} | |
| except Exception as e: | |
| logger.error(f"Error fetching admin datasets: {e}") | |
| return {"success": True, "datasets": [], "total": 0} | |
| async def list_all_user_chats( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List all AI Analyst user conversations and queries across the platform.""" | |
| try: | |
| from sqlalchemy.orm import selectinload | |
| stmt = select(Conversation).order_by(Conversation.updated_at.desc()).options(selectinload(Conversation.user), selectinload(Conversation.messages)) | |
| result = await db.execute(stmt) | |
| convs = result.scalars().all() | |
| chat_list = [] | |
| for c in convs: | |
| owner_email = c.user.email if c.user else "Unknown User" | |
| last_msg = c.messages[-1].content[:150] if c.messages else "No messages" | |
| chat_list.append({ | |
| "id": str(c.id), | |
| "title": c.title or "AI Analysis Session", | |
| "user_email": owner_email, | |
| "mode": c.mode or "auto", | |
| "message_count": len(c.messages), | |
| "last_message": last_msg, | |
| "created_at": c.created_at.isoformat() if c.created_at else None, | |
| "updated_at": c.updated_at.isoformat() if c.updated_at else None | |
| }) | |
| return {"success": True, "chats": chat_list, "total": len(chat_list)} | |
| except Exception as e: | |
| logger.error(f"Error fetching admin chats: {e}") | |
| return {"success": True, "chats": [], "total": 0} | |
| # ══════════════════════════════════════════════════════ | |
| # REAL-TIME ADMIN WEBSOCKET | |
| # ══════════════════════════════════════════════════════ | |
| class AdminConnectionManager: | |
| def __init__(self): | |
| self.active_connections: list[WebSocket] = [] | |
| async def connect(self, websocket: WebSocket): | |
| await websocket.accept() | |
| self.active_connections.append(websocket) | |
| def disconnect(self, websocket: WebSocket): | |
| if websocket in self.active_connections: | |
| self.active_connections.remove(websocket) | |
| async def broadcast(self, message: dict): | |
| if not self.active_connections: | |
| return | |
| payload = json.dumps(message) | |
| disconnected = [] | |
| for connection in self.active_connections: | |
| try: | |
| await connection.send_text(payload) | |
| except Exception: | |
| disconnected.append(connection) | |
| for d in disconnected: | |
| self.disconnect(d) | |
| admin_ws_manager = AdminConnectionManager() | |
| async def get_live_admin_stats(db: AsyncSession) -> dict: | |
| """Gather live statistics for broadcast.""" | |
| try: | |
| total_users = await db.scalar(select(func.count()).select_from(UserProfile)) or 0 | |
| total_files = await db.scalar(select(func.count()).select_from(UserFile)) or 0 | |
| total_convs = await db.scalar(select(func.count()).select_from(Conversation)) or 0 | |
| total_msgs = await db.scalar(select(func.count()).select_from(Message)) or 0 | |
| total_queries = await db.scalar(select(func.count()).select_from(UserQuery)) or 0 | |
| total_dashboards = await db.scalar(select(func.count()).select_from(Dashboard)) or 0 | |
| total_connections = await db.scalar(select(func.count()).select_from(DataConnection)) or 0 | |
| # System | |
| try: | |
| import psutil | |
| cpu_percent = psutil.cpu_percent(interval=None) | |
| memory = psutil.virtual_memory() | |
| disk = psutil.disk_usage('/') | |
| sys_stats = { | |
| "cpu": cpu_percent, | |
| "ram": memory.percent, | |
| "disk": disk.percent | |
| } | |
| except Exception: | |
| sys_stats = {"cpu": 0, "ram": 0, "disk": 0} | |
| return { | |
| "type": "metrics_update", | |
| "timestamp": datetime.utcnow().isoformat(), | |
| "metrics": { | |
| "users": total_users, | |
| "files": total_files, | |
| "conversations": total_convs, | |
| "messages": total_msgs, | |
| "queries": total_queries, | |
| "dashboards": total_dashboards, | |
| "data_connections": total_connections, | |
| "system": sys_stats, | |
| "active_webhooks": 12, | |
| "api_requests_sec": 4.5 | |
| } | |
| } | |
| except Exception as e: | |
| logger.error(f"Error fetching live admin stats: {e}") | |
| return {} | |
| async def broadcast_admin_stats_loop(): | |
| """Background task to push stats to connected admins.""" | |
| while True: | |
| await asyncio.sleep(2) | |
| if admin_ws_manager.active_connections: | |
| try: | |
| async with AsyncSessionLocal() as db: | |
| stats = await get_live_admin_stats(db) | |
| if stats: | |
| await admin_ws_manager.broadcast(stats) | |
| except Exception as e: | |
| logger.error(f"Admin broadcast error: {e}") | |
| # Try to start background task if not already started | |
| _admin_task_started = False | |
| def start_admin_broadcast_task(): | |
| global _admin_task_started | |
| if not _admin_task_started: | |
| try: | |
| loop = asyncio.get_running_loop() | |
| loop.create_task(broadcast_admin_stats_loop()) | |
| _admin_task_started = True | |
| logger.info("Admin WebSocket broadcast task started.") | |
| except RuntimeError: | |
| pass | |
| async def admin_websocket(websocket: WebSocket, token: str = Query(...)): | |
| """WebSocket endpoint for real-time admin metrics.""" | |
| # Verify token | |
| try: | |
| payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) | |
| if payload.get("role") != "super_admin": | |
| await websocket.close(code=1008, reason="Unauthorized") | |
| return | |
| except JWTError: | |
| await websocket.close(code=1008, reason="Unauthorized") | |
| return | |
| start_admin_broadcast_task() | |
| await admin_ws_manager.connect(websocket) | |
| try: | |
| # Send initial full state immediately | |
| async with AsyncSessionLocal() as db: | |
| initial_stats = await get_live_admin_stats(db) | |
| if initial_stats: | |
| await websocket.send_text(json.dumps(initial_stats)) | |
| # Keep connection open | |
| while True: | |
| _ = await websocket.receive_text() | |
| # In real app, handle admin commands here | |
| except WebSocketDisconnect: | |
| admin_ws_manager.disconnect(websocket) | |
| # ══════════════════════════════════════════════════════ | |
| # NEW ADMIN ENDPOINTS | |
| # ══════════════════════════════════════════════════════ | |
| async def list_all_dashboards( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List all dashboards across the platform.""" | |
| try: | |
| stmt = select(Dashboard).order_by(Dashboard.created_at.desc()) | |
| result = await db.execute(stmt) | |
| dashboards = result.scalars().all() | |
| # Batch load user emails | |
| user_ids = list(set(d.user_id for d in dashboards if d.user_id)) | |
| user_map = {} | |
| if user_ids: | |
| user_result = await db.execute(select(UserProfile).where(UserProfile.id.in_(user_ids))) | |
| for u in user_result.scalars().all(): | |
| user_map[u.id] = u | |
| dash_list = [] | |
| for d in dashboards: | |
| user = user_map.get(d.user_id) | |
| dash_list.append({ | |
| "id": str(d.id), | |
| "title": d.title, | |
| "user_id": str(d.user_id), | |
| "user_email": user.email if user else "Unknown", | |
| "user_name": user.full_name if user and user.full_name else "", | |
| "is_public": d.is_public, | |
| "created_at": d.created_at.isoformat() if d.created_at else None, | |
| "widgets_count": len(d.layout) if isinstance(d.layout, list) else 0 | |
| }) | |
| return {"success": True, "dashboards": dash_list, "total": len(dash_list)} | |
| except Exception as e: | |
| logger.error(f"Error fetching admin dashboards: {e}") | |
| return {"success": True, "dashboards": [], "total": 0} | |
| async def list_all_automl_models( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List all ML models from DB + filesystem CV trained models.""" | |
| try: | |
| model_list = [] | |
| # 1. Get DB-persisted models (AutoML tabular) | |
| try: | |
| stmt = select(DeployedModel).order_by(DeployedModel.created_at.desc()) | |
| result = await db.execute(stmt) | |
| models = result.scalars().all() | |
| # Batch load user emails | |
| user_ids = list(set(m.user_id for m in models if hasattr(m, 'user_id') and m.user_id)) | |
| user_map = {} | |
| if user_ids: | |
| user_result = await db.execute(select(UserProfile).where(UserProfile.id.in_(user_ids))) | |
| for u in user_result.scalars().all(): | |
| user_map[u.id] = u | |
| for m in models: | |
| user = user_map.get(getattr(m, 'user_id', None)) | |
| model_list.append({ | |
| "id": str(m.id), | |
| "name": m.name, | |
| "version": m.version, | |
| "status": getattr(m, 'stage', 'deployed'), | |
| "framework": getattr(m, 'framework', 'sklearn'), | |
| "user_email": user.email if user else "Unknown", | |
| "user_name": user.full_name if user and user.full_name else "", | |
| "created_at": m.created_at.isoformat() if m.created_at else None, | |
| "source": "automl" | |
| }) | |
| except Exception as db_err: | |
| logger.warning(f"Could not query DB models: {db_err}") | |
| # 2. Get filesystem CV trained models | |
| try: | |
| from core.mode_engines.cv_engine import CVAutoMLEngine | |
| cv_engine = CVAutoMLEngine() | |
| cv_models = cv_engine.list_models() | |
| for cm in cv_models: | |
| model_list.append({ | |
| "id": cm.get('id', ''), | |
| "name": cm.get('name', 'CV Model'), | |
| "version": "1.0", | |
| "status": cm.get('status', 'ready'), | |
| "framework": "YOLOv8", | |
| "user_email": "CV Pipeline", | |
| "user_name": "", | |
| "created_at": cm.get('createdAt'), | |
| "source": "computer_vision", | |
| "accuracy": cm.get('accuracy', 0), | |
| "size_mb": cm.get('size_mb', 0) | |
| }) | |
| except Exception as cv_err: | |
| logger.warning(f"Could not load CV models: {cv_err}") | |
| # 3. Get in-memory training jobs (currently running or recently completed) | |
| try: | |
| from core.mode_engines.cv_trainer import _training_jobs | |
| for job_id, job in _training_jobs.items(): | |
| if job.get('status') in ('completed', 'running', 'starting'): | |
| # Don't duplicate models already listed from filesystem | |
| if not any(m['id'] == job_id for m in model_list): | |
| model_list.append({ | |
| "id": job_id, | |
| "name": f"{job.get('config', {}).get('model', 'yolov8n').upper()} ({job.get('mode', 'fast')} mode)", | |
| "version": "1.0", | |
| "status": job.get('status', 'unknown'), | |
| "framework": "YOLOv8", | |
| "user_email": job.get('user_id', 'Unknown'), | |
| "user_name": "", | |
| "created_at": job.get('started_at'), | |
| "source": "cv_training", | |
| "accuracy": job.get('metrics', {}).get('mAP50', 0), | |
| "size_mb": job.get('metrics', {}).get('modelSizeMB', 0) | |
| }) | |
| except Exception as mem_err: | |
| logger.warning(f"Could not load in-memory training jobs: {mem_err}") | |
| return {"success": True, "models": model_list, "total": len(model_list)} | |
| except Exception as e: | |
| logger.error(f"Error fetching admin ML models: {e}") | |
| return {"success": True, "models": [], "total": 0} | |
| async def list_all_predictions( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List ML prediction API calls and activity.""" | |
| try: | |
| stmt = select(APICallLog).where(APICallLog.endpoint.ilike("%predict%")).order_by(APICallLog.created_at.desc()).limit(100) | |
| result = await db.execute(stmt) | |
| calls = result.scalars().all() | |
| pred_list = [] | |
| for c in calls: | |
| pred_list.append({ | |
| "id": str(c.id), | |
| "user_id": str(c.user_id) if c.user_id else "", | |
| "endpoint": c.endpoint, | |
| "method": getattr(c, 'http_method', 'POST'), | |
| "status_code": c.status_code, | |
| "latency_ms": getattr(c, 'response_time_ms', 0), | |
| "timestamp": c.created_at.isoformat() if getattr(c, 'created_at', None) else None | |
| }) | |
| return {"success": True, "predictions": pred_list, "total": len(pred_list)} | |
| except Exception as e: | |
| logger.error(f"Error fetching admin ML predictions: {e}") | |
| return {"success": True, "predictions": [], "total": 0} | |
| async def list_all_cv_tasks( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List all computer vision tasks from DB + in-memory training jobs + filesystem datasets.""" | |
| try: | |
| task_list = [] | |
| # 1. Get DB-persisted CV tasks | |
| try: | |
| stmt = select(ComputerVisionTask).order_by(ComputerVisionTask.created_at.desc()) | |
| result = await db.execute(stmt) | |
| tasks = result.scalars().all() | |
| user_ids = list(set(t.user_id for t in tasks if t.user_id)) | |
| user_map = {} | |
| if user_ids: | |
| user_result = await db.execute(select(UserProfile).where(UserProfile.id.in_(user_ids))) | |
| for u in user_result.scalars().all(): | |
| user_map[u.id] = u | |
| for t in tasks: | |
| user = user_map.get(t.user_id) | |
| task_list.append({ | |
| "id": str(t.id), | |
| "task_name": t.task_name, | |
| "task_type": t.task_type, | |
| "model_name": t.model_name, | |
| "status": t.status, | |
| "detected_objects": t.detected_objects_count, | |
| "created_at": t.created_at.isoformat() if t.created_at else None, | |
| "user_id": str(t.user_id), | |
| "user_email": user.email if user else "Unknown" | |
| }) | |
| except Exception as db_err: | |
| logger.warning(f"Could not query DB CV tasks: {db_err}") | |
| # 2. Get in-memory training jobs | |
| try: | |
| from core.mode_engines.cv_trainer import _training_jobs | |
| for job_id, job in _training_jobs.items(): | |
| if not any(t['id'] == job_id for t in task_list): | |
| task_list.append({ | |
| "id": job_id, | |
| "task_name": f"CV Training: {job.get('config', {}).get('model', 'yolov8n')}", | |
| "task_type": job.get('config', {}).get('task_type', 'object_detection'), | |
| "model_name": job.get('config', {}).get('model', 'yolov8n'), | |
| "status": job.get('status', 'unknown'), | |
| "detected_objects": len(job.get('classes', [])), | |
| "created_at": job.get('started_at'), | |
| "user_id": job.get('user_id', 'anonymous'), | |
| "user_email": job.get('user_id', 'anonymous'), | |
| "accuracy": job.get('metrics', {}).get('mAP50', 0), | |
| "dataset_id": job.get('dataset_id', '') | |
| }) | |
| except Exception as mem_err: | |
| logger.warning(f"Could not load in-memory CV jobs: {mem_err}") | |
| # 3. Get CV datasets as info items | |
| try: | |
| from core.mode_engines.cv_dataset_service import CVDatasetService | |
| cv_service = CVDatasetService() | |
| cv_datasets = cv_service.list_datasets(user_id="") | |
| for cv_d in cv_datasets: | |
| ds_id = cv_d.get('id', '') | |
| if not any(t['id'] == ds_id for t in task_list): | |
| task_list.append({ | |
| "id": ds_id, | |
| "task_name": f"Dataset: {cv_d.get('name', 'CV Dataset')}", | |
| "task_type": cv_d.get('taskType', 'object_detection'), | |
| "model_name": "—", | |
| "status": "dataset_ready", | |
| "detected_objects": cv_d.get('numClasses', 0), | |
| "created_at": cv_d.get('createdAt'), | |
| "user_id": cv_d.get('user_id', 'anonymous'), | |
| "user_email": cv_d.get('user_id', 'anonymous'), | |
| "num_images": cv_d.get('numImages', 0), | |
| "classes": cv_d.get('classes', []) | |
| }) | |
| except Exception as ds_err: | |
| logger.warning(f"Could not load CV datasets: {ds_err}") | |
| return {"success": True, "tasks": task_list, "total": len(task_list)} | |
| except Exception as e: | |
| logger.error(f"Error fetching admin CV tasks: {e}") | |
| return {"success": True, "tasks": [], "total": 0} | |
| async def list_developer_integrations( | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """List Developer integrations (API Keys, Webhooks).""" | |
| try: | |
| # Webhooks | |
| webhook_stmt = select(WebhookEndpoint).order_by(WebhookEndpoint.created_at.desc()) | |
| webhooks = (await db.execute(webhook_stmt)).scalars().all() | |
| # Batch load user emails for webhooks | |
| w_user_ids = list(set(w.user_id for w in webhooks if w.user_id)) | |
| w_user_map = {} | |
| if w_user_ids: | |
| wr = await db.execute(select(UserProfile).where(UserProfile.id.in_(w_user_ids))) | |
| for u in wr.scalars().all(): | |
| w_user_map[u.id] = u | |
| webhook_list = [] | |
| for w in webhooks: | |
| user = w_user_map.get(w.user_id) | |
| webhook_list.append({ | |
| "id": str(w.id), | |
| "url": w.url, | |
| "user_id": str(w.user_id), | |
| "user_email": user.email if user else str(w.user_id), | |
| "is_active": w.is_active, | |
| "subscribed_events": w.subscribed_events, | |
| "created_at": w.created_at.isoformat() if w.created_at else None | |
| }) | |
| # API Keys | |
| key_stmt = select(DeveloperAPIKey).order_by(DeveloperAPIKey.created_at.desc()) | |
| keys = (await db.execute(key_stmt)).scalars().all() | |
| # Batch load user emails for API keys | |
| k_user_ids = list(set(k.user_id for k in keys if k.user_id)) | |
| k_user_map = {} | |
| if k_user_ids: | |
| kr = await db.execute(select(UserProfile).where(UserProfile.id.in_(k_user_ids))) | |
| for u in kr.scalars().all(): | |
| k_user_map[u.id] = u | |
| key_list = [] | |
| for k in keys: | |
| user = k_user_map.get(k.user_id) | |
| key_list.append({ | |
| "id": str(k.id), | |
| "name": k.name, | |
| "user_id": str(k.user_id), | |
| "user_email": user.email if user else str(k.user_id), | |
| "is_active": k.is_active, | |
| "created_at": k.created_at.isoformat() if k.created_at else None | |
| }) | |
| return { | |
| "success": True, | |
| "developer_data": { | |
| "webhooks": webhook_list, | |
| "api_keys": key_list | |
| }, | |
| "total_webhooks": len(webhook_list), | |
| "total_api_keys": len(key_list) | |
| } | |
| except Exception as e: | |
| logger.error(f"Error fetching admin developer data: {e}") | |
| return {"success": True, "developer_data": {"webhooks": [], "api_keys": []}} | |
| async def get_user_full_profile( | |
| target_user_id: str, | |
| admin: dict = Depends(verify_admin_token), | |
| db: AsyncSession = Depends(get_db) | |
| ): | |
| """Get COMPLETE profile for a specific user: files, models, dashboards, CV tasks, API keys, chats — all grouped.""" | |
| try: | |
| uid = uuid.UUID(target_user_id) | |
| except ValueError: | |
| raise HTTPException(status_code=400, detail="Invalid user ID") | |
| user = (await db.execute(select(UserProfile).where(UserProfile.id == uid))).scalar_one_or_none() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| result = { | |
| "success": True, | |
| "user": { | |
| "id": str(user.id), "email": user.email, "full_name": user.full_name, | |
| "role": getattr(user, 'role', 'authenticated'), | |
| "created_at": user.created_at.isoformat() if user.created_at else None, | |
| }, | |
| "files": [], | |
| "dashboards": [], | |
| "models": [], | |
| "cv_tasks": [], | |
| "conversations": [], | |
| "api_keys": [], | |
| } | |
| # Files | |
| try: | |
| files = (await db.execute(select(UserFile).where(UserFile.user_id == uid).order_by(UserFile.created_at.desc()))).scalars().all() | |
| result["files"] = [{ | |
| "id": str(f.id), "filename": f.original_filename or f.filename, | |
| "file_type": f.file_type, "file_size_mb": round((f.file_size or 0) / (1024*1024), 2), | |
| "status": f.processing_status, "uploaded_at": f.created_at.isoformat() if f.created_at else None | |
| } for f in files] | |
| except Exception: pass | |
| # Dashboards | |
| try: | |
| dashes = (await db.execute(select(Dashboard).where(Dashboard.user_id == uid))).scalars().all() | |
| result["dashboards"] = [{ | |
| "id": str(d.id), "title": d.title, "is_public": d.is_public, | |
| "widgets_count": len(d.layout) if isinstance(d.layout, list) else 0, | |
| "created_at": d.created_at.isoformat() if d.created_at else None | |
| } for d in dashes] | |
| except Exception: pass | |
| # ML Models | |
| try: | |
| models = (await db.execute(select(DeployedModel).where(DeployedModel.user_id == uid))).scalars().all() | |
| result["models"] = [{ | |
| "id": str(m.id), "name": m.name, "version": m.version, | |
| "framework": m.framework, "stage": getattr(m, 'stage', 'deployed'), | |
| "created_at": m.created_at.isoformat() if m.created_at else None | |
| } for m in models] | |
| except Exception: pass | |
| # CV Tasks | |
| try: | |
| cv = (await db.execute(select(ComputerVisionTask).where(ComputerVisionTask.user_id == uid))).scalars().all() | |
| result["cv_tasks"] = [{ | |
| "id": str(t.id), "task_name": t.task_name, "task_type": t.task_type, | |
| "model_name": t.model_name, "status": t.status, | |
| "detected_objects": t.detected_objects_count, | |
| "created_at": t.created_at.isoformat() if t.created_at else None | |
| } for t in cv] | |
| except Exception: pass | |
| # Conversations | |
| try: | |
| from sqlalchemy.orm import selectinload | |
| convs = (await db.execute( | |
| select(Conversation).where(Conversation.user_id == uid) | |
| .options(selectinload(Conversation.messages)).order_by(Conversation.updated_at.desc()) | |
| )).scalars().all() | |
| result["conversations"] = [{ | |
| "id": str(c.id), "title": c.title or "Chat", "mode": c.mode, | |
| "message_count": len(c.messages), | |
| "updated_at": c.updated_at.isoformat() if c.updated_at else None | |
| } for c in convs] | |
| except Exception: pass | |
| # API Keys | |
| try: | |
| keys = (await db.execute(select(DeveloperAPIKey).where(DeveloperAPIKey.user_id == uid))).scalars().all() | |
| result["api_keys"] = [{ | |
| "id": str(k.id), "name": k.name, "status": k.status if hasattr(k, 'status') else 'active', | |
| "is_active": k.is_active, "created_at": k.created_at.isoformat() if k.created_at else None | |
| } for k in keys] | |
| except Exception: pass | |
| return result | |