2025-10-13 17:26:41 +03:00

204 lines
6.2 KiB
Python

"""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"""
print(f"\n[BROADCAST] Sending to {len(self.active_connections)} clients")
print(f"[BROADCAST] Message type: {message.get('type')}")
print(f"[BROADCAST] Message: {str(message)[:200]}...")
sent_count = 0
error_count = 0
for i, connection in enumerate(self.active_connections):
try:
await connection.send_json(message)
sent_count += 1
print(f"[BROADCAST] ✓ Sent to client #{i+1}")
except Exception as e:
error_count += 1
print(f"[BROADCAST] ✗ Failed to send to client #{i+1}: {e}")
print(f"[BROADCAST] Result: {sent_count} sent, {error_count} failed")
# 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("/api/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 as e:
print(f"Error reading version: {e}")
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
)