""" BAN YARO — FastAPI Hauptanwendung """ import os import logging from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse from contextlib import asynccontextmanager from database import init_db import ki import scheduler as sched logging.basicConfig( level = logging.INFO, format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logger = logging.getLogger(__name__) # ------------------------------------------------------------------ # Startup / Shutdown # ------------------------------------------------------------------ @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Ban Yaro startet...") init_db() logger.info(f"KI-Modus: {ki.KI_MODE}") sched.start() yield sched.stop() logger.info("Ban Yaro beendet.") # ------------------------------------------------------------------ # App # ------------------------------------------------------------------ app = FastAPI( title = "Ban Yaro API", version = "0.1.0", lifespan = lifespan, docs_url = "/api/docs" if os.getenv("ENV") != "production" else None, redoc_url = None, ) # ------------------------------------------------------------------ # API-Router registrieren (werden nach und nach hinzugefügt) # ------------------------------------------------------------------ from routes.auth import router as auth_router from routes.dogs import router as dogs_router from routes.diary import router as diary_router from routes.health import router as health_router from routes.poison import router as poison_router from routes.push import router as push_router from routes.ki import router as ki_router from routes.tieraerzte import router as tieraerzte_router from routes.places import router as places_router from routes.routen import router as routen_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) app.include_router(diary_router, prefix="/api/dogs", tags=["Tagebuch"]) app.include_router(health_router, prefix="/api/dogs", tags=["Gesundheit"]) app.include_router(poison_router, prefix="/api/poison", tags=["Giftköder"]) app.include_router(push_router, prefix="/api/push", tags=["Push"]) app.include_router(ki_router, prefix="/api/ki", tags=["KI"]) app.include_router(tieraerzte_router, prefix="/api/tieraerzte", tags=["Tierärzte"]) app.include_router(places_router, prefix="/api/places", tags=["Orte"]) app.include_router(routen_router, prefix="/api/routes", tags=["Routen"]) # ------------------------------------------------------------------ # Fehlerbehandlung — einheitliches JSON-Format # ------------------------------------------------------------------ @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): logger.error(f"Unbehandelter Fehler: {exc}", exc_info=True) return JSONResponse( status_code=500, content={"detail": "Interner Serverfehler."} ) # ------------------------------------------------------------------ # Statische Dateien + SPA-Fallback # ------------------------------------------------------------------ STATIC_DIR = os.path.join(os.path.dirname(__file__), "static") app.mount("/css", StaticFiles(directory=f"{STATIC_DIR}/css"), name="css") app.mount("/js", StaticFiles(directory=f"{STATIC_DIR}/js"), name="js") app.mount("/icons", StaticFiles(directory=f"{STATIC_DIR}/icons"), name="icons") # User-generierte Medien (Fotos aus Tagebuch, Giftköder-Alarm, etc.) MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") @app.get("/favicon.ico") async def favicon(): return FileResponse(f"{STATIC_DIR}/icons/favicon.ico") @app.get("/manifest.json") async def manifest(): return FileResponse(f"{STATIC_DIR}/manifest.json") @app.get("/sw.js") async def service_worker(): return FileResponse( f"{STATIC_DIR}/sw.js", headers={"Cache-Control": "no-cache, no-store, must-revalidate"} ) # Web Share Target @app.post("/share") async def share_target(request: Request): # Empfängt geteilte Inhalte vom Handy (Fotos, Links, Text) # Weiterleitung zur App mit den Daten return FileResponse( f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"} ) # SPA Fallback — ALLE nicht-API-Routen gehen zur index.html @app.get("/{full_path:path}") async def spa_fallback(full_path: str): return FileResponse( f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"} )