File size: 4,734 Bytes
4d18cf9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""
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")