204 lines
6.2 KiB
Python
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
|
|
)
|
|
|