"""P42 Dashboard – FastAPI-Backend. Liefert die API unter /api, die hochgeladenen Bilder unter /uploads und (falls gebaut) das React-Frontend aus frontend/dist. """ import asyncio import re from contextlib import asynccontextmanager from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.middleware.gzip import GZipMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from . import auth, haushalt, kanban, links, monitoring, projects, reisen, rezept from .auth import current_user from .config import FRONTEND_DIST from .tracking import track @asynccontextmanager async def lifespan(_: FastAPI): # Verlauf der Server-Auslastung (P42-S.M.) im Hintergrund sammeln sampler = asyncio.create_task(monitoring.sample_loop()) # Erledigte Kanban-Aufgaben nach 14 Tagen löschen cleaner = asyncio.create_task(kanban.cleanup_loop()) # Hauptstadt-Kennzeichen älterer Reise-Städte nachtragen (für den Karten-Export) capitals = asyncio.create_task(reisen.capitals_loop()) yield sampler.cancel() cleaner.cancel() capitals.cancel() app = FastAPI(title="P42 Dashboard", docs_url="/api/docs", openapi_url="/api/openapi.json", lifespan=lifespan) # Komprimiert u. a. die JS-Bundles und die Weltkarte (~740 KB → ~200 KB) app.add_middleware(GZipMiddleware, minimum_size=1000) # ---------- Fehler einheitlich als {"error": "..."} ---------- @app.exception_handler(HTTPException) async def http_error(_: Request, exc: HTTPException): return JSONResponse({"error": exc.detail}, status_code=exc.status_code) @app.exception_handler(projects.ValidationError) @app.exception_handler(links.ValidationError) async def validation_error(_: Request, exc: ValueError): return JSONResponse({"error": str(exc)}, status_code=400) # ---------- API ---------- app.include_router(auth.router) app.include_router(links.router) app.include_router(projects.router) app.include_router(monitoring.router) app.include_router(haushalt.router) app.include_router(rezept.router) app.include_router(reisen.router) app.include_router(kanban.router) @app.get("/api/track") async def get_tracking(number: str = "", _: dict = Depends(current_user)): number = re.sub(r"\s+", "", number).upper() if not re.fullmatch(r"[A-Z0-9-]{6,40}", number): raise HTTPException(400, "Ungültige Sendungsnummer.") return await track(number) @app.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"], include_in_schema=False) def api_not_found(path: str): raise HTTPException(404, "Nicht gefunden.") # ---------- React-Frontend (nach `npm run build`) ---------- if (FRONTEND_DIST / "index.html").exists(): app.mount("/assets", StaticFiles(directory=FRONTEND_DIST / "assets"), name="assets") @app.get("/{path:path}", include_in_schema=False) def spa(path: str): file = (FRONTEND_DIST / path).resolve() if path and file.is_file() and file.is_relative_to(FRONTEND_DIST.resolve()): return FileResponse(file) # Alle anderen Pfade übernimmt der React-Router return FileResponse(FRONTEND_DIST / "index.html", headers={"Cache-Control": "no-cache"})