""" backend/main.py ───────────────────────────────────────────── FastAPI application — Rift Breakdown Backend Carga los 3 modelos ML al startup y expone endpoints para predicción de win probability minuto-a-minuto. ───────────────────────────────────────────── """ import logging import os from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from backend.routers import predictions from backend.services.model_service import model_service # ───────────────────────────────────────────── # Logging # ───────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(name)s | %(levelname)s | %(message)s", ) logger = logging.getLogger(__name__) # ───────────────────────────────────────────── # Lifespan (startup / shutdown) # ───────────────────────────────────────────── @asynccontextmanager async def lifespan(app: FastAPI): """Load models once on startup, release on shutdown.""" logger.info("🚀 Starting Rift Breakdown backend…") try: model_service.load_models() logger.info("✅ All models loaded — server ready.") except Exception as e: logger.error("❌ Model loading failed: %s", e, exc_info=True) # Server starts anyway but /predict will return 503 yield logger.info("🛑 Shutting down Rift Breakdown backend.") # ───────────────────────────────────────────── # App # ───────────────────────────────────────────── app = FastAPI( title="Rift Breakdown — LoL Win Probability Predictor", version="0.2.0", description="Post-match analysis with 3 ML models (XGBoost, LSTM, LogReg)", lifespan=lifespan, # Docs bajo /api para no colisionar con React SPA docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json", ) # ───────────────────────────────────────────── # CORS (Secured for Production) # ───────────────────────────────────────────── allowed_origins_env = os.environ.get( "ALLOWED_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173,http://localhost:3000,https://huggingface.co" ) origins = [origin.strip() for origin in allowed_origins_env.split(",") if origin.strip()] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["GET", "POST", "OPTIONS"], allow_headers=["Content-Type", "Authorization", "Accept"], ) # ───────────────────────────────────────────── # Routers # ───────────────────────────────────────────── app.include_router(predictions.router, prefix="/api/v1", tags=["predictions"]) # ───────────────────────────────────────────── # Health check # ───────────────────────────────────────────── @app.get("/api/health", tags=["health"]) def health_check(): return { "status": "ok", "version": "0.2.0", "models_loaded": model_service.is_loaded, } DIST = Path(__file__).parent.parent / "frontend" / "dist" if DIST.exists(): # Archivos estáticos (JS, CSS, imágenes) app.mount("/assets", StaticFiles(directory=DIST / "assets"), name="assets") @app.get("/{full_path:path}", include_in_schema=False) def serve_spa(full_path: str): """Catch-all: cualquier ruta no-API sirve el index.html de React.""" return FileResponse(DIST / "index.html")