""" BAN YARO — FastAPI Hauptanwendung """ import os import html import logging from collections import deque from fastapi import FastAPI, Request from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse from starlette.middleware.base import BaseHTTPMiddleware from contextlib import asynccontextmanager from database import init_db import ki import scheduler as sched # In-Memory Log-Buffer (letzte 500 Zeilen) log_buffer: deque = deque(maxlen=500) class _BufferHandler(logging.Handler): _fmt = logging.Formatter() def emit(self, record): log_buffer.append({ 't': self._fmt.formatTime(record, '%H:%M:%S'), 'l': record.levelname, 'm': record.getMessage(), 'n': record.name, }) logging.basicConfig( level = logging.INFO, format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s", ) logging.getLogger().addHandler(_BufferHandler()) 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, ) # Globales File-Upload-Limit (20 MB) _MAX_UPLOAD_BYTES = 20 * 1024 * 1024 class _UploadSizeMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): if request.method in ("POST", "PUT", "PATCH"): cl = request.headers.get("content-length") if cl and int(cl) > _MAX_UPLOAD_BYTES: return JSONResponse( status_code=413, content={"detail": f"Datei zu groß (max. {_MAX_UPLOAD_BYTES // 1024 // 1024} MB)."} ) return await call_next(request) app.add_middleware(_UploadSizeMiddleware) # ------------------------------------------------------------------ # 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 from routes.notifications import router as notifications_router from routes.alerts import router as alerts_router from routes.services import router as services_router from routes.ratings import router as ratings_router from routes.sitting_access import router as sitting_access_router from routes.stats import router as stats_router from routes.achievements import router as achievements_router from routes.training import router as training_router from routes.praise import router as praise_router from routes.weather import router as weather_router from routes.social import router as social_router from routes.moderation import router as moderation_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(weather_router, prefix="/api/weather", tags=["Wetter"]) app.include_router(social_router, prefix="/api/social", tags=["Social"]) 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"]) app.include_router(notifications_router, prefix="/api/notifications", tags=["Notifications"]) app.include_router(alerts_router, prefix="/api/alerts", tags=["Alerts"]) app.include_router(services_router, prefix="/api/services", tags=["Services"]) app.include_router(ratings_router, prefix="/api/ratings", tags=["Ratings"]) app.include_router(sitting_access_router, prefix="/api/sitting-access", tags=["SittingAccess"]) app.include_router(stats_router, prefix="/api/stats", tags=["Stats"]) app.include_router(achievements_router, prefix="/api/achievements", tags=["Achievements"]) app.include_router(training_router, prefix="/api/training", tags=["Training"]) app.include_router(praise_router, prefix="/api/praise", tags=["Praise"]) app.include_router(moderation_router, prefix="/api/moderation", tags=["Moderation"]) # ------------------------------------------------------------------ # 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("/robots.txt") async def robots(): return FileResponse(f"{STATIC_DIR}/robots.txt", media_type="text/plain") @app.get("/llms.txt") async def llms(): return FileResponse(f"{STATIC_DIR}/llms.txt", media_type="text/plain") @app.get("/sitemap.xml") async def sitemap(): from fastapi.responses import Response from database import db as _db from datetime import date today = date.today().isoformat() urls = [ ("https://banyaro.app/", "weekly", "1.0"), ("https://banyaro.app/info", "monthly", "0.9"), ("https://banyaro.app/wiki/rassen", "weekly", "0.8"), ("https://banyaro.app/knigge", "monthly", "0.8"), ] try: with _db() as conn: rassen = conn.execute( "SELECT slug FROM wiki_rassen WHERE slug IS NOT NULL AND slug != '' LIMIT 500" ).fetchall() if rassen: urls.append(("https://banyaro.app/wiki/rassen", "weekly", "0.8")) for r in rassen: urls.append((f"https://banyaro.app/wiki/rasse/{r['slug']}", "monthly", "0.7")) events = conn.execute( "SELECT id FROM events WHERE datum >= date('now') LIMIT 200" ).fetchall() for e in events: urls.append((f"https://banyaro.app/api/events/{e['id']}", "weekly", "0.5")) except Exception: pass entries = "\n".join( f""" {loc} {today} {freq} {prio} """ for loc, freq, prio in urls ) xml = f""" {entries} """ return Response(content=xml, media_type="application/xml") @app.get("/info") async def info_page(): return FileResponse(f"{STATIC_DIR}/landing.html", headers={"Cache-Control": "max-age=3600"}) # ------------------------------------------------------------------ # SEO: Server-gerenderete Wiki-Rassen-Übersicht /wiki/rassen # ------------------------------------------------------------------ @app.get("/wiki/rassen") async def wiki_rassen_page(): from fastapi.responses import HTMLResponse from database import db as _db with _db() as conn: rows = conn.execute( """SELECT name, slug, gruppe, groesse, aktivitaet, foto_url, kinder_geeignet, wohnung_geeignet FROM wiki_rassen WHERE slug IS NOT NULL ORDER BY name ASC""" ).fetchall() rassen = [dict(r) for r in rows] total = len(rassen) groessen_map = {"klein": "Klein", "mittel": "Mittel", "gross": "Groß", "sehr_gross": "Sehr groß"} aktivitaet_map = {"niedrig": "Niedrig", "mittel": "Mittel", "hoch": "Hoch", "sehr_hoch": "Sehr hoch"} cards = "" for r in rassen: foto = r["foto_url"] or "" img = f'{r[' if foto else '
🐕
' groesse = groessen_map.get(r.get("groesse") or "", r.get("groesse") or "") kinder = "✓ Kinder" if r.get("kinder_geeignet") else "" wohnung = "✓ Wohnung" if r.get("wohnung_geeignet") else "" tags = " ".join(f'{t}' for t in [groesse, kinder, wohnung] if t) cards += f""" {img}

{r['name']}

{r.get('gruppe') or ''}

{tags}
\n""" html = f""" Hunderassen-Wiki — {total} Rassen im Überblick | Ban Yaro

Hunderassen-Wiki

{total} Rassen — Charakter, Eignung, Pflege auf einen Blick

{cards}
""" return HTMLResponse(content=html, headers={"Cache-Control": "max-age=3600"}) # ------------------------------------------------------------------ # SEO: Server-gerenderete Wiki-Rassen-Detailseite /wiki/rasse/{slug} # ------------------------------------------------------------------ @app.get("/wiki/rasse/{slug}") async def wiki_rasse_page(slug: str): from fastapi.responses import HTMLResponse from database import db as _db def esc(s): if not s: return "" return str(s).replace("&","&").replace("<","<").replace(">",">").replace('"',""") with _db() as conn: rasse = conn.execute("SELECT * FROM wiki_rassen WHERE slug=?", (slug,)).fetchone() if not rasse: return HTMLResponse("

Rasse nicht gefunden

Alle Rassen", status_code=404) berichte = conn.execute( """SELECT wb.titel, wb.text, wb.created_at, u.name AS autor FROM wiki_berichte wb JOIN users u ON u.id=wb.user_id WHERE wb.rasse=? ORDER BY wb.created_at DESC LIMIT 20""", (slug,) ).fetchall() r = dict(rasse) try: dogs_count = conn.execute( "SELECT COUNT(DISTINCT d.user_id) FROM dogs d WHERE LOWER(d.rasse) LIKE ?", (f"%{r.get('name','').lower()}%",) ).fetchone()[0] except Exception: dogs_count = 0 try: zuchter_count = conn.execute( "SELECT COUNT(*) FROM wiki_zuchter WHERE rasse_slug=? AND verified=1", (slug,) ).fetchone()[0] except Exception: zuchter_count = 0 berichte = [dict(b) for b in berichte] berichte_count = len(berichte) groessen_map = {"klein":"Klein","mittel":"Mittel","gross":"Groß","sehr_gross":"Sehr groß"} aktivitaet_map = {"niedrig":"Niedrig","mittel":"Mittel","hoch":"Hoch","sehr_hoch":"Sehr hoch"} erfahrung_map = {"anfaenger":"Anfänger geeignet","fortgeschritten":"Für Erfahrene","experte":"Nur für Experten"} groesse = groessen_map.get(r.get("groesse") or "", r.get("groesse") or "–") aktivitaet = aktivitaet_map.get(r.get("aktivitaet") or "", r.get("aktivitaet") or "–") erfahrung = erfahrung_map.get(r.get("erfahrung") or "", r.get("erfahrung") or "–") kinder = "Ja" if r.get("kinder_geeignet") else "Bedingt" wohnung = "Ja" if r.get("wohnung_geeignet") else "Besser Haus mit Garten" gewicht = "" if r.get("gewicht_min_kg") and r.get("gewicht_max_kg"): gewicht = f"{r['gewicht_min_kg']}–{r['gewicht_max_kg']} kg" elif r.get("gewicht_max_kg"): gewicht = f"bis {r['gewicht_max_kg']} kg" foto_html = f'{esc(r[' if r.get("foto_url") else '
🐕
' facts = [ ("Gruppe / FCI", esc(r.get("gruppe") or "")), ("Herkunft", esc(r.get("herkunft") or "")), ("Größe", groesse), ("Gewicht", gewicht), ("Lebensdauer", esc(r.get("lebensdauer") or "")), ("Aktivitätslevel", aktivitaet), ("Für Anfänger", erfahrung), ("Kinder geeignet", kinder), ("Wohnungsgeeignet", wohnung), ("Ursprüngliche Aufgabe", esc(r.get("bred_for") or "")), ] facts_html = "".join( f'
{label}{val}
' for label, val in facts if val ) temperament_html = "" if r.get("temperament"): tags = [t.strip() for t in str(r["temperament"]).split(",") if t.strip()] temperament_html = ( '
' + "".join( f'{esc(t)}' for t in tags ) + '
' ) berichte_html = "" if berichte: for b in berichte: datum = b.get("created_at","")[:10] if b.get("created_at") else "" berichte_html += f"""
{esc(b.get('autor',''))} · {datum}

{esc(b.get('titel',''))}

{esc(b.get('text',''))}

""" beschreibung_html = "" if r.get("beschreibung"): beschreibung_html = ( '
' '

Charakter & Wesen

' f'

{esc(r["beschreibung"])}

' '
' ) vorkommen_html = "" if r.get("vorkommen_de"): vorkommen_html = ( '
' '

Vorkommen in Deutschland

' f'

{esc(r["vorkommen_de"])}

' '
' ) stats_html = ( '
' f'🐕 {dogs_count} Nutzer haben diesen Hund' f'🏆 {zuchter_count} Züchter eingetragen' f'💬 {berichte_count} Community-Berichte' '
' ) name = esc(r.get("name","")) gruppe = esc(r.get("gruppe") or "") herkunft = esc(r.get("herkunft") or "") temp_str = esc(r.get("temperament") or "") beschr_str = esc(r.get("beschreibung") or "") if beschr_str: desc = f"{name} — {beschr_str[:160]}".strip().rstrip(".") else: desc = f"{name} — Hunderasse aus {herkunft}. Größe: {groesse}. Aktivität: {aktivitaet}. {temp_str[:120] if temp_str else ''}".strip().rstrip(".") json_ld = f"""{{ "@context":"https://schema.org", "@type":"Article", "headline":"{name} — Rasse-Profil", "description":"{desc}", "url":"https://banyaro.app/wiki/rasse/{slug}", "inLanguage":"de", "publisher":{{"@type":"Organization","name":"Ban Yaro","url":"https://banyaro.app"}}, "mainEntityOfPage":{{"@type":"WebPage","@id":"https://banyaro.app/wiki/rasse/{slug}"}} }}""" html = f""" {name} — Hunderasse Profil | Ban Yaro Wiki
{foto_html}

{name}

{'

' + gruppe + '

' if gruppe else ''} {temperament_html} In der App öffnen
{beschreibung_html} {vorkommen_html}

Steckbrief

{facts_html}
{stats_html} {'

Erfahrungsberichte der Community (' + str(berichte_count) + ')

' + berichte_html + '
' if berichte else ''}

In der App

Ban Yaro ist die kostenlose Hunde-App für Deutschland, Österreich und die Schweiz. Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community und mehr — DSGVO-konform, ohne App Store.

Kostenlos starten
""" return HTMLResponse(content=html, headers={"Cache-Control": "max-age=3600"}) @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): from database import db as _db _og_name = "Hunde-Profil" _og_desc = "Hunde-Profil auf Ban Yaro — der deutschen Hunde-Plattform" _og_img = "https://banyaro.app/icons/icon-512.png" try: with _db() as conn: _dog = conn.execute( "SELECT name, rasse, foto_url, bio FROM dogs WHERE id=? AND is_public=1", (dog_id,) ).fetchone() if _dog: _og_name = _dog["name"] _rasse = f" · {_dog['rasse']}" if _dog.get("rasse") else "" _og_desc = f"{_dog['name']}{_rasse} — Profil auf Ban Yaro" if _dog.get("bio"): _og_desc = f"{_dog['name']}{_rasse}: {str(_dog['bio'])[:120]}" if _dog.get("foto_url"): _og_img = f"https://banyaro.app{_dog['foto_url']}" except Exception: pass _s = html.escape # XSS-Schutz für OG-Meta-Tags _html = f""" {_s(_og_name)} — 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) # ------------------------------------------------------------------ # SEO: Hunde-Knigge als server-gerenderete Seite /knigge # ------------------------------------------------------------------ @app.get("/knigge") async def knigge_page(): from fastapi.responses import HTMLResponse begegnungen = [ ("Fremder Hund", "Kurze Leine, ruhig bleiben. Hunde schnüffeln lassen wenn beide entspannt sind. Bei Eskalation: weglenken, Richtung wechseln. Freilaufende Hunde auf angeleine Hunde zulaufen lassen ist unhöflich — der angeleine Hund kann in Stress (Leinenfrust) geraten."), ("Kinder", "Hund nie unbeaufsichtigt mit fremden Kindern lassen. Kind fragen ob es streicheln darf. Hund seitlich positionieren, nicht zwischen Kind und Weg. Kinder sollten keinen direkten Augenkontakt mit unbekannten Hunden halten."), ("Radfahrer", "Hund rechtzeitig an die Seite nehmen. Fahrräder können für manche Hunde bedrohlich wirken. Frühzeitig Abstand gewinnen, ruhig bleiben, Hund bei sich halten."), ("Jogger", "Kurze Leine, Abstand halten, Hund nicht anspringen lassen. Jogger bewegen sich schnell — das kann Jagdinstinkt auslösen."), ("ÖPNV (Bus & Bahn)", "In Deutschland gilt im ÖPNV grundsätzlich Maulkorbpflicht für Hunde. Kleine Hunde in Transportbox fahren kostenlos oder zum Kindertarif. Große Hunde brauchen in den meisten Städten einen eigenen Fahrschein. Regeln variieren je nach Verkehrsverbund."), ("Supermarkt & Geschäfte", "Es gilt das Hausrecht des Betreibers. Ein 'Hunde willkommen'-Schild ist eine explizite Einladung. Im Zweifel immer fragen. Außen anbinden ist nur kurzzeitig und mit Sichtkontakt akzeptabel."), ("Restaurant", "Im Außenbereich erlauben viele Restaurants Hunde — aber Hausrecht gilt. Wenn ein anderer Gast Angst hat, ist Kompromissbereitschaft ein Zeichen guter Hundehaltung. Im Zweifelsfall Personal entscheiden lassen."), ("Spielplätze", "Hunde sind auf Kinderspielplätzen generell verboten (§ 2 Abs. 4 BImSchG und Gemeindesatzungen). Gilt auch für gut erzogene Hunde. Abstand halten, auch beim Vorbeigehen."), ] szenarien = [ ("Muss Kot immer aufgesammelt werden?", "Ja — auch im Gebüsch abseits des Weges. Kinder spielen überall, und Parasiten wie Spulwurm können für Menschen gefährlich sein. Bußgelder für liegengelassenen Hundekot liegen je nach Stadt zwischen 50 € und 500 €."), ("Leinenpflicht ohne Schild?", "Leinenpflicht ist Ländersache. Viele Bundesländer haben eine allgemeine Anleinpflicht in Ortschaften und öffentlichen Grünanlagen. Im Zweifel anleinen und die Gemeindesatzung prüfen."), ("Hund frei laufen trotz angeleintem Hund?", "Nein. Freilaufende Hunde auf angeleine Hunde zuzulassen ist unhöflich und kann den angeleinten Hund in Stress versetzen (Leinenfrust). Immer erst anleinen und fragen ob ein Treffen gewünscht ist."), ("Haftung bei Beißvorfall?", "Hundehalter haften verschuldensunabhängig für Schäden durch ihren Hund (§ 833 BGB). Eine Hunde-Haftpflichtversicherung ist in vielen Bundesländern Pflicht und in jedem Fall empfehlenswert."), ] begs_html = "".join( f'

{titel}

{text}

' for titel, text in begegnungen ) szen_html = "".join( f'

{frage}

{antwort}

' for frage, antwort in szenarien ) html = """ Hunde-Knigge — Regeln für Hundebesitzer | Ban Yaro

Hunde-Knigge

Regeln, Tipps und häufige Fragen für Hundebesitzer — im Alltag, im ÖPNV und in der Community

Begegnungen im Alltag

""" + begs_html + """

Häufige Fragen & Regeln

""" + szen_html + """

Mehr Tipps in der Ban Yaro App

Community-Abstimmungen zu kniffligen Situationen, KI-Situationsberater und alle Hunde-Funktionen kostenlos nutzen.

Kostenlos starten
""" return HTMLResponse(content=html, headers={"Cache-Control": "max-age=7200"}) # 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"} )