FastAPI-Backend und React-Frontend mit Konten, Sendungsverfolgung, Projekt-Artikeln, Vorrat & Einkauf mit Rezeptabgleich, Reise-Weltkarte, Kanban-Projekten mit Priorität, Server-Monitoring und den Designs Matrix und Blackwall. Betrieb per Docker hinter nginx (siehe DEPLOY.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""P42 Dashboard – FastAPI-Backend.
|
||
|
||
Liefert die API unter /api, die hochgeladenen Bilder unter /uploads
|
||
und (falls gebaut) das React-Frontend aus frontend/dist.
|
||
"""
|
||
import re
|
||
|
||
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
|
||
|
||
app = FastAPI(title="P42 Dashboard", docs_url="/api/docs", openapi_url="/api/openapi.json")
|
||
# 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"})
|