"""Main FastAPI application""" from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from contextlib import asynccontextmanager from typing import List from pathlib import Path import json from app.config import settings from app.database import init_db from app.api import api_router class ConnectionManager: """WebSocket connection manager""" 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): self.active_connections.remove(websocket) async def broadcast(self, message: dict): """Broadcast message to all connected clients""" for connection in self.active_connections: try: await connection.send_json(message) except Exception: pass # Create connection manager manager = ConnectionManager() @asynccontextmanager async def lifespan(app: FastAPI): """Lifespan events""" # Startup await init_db() # Start task worker from app.workers.task_worker import start_worker await start_worker() yield # Shutdown from app.workers.task_worker import stop_worker await stop_worker() # Create FastAPI app app = FastAPI( title="AI Code Review Agent", description="AI агент для автоматического ревью Pull Request", version="0.1.0", lifespan=lifespan ) # Configure CORS app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include API routes app.include_router(api_router, prefix="/api") # Serve static files (frontend build) public_dir = Path(__file__).parent.parent / "public" if public_dir.exists(): # Serve assets (JS, CSS, images) app.mount("/assets", StaticFiles(directory=str(public_dir / "assets")), name="assets") @app.get("/", response_class=FileResponse) async def serve_frontend_root(): """Serve frontend index.html""" return FileResponse(str(public_dir / "index.html")) @app.get("/{full_path:path}", response_class=FileResponse) async def serve_frontend_routes(full_path: str): """Serve frontend for all routes (SPA support)""" # Skip API and WebSocket routes if full_path.startswith(("api/", "ws/", "docs", "redoc", "openapi.json")): return None file_path = public_dir / full_path if file_path.exists() and file_path.is_file(): return FileResponse(str(file_path)) # Fallback to index.html for SPA routing return FileResponse(str(public_dir / "index.html")) else: @app.get("/") async def root(): """Root endpoint (when frontend not built)""" return { "message": "AI Code Review Agent API", "version": "0.1.0", "docs": "/docs", "note": "Frontend not built. Run 'npm run build' in frontend directory." } @app.get("/health") async def health_check(): """Health check endpoint""" return {"status": "healthy"} @app.get("/version") async def get_version(): """Get backend version""" try: version_file = Path(__file__).parent.parent / "VERSION" if version_file.exists(): version = version_file.read_text().strip() else: version = "unknown" return {"version": version} except Exception: return {"version": "unknown"} @app.websocket("/ws/reviews") async def websocket_endpoint(websocket: WebSocket): """WebSocket endpoint for real-time review updates""" await manager.connect(websocket) print(f"✅ WebSocket connected. Total connections: {len(manager.active_connections)}") try: # Send welcome message await websocket.send_json({ "type": "connection", "status": "connected", "message": "WebSocket подключен к серверу review", "timestamp": __import__('datetime').datetime.utcnow().isoformat() }) while True: # Keep connection alive and handle client messages data = await websocket.receive_text() print(f"📨 Received from client: {data}") # Handle ping/pong if data == "ping": await websocket.send_json({ "type": "pong", "timestamp": __import__('datetime').datetime.utcnow().isoformat() }) else: # Echo back for debugging await websocket.send_json({ "type": "echo", "message": f"Получено: {data}" }) except WebSocketDisconnect: manager.disconnect(websocket) print(f"❌ WebSocket disconnected. Remaining connections: {len(manager.active_connections)}") async def broadcast_review_update(review_id: int, event_type: str, data: dict = None): """Broadcast review update to all connected clients""" message = { "type": event_type, "review_id": review_id, "data": data or {} } await manager.broadcast(message) if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host=settings.host, port=settings.port, reload=settings.debug )