alexchilton
Fix SPA routing guard and set demo env
6b65db9
"""FastAPI application entry point."""
from __future__ import annotations
import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from app.api import analysis, export, health, webhooks
from app.core.config import settings
from app.core.logging import get_logger, setup_logging
from app.core.middleware import register_middleware
from app.core.security import limiter
from app.core.telemetry import setup_telemetry
from app.services.redis_client import close_redis
setup_logging(settings.log_level, settings.log_format)
logger = get_logger(__name__)
STATIC_DIR = Path(__file__).parent.parent / "static"
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(
"application_starting",
app_name=settings.app_name,
env=settings.app_env,
)
settings.upload_path # Ensure upload directory exists
yield
logger.info("application_shutting_down")
await close_redis()
app = FastAPI(
title=settings.app_name,
description="Sentiment & Topic Analysis Dashboard API",
version="1.0.0",
lifespan=lifespan,
docs_url="/docs" if not settings.is_production else None,
redoc_url="/redoc" if not settings.is_production else None,
)
# Middleware
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
register_middleware(app)
setup_telemetry(app)
# Routes
app.include_router(health.router)
app.include_router(analysis.router)
app.include_router(export.router)
app.include_router(webhooks.router)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
from app.core.logging import get_correlation_id
logger.error(
"unhandled_exception",
path=request.url.path,
error=str(exc),
exc_info=True,
)
return JSONResponse(
status_code=500,
content={
"detail": "Internal server error",
"correlation_id": get_correlation_id(),
},
)
# Serve frontend static files in production (when static/ dir exists)
if STATIC_DIR.is_dir():
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="static-assets")
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""Serve the SPA index.html for all non-API routes."""
# Don't intercept API, docs, health, or metrics paths
if full_path.startswith(("api/", "docs", "redoc", "openapi.json", "health", "metrics")):
return JSONResponse(status_code=404, content={"detail": "Not found"})
file_path = STATIC_DIR / full_path
if file_path.is_file():
return FileResponse(str(file_path))
return FileResponse(str(STATIC_DIR / "index.html"))