import logging import time import traceback from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from app.routers.api import router as api_router from app.services.analytics import init_db logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): init_db() yield app = FastAPI( title="NEM Battery SCADA Data Explorer", description="Explore 4-second BESS SCADA data from Australia's National Electricity Market.", version="1.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["GET"], allow_headers=["*"], ) app.include_router(api_router) @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: """ Catch-all for any exception not already handled by a specific handler or HTTPException. Logs the full traceback so silent 500s become visible in the application log, then returns a JSON 500 response. """ logger.error( "Unhandled exception on %s %s\n%s", request.method, request.url, traceback.format_exc(), ) return JSONResponse( status_code=500, content={"detail": "An internal server error occurred. Please try again."}, ) STATIC_DIR = Path(__file__).parent / "static" app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") # Bust the browser cache for /static/js/app.js and /static/css/style.css on # every container start so deploys don't get served stale assets. Each new # container produces a new __VERSION__ value; the HTML is rendered once at # startup and served from memory. _BUILD_VERSION = str(int(time.time())) _INDEX_HTML = ( (STATIC_DIR / "index.html") .read_text() .replace("__VERSION__", _BUILD_VERSION) ) @app.get("/", include_in_schema=False) async def index(): return HTMLResponse(content=_INDEX_HTML)