""" 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 from routes.walks import router as walks_router from routes.events import router as events_router from routes.sitting import router as sitting_router from routes.osm import router as osm_router from routes.forum import router as forum_router from routes.lost import router as lost_router from routes.knigge import router as knigge_router from routes.wiki import router as wiki_router from routes.movies import router as movies_router from routes.friends import router as friends_router from routes.chat import router as chat_router from routes.admin import router as admin_router from routes.webcal import router as webcal_router from routes.profile import router as profile_router from routes.import_data import router as import_router from routes.sharing import dog_router as sharing_dog_router, share_router as sharing_share_router from routes.widget import router as widget_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"]) app.include_router(walks_router, prefix="/api/walks", tags=["Gassi-Treffen"]) app.include_router(events_router, prefix="/api/events", tags=["Events"]) app.include_router(sitting_router, prefix="/api/sitting", tags=["Sitting"]) app.include_router(osm_router, prefix="/api/osm", tags=["OSM"]) app.include_router(forum_router, prefix="/api/forum", tags=["Forum"]) app.include_router(lost_router, prefix="/api/lost", tags=["Verlorener Hund"]) app.include_router(knigge_router, prefix="/api/knigge", tags=["Knigge"]) app.include_router(wiki_router, prefix="/api/wiki", tags=["Wiki"]) app.include_router(movies_router, prefix="/api/movies", tags=["Filme"]) app.include_router(friends_router, prefix="/api/friends", tags=["Freunde"]) app.include_router(chat_router, prefix="/api/chat", tags=["Chat"]) app.include_router(admin_router, prefix="/api/admin", tags=["Admin"]) app.include_router(webcal_router, prefix="/api/webcal", tags=["WebCal"]) app.include_router(profile_router, prefix="/api/profile", tags=["Profil"]) app.include_router(import_router, prefix="/api/import", tags=["Import"]) app.include_router(sharing_dog_router, prefix="/api/dogs", tags=["Teilen"]) app.include_router(sharing_share_router, prefix="/api/share", tags=["Teilen"]) app.include_router(widget_router, prefix="/api/widget", tags=["Widget"]) # ------------------------------------------------------------------ # 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"} ) # Öffentliche Hunde-Profilseite (für NFC-Tags, kein Login nötig) @app.get("/hund/{dog_id}") async def public_dog_page(dog_id: int): html = f""" Hunde-Profil — BAN YARO
Lade Profil…
""" from fastapi.responses import HTMLResponse return HTMLResponse(content=html) # ------------------------------------------------------------------ # Einladungsseite /teilen/{token} — SPA lädt + nimmt Einladung an # ------------------------------------------------------------------ @app.get("/teilen/{token}") async def invite_page(token: str): return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"}) # ------------------------------------------------------------------ # Widget-Vorschau /widget # ------------------------------------------------------------------ @app.get("/widget") async def widget_page(): return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"}) # ------------------------------------------------------------------ # Digitaler Heimtierausweis /ausweis/{dog_id} # ------------------------------------------------------------------ @app.get("/ausweis/{dog_id}") async def ausweis_page(dog_id: int, request: Request): from fastapi.responses import HTMLResponse from auth import get_current_user_optional, decode_token import json as _json # Auth via Cookie token = request.cookies.get("by_token") user_id = None if token: try: payload = decode_token(token) user_id = int(payload["sub"]) except Exception: pass if not user_id: return HTMLResponse( '

' 'Bitte einloggen um den Ausweis anzuzeigen.

', status_code=401 ) from database import db as _db with _db() as conn: dog = conn.execute( """SELECT d.* FROM dogs d LEFT JOIN dog_shares ds ON ds.dog_id=d.id AND ds.shared_with_id=? AND ds.accepted_at IS NOT NULL WHERE d.id=? AND (d.user_id=? OR ds.id IS NOT NULL)""", (user_id, dog_id, user_id) ).fetchone() if not dog: return HTMLResponse("

Hund nicht gefunden.

", status_code=404) owner = conn.execute("SELECT name, email FROM users WHERE id=?", (dog["user_id"],)).fetchone() health_rows = conn.execute( "SELECT * FROM health WHERE dog_id=? ORDER BY datum DESC", (dog_id,) ).fetchall() vets = conn.execute( """SELECT DISTINCT t.name, t.strasse, t.plz, t.ort, t.telefon FROM tieraerzte t JOIN health h ON h.tierarzt_id = t.id WHERE h.dog_id=?""", (dog_id,) ).fetchall() dog = dict(dog) vets = [dict(v) for v in vets] def esc(s): if not s: return "" return str(s).replace("&","&").replace("<","<").replace(">",">").replace('"',""") def fmt_date(d): if not d: return "–" try: from datetime import date parts = d.split("-") return f"{int(parts[2])}.{int(parts[1])}.{parts[0]}" except Exception: return d geschlecht = {"m": "Rüde", "w": "Hündin"}.get(dog.get("geschlecht",""), "–") # Impfungen impfungen = [r for r in health_rows if r["typ"] == "impfung"] # Medikamente (aktiv) medis = [r for r in health_rows if r["typ"] == "medikament" and r["aktiv"]] # Allergien allergien = [r for r in health_rows if r["typ"] == "allergie"] def health_rows_html(rows, cols): if not rows: return 'Keine Einträge' out = "" for r in rows: out += "" + "".join(f"{esc(r[c])}" for c in cols) + "" return out photo_html = f'{esc(dog[' if dog.get("foto_url") else '
🐕
' vets_html = "" for v in vets: addr = ", ".join(filter(None, [v.get("strasse"), v.get("plz"), v.get("ort")])) vets_html += f'
{esc(v["name"])}' if addr: vets_html += f'
{esc(addr)}' if v.get("telefon"): vets_html += f'
☎ {esc(v["telefon"])}' vets_html += "
" if not vets_html: vets_html = 'Keine Tierärzte eingetragen' html = f""" Heimtierausweis – {esc(dog["name"])}
{photo_html}

{esc(dog["name"])}

{esc(dog.get("rasse") or "Rasse unbekannt")}
Geburtstag
{fmt_date(dog.get("geburtstag"))}
Geschlecht
{geschlecht}
Gewicht
{f'{dog["gewicht_kg"]} kg' if dog.get("gewicht_kg") else "–"}
Transponder
{esc(dog.get("chip_nr")) or "–"}
Besitzer
{esc(owner["name"]) if owner else "–"}

Impfungen

{health_rows_html(impfungen, ["bezeichnung","datum","naechstes","charge_nr","tierarzt_name"])}
ImpfungDatumNächste FälligkeitChargeTierarzt

Aktive Medikamente

{health_rows_html(medis, ["bezeichnung","datum","dosierung","haeufigkeit"])}
MedikamentSeitDosierungHäufigkeit

Allergien & Unverträglichkeiten

{health_rows_html(allergien, ["bezeichnung","schweregrad","reaktion","datum"])}
AllergenSchweregradReaktionSeit

Tierärzte

{vets_html}
""" return HTMLResponse(html) # 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"} )