diff --git a/backend/database.py b/backend/database.py
index 2f8182d..3bea359 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -550,7 +550,18 @@ def _migrate(conn_factory):
("notes", "parent_label", "TEXT"),
("users", "notes_ki_enabled", "INTEGER NOT NULL DEFAULT 1"),
# Züchter-Rolle
- ("users", "breeder_status", "TEXT"),
+ ("users", "breeder_status", "TEXT"),
+ # Würfe: Verknüpfung mit Zuchtkartei-Hunden + Welfare
+ ("litters", "vater_id", "INTEGER"),
+ ("litters", "mutter_id", "INTEGER"),
+ ("litters", "welfare_level", "TEXT"),
+ ("litters", "welfare_acknowledged", "INTEGER NOT NULL DEFAULT 0"),
+ # KI-Züchter-Features (pro User an/abschaltbar, außer Tierschutz)
+ ("users", "ki_zucht_wurfankuendigung", "INTEGER NOT NULL DEFAULT 1"),
+ ("users", "ki_zucht_genetik", "INTEGER NOT NULL DEFAULT 1"),
+ ("users", "ki_zucht_paarung", "INTEGER NOT NULL DEFAULT 1"),
+ ("users", "ki_zucht_beschreibung", "INTEGER NOT NULL DEFAULT 1"),
+ ("users", "ki_zucht_jahresbericht", "INTEGER NOT NULL DEFAULT 1"),
]
with conn_factory() as conn:
for table, column, col_type in migrations:
diff --git a/backend/ki.py b/backend/ki.py
index 2c5fcaa..89e9065 100644
--- a/backend/ki.py
+++ b/backend/ki.py
@@ -82,6 +82,28 @@ def _track_usage(user_id: int | None, source: str) -> None:
logger.warning(f"KI-Tracking fehlgeschlagen: {exc}")
+def _is_cloud_priority_user(user_id: int | None) -> bool:
+ """Privilegierte Rollen (Admin, Moderator, Züchter, Manager) nutzen Cloud-KI primär."""
+ if not user_id or not ANTHROPIC_KEY:
+ return False
+ try:
+ from database import db
+ with db() as conn:
+ user = conn.execute(
+ "SELECT rolle, is_moderator, is_social_media FROM users WHERE id=?",
+ (user_id,)
+ ).fetchone()
+ if not user:
+ return False
+ return bool(
+ user["rolle"] in ("admin", "breeder", "moderator")
+ or user["is_moderator"]
+ or user["is_social_media"]
+ )
+ except Exception:
+ return False
+
+
def _check_weekly_cloud_limit(user_id: int | None) -> None:
"""Wirft KIPremiumRequired wenn user_id das wöchentliche Cloud-Limit erreicht hat."""
if user_id is None or CLOUD_WEEKLY_LIMIT <= 0:
@@ -92,9 +114,9 @@ def _check_weekly_cloud_limit(user_id: int | None) -> None:
user = conn.execute(
"SELECT rolle, is_moderator FROM users WHERE id=?", (user_id,)
).fetchone()
- # Admins, Moderatoren und Media Manager haben kein Limit
+ # Admins, Moderatoren, Züchter und Media Manager haben kein Limit
if user and (
- user["rolle"] in ("admin", "moderator", "media_manager")
+ user["rolle"] in ("admin", "breeder", "moderator", "media_manager")
or user["is_moderator"]
):
return
@@ -137,8 +159,28 @@ async def complete(
if requires_premium and not user_is_premium:
raise KIPremiumRequired("Dieses Feature ist Teil von Ban Yaro Premium.")
- # Immer lokal zuerst — Cloud ist Fallback wenn lokal nicht erreichbar
if KI_MODE in ("local", "cloud"):
+ # Privilegierte Rollen (Admin, Moderator, Züchter, Manager) → Cloud zuerst
+ if _is_cloud_priority_user(user_id):
+ try:
+ _check_weekly_cloud_limit(user_id)
+ text = await _cloud_complete(prompt, system, max_tokens, json_mode)
+ _track_usage(user_id, "cloud")
+ if return_model:
+ return (text, CLOUD_MODEL)
+ return (text, "cloud") if return_source else text
+ except KIPremiumRequired:
+ raise
+ except Exception as e:
+ logger.warning(f"Cloud-KI nicht erreichbar für privilegierten User, Fallback lokal: {e}")
+ # Fallback auf lokales Modell
+ text = await _local_complete(prompt, system, max_tokens, json_mode)
+ _track_usage(user_id, "local")
+ if return_model:
+ return (text, LOCAL_MODEL)
+ return (text, "local") if return_source else text
+
+ # Standard-User → lokal zuerst, Cloud als Fallback
try:
text = await _local_complete(prompt, system, max_tokens, json_mode)
_track_usage(user_id, "local")
diff --git a/backend/main.py b/backend/main.py
index 92936ed..fbae790 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -160,6 +160,8 @@ from routes.breeder import router as breeder_router
from routes.litters import router as litters_router
from routes.breeder_photos import router as breeder_photos_router
from routes.zucht_hunde import router as zucht_hunde_router
+from routes.breeder_export import router as breeder_export_router
+from routes.zucht_ki import router as zucht_ki_router
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
@@ -189,6 +191,8 @@ app.include_router(breeder_router, prefix="/api", tags=["Züchter"
app.include_router(litters_router, prefix="/api", tags=["Würfe"])
app.include_router(breeder_photos_router, prefix="/api", tags=["Züchter-Fotos"])
app.include_router(zucht_hunde_router, prefix="/api", tags=["Zuchtkartei"])
+app.include_router(breeder_export_router, prefix="/api", tags=["Export"])
+app.include_router(zucht_ki_router, prefix="/api", tags=["Züchter-KI"])
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"])
@@ -255,6 +259,7 @@ async def sitemap():
("https://banyaro.app/info", "monthly", "0.9"),
("https://banyaro.app/wiki/rassen", "weekly", "0.8"),
("https://banyaro.app/knigge", "monthly", "0.8"),
+ ("https://banyaro.app/wurfboerse", "daily", "0.8"),
]
try:
@@ -262,8 +267,6 @@ async def sitemap():
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"))
@@ -272,6 +275,20 @@ async def sitemap():
).fetchall()
for e in events:
urls.append((f"https://banyaro.app/api/events/{e['id']}", "weekly", "0.5"))
+
+ # Öffentliche Züchter-Profile
+ breeders = conn.execute(
+ "SELECT bp.zwingername FROM breeder_profiles bp "
+ "JOIN users u ON u.id = bp.user_id "
+ "WHERE bp.verified_at IS NOT NULL AND u.rolle = 'breeder'"
+ ).fetchall()
+ for b in breeders:
+ if b["zwingername"]:
+ from urllib.parse import quote
+ urls.append((
+ f"https://banyaro.app/breeder/{quote(b['zwingername'])}",
+ "weekly", "0.7"
+ ))
except Exception:
pass
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 011f882..25c2274 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -11,3 +11,4 @@ openai==1.59.2
anthropic==0.49.0
pywebpush==2.0.0
apscheduler==3.10.4
+odfpy==1.4.1
diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py
index d3ea366..061c9a7 100644
--- a/backend/routes/breeder.py
+++ b/backend/routes/breeder.py
@@ -52,7 +52,7 @@ async def breeder_status(user=Depends(get_current_user)):
if not row:
raise HTTPException(404, "User nicht gefunden.")
profile = None
- if row["rolle"] == "breeder":
+ if row["rolle"] in ("breeder", "admin"):
profile = conn.execute(
"SELECT zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung, verified_at "
"FROM breeder_profiles WHERE user_id=?",
@@ -318,6 +318,28 @@ async def breeder_public_profile(zwingername: str):
return dict(row)
+# ------------------------------------------------------------------
+# POST /api/admin/breeder/create-profile — Admin-Schnellprofil
+# ------------------------------------------------------------------
+@router.post("/admin/breeder/create-profile")
+async def admin_create_profile(admin=Depends(require_admin)):
+ with db() as conn:
+ existing = conn.execute(
+ "SELECT id FROM breeder_profiles WHERE user_id=?", (admin["id"],)
+ ).fetchone()
+ if existing:
+ return {"message": "Profil existiert bereits.", "profile_id": existing["id"]}
+ cur = conn.execute(
+ "INSERT INTO breeder_profiles (user_id, zwingername, rasse_text, verein, stadt, verified_at) "
+ "VALUES (?, ?, ?, ?, ?, datetime('now'))",
+ (admin["id"], "Admin-Zwinger", "Alle Rassen", "Admin", "Überall")
+ )
+ conn.execute(
+ "UPDATE users SET breeder_status='approved' WHERE id=?", (admin["id"],)
+ )
+ return {"message": "Admin-Züchterprofil angelegt.", "profile_id": cur.lastrowid}
+
+
# ------------------------------------------------------------------
# PUT /api/breeder/profile — eigenes Profil bearbeiten
# ------------------------------------------------------------------
diff --git a/backend/routes/breeder_export.py b/backend/routes/breeder_export.py
new file mode 100644
index 0000000..f75516e
--- /dev/null
+++ b/backend/routes/breeder_export.py
@@ -0,0 +1,497 @@
+"""BAN YARO — Züchter-Datenexport (HTML + ODS)"""
+import io
+import zipfile
+import logging
+from datetime import date, datetime
+from fastapi import APIRouter, Depends, HTTPException
+from fastapi.responses import StreamingResponse
+
+from database import db
+from auth import get_current_user
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+
+
+def _require_breeder(user=Depends(get_current_user)):
+ if user["rolle"] not in ("breeder", "admin"):
+ raise HTTPException(403, "Nur für Züchter.")
+ return user
+
+
+# ------------------------------------------------------------------
+# Datenabruf
+# ------------------------------------------------------------------
+def _collect_data(user_id: int) -> dict:
+ with db() as conn:
+ profile = conn.execute(
+ "SELECT bp.*, u.name AS user_name, u.email "
+ "FROM breeder_profiles bp JOIN users u ON u.id = bp.user_id "
+ "WHERE bp.user_id=?", (user_id,)
+ ).fetchone()
+ if not profile:
+ return None
+
+ bid = profile["id"]
+
+ hunde = conn.execute(
+ "SELECT * FROM zucht_hunde WHERE breeder_id=? ORDER BY name", (bid,)
+ ).fetchall()
+
+ hund_ids = [h["id"] for h in hunde]
+
+ def rows_for(table, id_col):
+ if not hund_ids:
+ return []
+ placeholders = ",".join("?" * len(hund_ids))
+ return conn.execute(
+ f"SELECT * FROM {table} WHERE {id_col} IN ({placeholders}) ORDER BY {id_col}",
+ hund_ids
+ ).fetchall()
+
+ health = rows_for("dog_health_tests", "hund_id")
+ genetic = rows_for("dog_genetic_tests", "hund_id")
+ titles = rows_for("dog_titles", "hund_id")
+
+ litters = conn.execute(
+ "SELECT * FROM litters WHERE breeder_id=? ORDER BY created_at DESC", (bid,)
+ ).fetchall()
+ litter_ids = [l["id"] for l in litters]
+
+ puppies, weights = [], []
+ if litter_ids:
+ placeholders = ",".join("?" * len(litter_ids))
+ puppies = conn.execute(
+ f"SELECT * FROM puppies WHERE wurf_id IN ({placeholders}) ORDER BY wurf_id",
+ litter_ids
+ ).fetchall()
+ puppy_ids = [p["id"] for p in puppies]
+ if puppy_ids:
+ ph2 = ",".join("?" * len(puppy_ids))
+ weights = conn.execute(
+ f"SELECT * FROM puppy_weights WHERE welpe_id IN ({ph2}) ORDER BY welpe_id, gemessen_am",
+ puppy_ids
+ ).fetchall()
+
+ # Stammbaum pro Hund (3 Generationen, flach)
+ def pedigree_flat(hund_id, depth=3):
+ if not hund_id or depth == 0:
+ return {}
+ row = conn.execute("SELECT id, name, zuchtbuchnummer, geburtsdatum FROM zucht_hunde WHERE id=?", (hund_id,)).fetchone()
+ if not row:
+ return {}
+ result = {"name": row["name"], "nr": row["zuchtbuchnummer"] or "", "geb": row["geburtsdatum"] or ""}
+ row2 = conn.execute("SELECT vater_id, mutter_id FROM zucht_hunde WHERE id=?", (hund_id,)).fetchone()
+ if row2:
+ result["vater"] = pedigree_flat(row2["vater_id"], depth - 1)
+ result["mutter"] = pedigree_flat(row2["mutter_id"], depth - 1)
+ return result
+
+ pedigrees = {h["id"]: pedigree_flat(h["id"]) for h in hunde}
+
+ return {
+ "profile": dict(profile),
+ "hunde": [dict(h) for h in hunde],
+ "health": [dict(r) for r in health],
+ "genetic": [dict(r) for r in genetic],
+ "titles": [dict(r) for r in titles],
+ "litters": [dict(l) for l in litters],
+ "puppies": [dict(p) for p in puppies],
+ "weights": [dict(w) for w in weights],
+ "pedigrees": pedigrees,
+ }
+
+
+# ------------------------------------------------------------------
+# HTML-Export
+# ------------------------------------------------------------------
+def _fmt_date(val):
+ if not val:
+ return "—"
+ try:
+ return datetime.strptime(str(val)[:10], "%Y-%m-%d").strftime("%d.%m.%Y")
+ except Exception:
+ return str(val)
+
+
+def _health_badge(test_typ, ergebnis):
+ color = "#6B7280"
+ e = (ergebnis or "").upper()
+ if test_typ == "HD":
+ color = {"A1": "#16a34a", "A2": "#16a34a", "B1": "#86efac", "B2": "#86efac",
+ "C": "#eab308", "C1": "#eab308", "C2": "#eab308",
+ "D": "#f97316", "D1": "#f97316", "D2": "#f97316",
+ "E": "#dc2626", "E1": "#dc2626", "E2": "#dc2626"}.get(e, "#6B7280")
+ elif test_typ == "ED":
+ color = {"0": "#16a34a", "1": "#eab308", "2": "#f97316", "3": "#dc2626"}.get(e, "#6B7280")
+ elif ergebnis in ("clear", "frei"):
+ color = "#16a34a"
+ elif ergebnis == "carrier":
+ color = "#eab308"
+ elif ergebnis == "affected":
+ color = "#dc2626"
+ return f'{ergebnis}'
+
+
+def _pedigree_node_html(node, gen):
+ if not node:
+ return '
Unbekannt
'
+ geb = f"*{node['geb'][:4]}" if node.get("geb") else ""
+ nr = node.get("nr", "")
+ bg = "#7c3aed" if gen == 1 else ("#ede9fe" if gen == 2 else ("#f5f3ff" if gen == 3 else "#faf9ff"))
+ col = "#fff" if gen == 1 else "#1f2937"
+ return (
+ f'
'
+ f'
{node["name"]}
'
+ + (f'
{nr}
' if nr else "")
+ + (f'
{geb}
' if geb else "")
+ + "
"
+ )
+
+
+def _pedigree_html(tree):
+ # 4-Gen horizontaler Stammbaum als Tabelle
+ def nodes(t, gen, max_gen=4):
+ if gen > max_gen:
+ return []
+ item = [{"node": t, "gen": gen}]
+ item += nodes(t.get("vater") if t else None, gen + 1, max_gen)
+ item += nodes(t.get("mutter") if t else None, gen + 1, max_gen)
+ return item
+
+ rows = 8 # 2^3 für 4 Generationen
+ cols = 4
+
+ def collect(node, gen, row_start, row_span):
+ if gen > cols:
+ return []
+ items = [{"node": node, "gen": gen, "row": row_start, "span": row_span}]
+ half = row_span // 2
+ items += collect(node.get("vater") if node else None, gen + 1, row_start, half)
+ items += collect(node.get("mutter") if node else None, gen + 1, row_start + half, half)
+ return items
+
+ cells = collect(tree, 1, 0, rows)
+ gen_labels = {1: "Proband", 2: "Eltern", 3: "Großeltern", 4: "Urgroßeltern"}
+
+ # Grid als Tabelle (row_height = 50px)
+ row_h = 52
+ total_h = rows * row_h
+
+ html = (
+ f'
'
+ f'
'
+ )
+ for c in cells:
+ node, gen, row, span = c["node"], c["gen"], c["row"], c["span"]
+ html += (
+ f'
'
+ + _pedigree_node_html(node, gen)
+ + "
"
+ )
+ html += "
"
+ return html
+
+
+def _generate_html(data: dict) -> str:
+ p = data["profile"]
+ today = date.today().strftime("%d.%m.%Y")
+
+ hunde_html = ""
+ for h in data["hunde"]:
+ hid = h["id"]
+ h_health = [r for r in data["health"] if r["hund_id"] == hid]
+ h_genetic = [r for r in data["genetic"] if r["hund_id"] == hid]
+ h_titles = [r for r in data["titles"] if r["hund_id"] == hid]
+ tree = data["pedigrees"].get(hid, {})
+
+ health_rows = "".join(
+ f'
{r["test_typ"]}
{_health_badge(r["test_typ"], r["ergebnis"])}
'
+ f'
{_fmt_date(r["untersuch_am"])}
{r.get("labor","") or ""}
'
+ f'
{r.get("untersucher","") or ""}
'
+ for r in h_health
+ ) or "
Keine Einträge
"
+
+ genetic_rows = "".join(
+ f'
{r["marker_name"]}
{_health_badge("DNA", r["ergebnis_klasse"])}
'
+ f'
{_fmt_date(r["getestet_am"])}
{r.get("labor","") or ""}
'
+ for r in h_genetic
+ ) or "
Keine Einträge
"
+
+ title_rows = "".join(
+ f'
{r["titel_name"]}
{r["titel_typ"]}
'
+ f'
{_fmt_date(r["verliehen_am"])}
{r.get("ort","") or ""}
'
+ f'
{r.get("richter","") or ""}
'
+ for r in sorted(h_titles, key=lambda x: x["verliehen_am"] or "", reverse=True)
+ ) or "
"""
+
+ litters_html = ""
+ for l in data["litters"]:
+ lid = l["id"]
+ l_puppies = [p for p in data["puppies"] if p["wurf_id"] == lid]
+ puppy_rows = "".join(
+ f'
{p.get("name") or "—"}
'
+ f'
{"Rüde" if p.get("geschlecht")=="maennlich" else "Hündin" if p.get("geschlecht")=="weiblich" else "—"}
Öffentliche Wurfankündigungen mit Filter nach Rasse und Status. Interessenten schreiben direkt per Nachricht an. Für Käufer kostenlos.
Züchter
+
+
+ 🌳
+
Stammbaum
4 Generationen visuell dargestellt. Klickbare Knoten öffnen das Hunde-Profil. Teilen per Link für Käufer-Dokumentation.
Züchter
+
+
+ 🧬
+
Inzucht-Koeffizient
Automatische Berechnung nach Wright's Formel. Ampel-Bewertung: optimal unter 2,5%, kritisch ab 12,5%. Probeverpaarung simuliert jeden beliebigen Anpaarungspartner.
Züchter
+
+
+ 🩺
+
Gesundheitsdokumentation
HD, ED, Augen, Herz, DNA-Tests — alle Nachweise strukturiert erfasst. Farbcodierte Ergebnis-Badges auf einen Blick.
Züchter
+
+
+ 🛡️
+
Tierschutz-Check
Automatische Prüfung bei jeder Verpaarung: Alter, Wurfhäufigkeit, Deckpause, genetische Risiken. Nicht abschaltbar — weil die Tiere zählen.
Züchter
+
+
+ 🤖
+
KI-Assistenz
Wurfankündigungen schreiben, Genetik-Erklärungen für Käufer formulieren, Paarungsanalyse mit Empfehlung, Jahresauswertung. Nutzt Claude Sonnet direkt.
Züchter
+
+
+ 📊
+
Datenexport
Alle Zuchtkartei-Daten als HTML-Dossier (druckbar, mit Stammbaum-Visualisierung) und ODS-Tabelle (editierbar in LibreOffice/Excel). Keine Datenfalle.
Züchter
+
+
+ 📄
+
Kaufvertrag
Automatisch ausgefüllter Kaufvertrag pro Welpe als druckbares Dokument — mit Chip-Nummer, Geburtsdatum, Käufer- und Züchterdaten.
Züchter
+
+
+
+
+
+
+
+
+
+
Die Plattform für verantwortungsvolle Züchter
+
Ban Yaro ist die erste Hunde-App die Zucht-Management, Tierschutz-Checks und KI-Assistenz in einer Plattform verbindet — gedacht für Züchter die ihre Tiere ernst nehmen.
+
+
+
+
+
🐕🦺
+
Für Käufer
+
Finde deinen Welpen in der Wurfbörse mit vollem Einblick in Gesundheitstests, Gentests und Stammbaum der Eltern. Schreibe direkt mit dem Züchter. Keine versteckten Händler.
+
+
+
+
✅
+
Transparenz als Standard
+
Verifizierte Züchter-Profile mit öffentlich sichtbaren Gesundheitsdaten. Der Tierschutz-Check läuft bei jeder Verpaarung automatisch — Ergebnisse gehen direkt an den Admin wenn kritische Grenzen überschritten werden.
+
+
+
diff --git a/backend/static/llms.txt b/backend/static/llms.txt
index 5f1cbf1..02c4ec2 100644
--- a/backend/static/llms.txt
+++ b/backend/static/llms.txt
@@ -1,6 +1,6 @@
# Ban Yaro — Die deutschsprachige Hunde-Plattform
# https://banyaro.app
-# Letzte Aktualisierung: 2026-04-25
+# Letzte Aktualisierung: 2026-04-28
## Was ist Ban Yaro?
@@ -15,11 +15,12 @@ Ban Yaro ist kostenlos nutzbar (Freemium-Modell). Die App ist auf allen Smartpho
## Zielgruppe
- Deutschsprachige Hundebesitzer (Deutschland, Österreich, Schweiz)
+- Verantwortungsvolle Hundezüchter (VDH und andere Verbände)
+- Welpen-Interessenten und Käufer
- Hundeschulen und Hundetrainer
- Tierärzte und Praxen
-- Züchter
-## Funktionen (aktuell verfügbar)
+## Funktionen
### Hunde-Profil & Tagebuch
- Digitales Hunde-Profil (Name, Rasse, Geburtstag, Foto, Chip-Nummer)
@@ -37,175 +38,148 @@ Ban Yaro ist kostenlos nutzbar (Freemium-Modell). Die App ist auf allen Smartpho
- Printbarer Heimtierausweis (PDF)
### Pflege-System
-- 43 rassenspezifische Pflegetipps in 10 Kategorien: Fell, Krallen, Zähne, Ohren, Augen, Pfoten, Parasiten, Saisonal, Gesundheitsvorsorge, Welpen-Pflege
-- Fell-Typ-Unterscheidung: kurz / lang / lockig / Doppelmantel
-- Unterscheidung Schneiden vs. Trimmen
-- Tipp des Tages automatisch nach Rasse und Fell-Typ ausgewählt
-- Rassen-Autocomplete im Profil verknüpft mit Pflege-Tipps
+- 43 rassenspezifische Pflegetipps in 10 Kategorien
+- Fell-Typ-Unterscheidung, Schneiden vs. Trimmen
+- Tipp des Tages automatisch nach Rasse ausgewählt
### Training & KI-Trainer
-- Tägliches Trainings-Tagebuch (Wiederholungen, Erfolgsquote 0–100%, Hundestimmung, Zufriedenheit)
-- Übungsfortschritt in 5 Stufen — von "noch nicht gezeigt" bis "sitzt sicher"
-- Virtueller KI-Trainer: analysiert letzte 20 Sessions, empfiehlt täglich welche Übungen anstehen
-- Fortschrittsprognose bis zur Meisterschaft (Trendanalyse)
-- 104 Übungen in 7 Kategorien
-- KI-Trainingsplan erstellen (Plus-Feature)
-- Trainingskalender im Habit-Tracker-Stil
-- Gamification: Streaks, Abzeichen, XP
-- Kommandos & Fähigkeiten sichtbar im Hunde-Profil (für Hundesitter)
+- Tägliches Trainings-Tagebuch (Wiederholungen, Erfolgsquote, Hundestimmung)
+- Übungsfortschritt in 5 Stufen, 104 Übungen in 7 Kategorien
+- Virtueller KI-Trainer: analysiert letzte 20 Sessions, tägliche Empfehlung
+- Fortschrittsprognose bis zur Meisterschaft
+- Gamification: Streaks, Abzeichen, Trainingskalender
-### Wöchentlicher Lober (KI)
-- Jeden Montag schreibt die KI automatisch 2-3 Sätze Lob für die Trainingsvorwoche
-- Nur Lob, kein Rat, kein Druck — positive Bestärkung
-- Basiert auf den geloggten Trainingseinheiten der Vorwoche
+### Züchter-Plattform (vollständig)
-### Wetter & Zecken-Warnung
-- Wetter-Chip direkt in der App (Open-Meteo API, ohne API-Key)
-- Zecken-Warnung regelbasiert: aktiv März–Oktober bei Temperatur >7°C
-- Push-Benachrichtigungen für Zecken-Saison
+Ban Yaro ist die erste Hunde-App mit vollständiger Züchter-Unterstützung:
-### Giftköder-Alarm
-- Giftköder-Meldungen mit GPS-Koordinaten und Foto
-- Push-Benachrichtigung für alle Nutzer im konfigurierbaren Umkreis
-- Interaktive Karte (OpenStreetMap/Leaflet)
-- Automatisches Ablaufdatum nach 7 Tagen
+**Züchter-Verifizierung:**
+- Antrag mit Dokumenten-Upload (VDH-Ausweis, Zuchtzulassung)
+- Admin-Prüfung und Freischaltung
+- Verifiziertes Züchter-Profil mit öffentlicher Seite (banyaro.app/breeder/{zwingername})
-### Sicherheit & Community-Alerts
-- Verlorener Hund: Alert mit Foto und letzter GPS-Position
-- Nearby-Alerts: Push-Benachrichtigungen für Ereignisse in der Nähe
+**Wurfbörse:**
+- Öffentliche Wurfankündigungen für alle Nutzer zugänglich (banyaro.app/wurfboerse)
+- Filtersuche nach Rasse und Status (geplant / verfügbar / geboren)
+- Käufer schreiben direkt per integriertem Chat an den Züchter
+- Vollständige Eltern-Dokumentation sichtbar: Gesundheitstests, Gentests, Stammbaum
-### NFC-Halsband-Tags
-- Jeder Hund hat eine öffentliche URL (ohne Login sichtbar)
-- "Ich habe diesen Hund gefunden"-Button → Besitzer bekommt Push-Benachrichtigung
-- Notfallkontakt ohne Telefonnummer preiszugeben
-- Physische NFC-Tags erhältlich (Shop)
+**Wurfverwaltung:**
+- CRUD für Würfe und einzelne Welpen
+- Gewichtsverlauf pro Welpe
+- Foto-System mit Sichtbarkeits-Stufen: öffentlich / nach Anfrage / privat
+- Automatisch ausgefüllter Kaufvertrag als druckbares HTML-Dokument
-### Gassi-Community
-- Gassi-Treffen erstellen und beitreten
-- GPS-Routen aufzeichnen und teilen (mit Anti-Cheat-Validierung)
-- Routen bewerten (Untergrund, Schatten, Leinenpflicht, Sicherheit)
-- Beliebte Routen entdecken
+**Zuchtkartei:**
+- Hunde-Stammdaten: Name, Rufname, Chip, Zuchtbuchnummer, Eltern (Vater/Mutter-Verknüpfung)
+- Gesundheitstests: HD, ED, OCD, Augen, Herz, Patella, ZTP — mit farbigen Ergebnis-Badges
+- Genetische Tests: MDR1, PRA, DM, vWD und weitere DNA-Marker (clear/carrier/affected)
+- Titel & Auszeichnungen: CAC, CACIB, BOB, IPO, BH — chronologisch mit Richter und Ort
-### Hundesitting-Netzwerk
-- Sitter-Profile mit Erfahrung und Bewertungen
-- Buchungsanfragen und Kalender
-- Nur 8% Provision (vs. 20% bei Rover/Pawshake)
-- Bewertungen verifizierter Buchungen
+**Stammbaum:**
+- Visualisierung bis 4 Generationen als horizontales CSS-Grid
+- Klickbare Knoten navigieren zum jeweiligen Hunde-Profil
+- Teilen-Link für Käufer-Dokumentation
+- Öffentliches Hunde-Profil (banyaro.app/zucht-profil?id={id})
-### Forum
-- Rassen-basierte Foren
-- KI-Zusammenfassung langer Threads
-- Experten-Badge (Tierarzt, Trainer)
+**Inzucht-Koeffizient:**
+- Automatische Berechnung nach Wright's Formel (bis 8 Generationen)
+- Ampel-Bewertung: optimal <2,5% / akzeptabel <6,25% / erhöht <12,5% / kritisch ≥12,5%
+- Probeverpaarung: simuliert beliebige Anpaarung ohne Speicherung
-### Hunde-Wiki — Rassendatenbank
-- 1003 Hunderassen, 97,6% KI-angereichert via Wikipedia-grounded Recherche
-- Inhalte: Charakter, Größe, Aktivität, Eignung, Lebensdauer, Temperament
-- Community-Fotos im Wiki: User können Fotos einreichen (mit Bildrechte-Bestätigung)
-- Moderatoren geben Community-Fotos frei, anschließend Galerie-Ansicht
-- Wiki-Foto-Badge als Gamification-Belohnung für Foto-Einreicher
-- "Passt diese Rasse zu mir?" Quiz für angehende Hundebesitzer
+**Tierschutz-Check (immer aktiv, nicht abschaltbar):**
+- Läuft automatisch bei jeder Verpaarung und jedem neuen Wurf
+- Prüft: IK, Alter der Zuchthündin (min. 18 Monate), Deckpause (min. 12 Monate),
+ Wurfanzahl (max. 4 empfohlen, kritisch ab 6), genetische Risiken
+- Farbcodierte Rückmeldung: grün (alles ok) / gelb (Hinweis) / rot (kritisch)
+- Bei "trotzdem fortfahren" auf rotem Befund: automatische Admin-Benachrichtigung
+- Philosophie: informieren statt blockieren, aber volle Transparenz und Accountability
-### Hunde-Knigge
-- Ratgeber für Begegnungen (fremder Hund, Kinder, Radfahrer)
-- Regeln in ÖPNV und öffentlichen Orten
-- Haftpflicht-Ratgeber
+**KI-Züchter-Assistenz:**
+- Wurfankündigungen schreiben (KI generiert Text aus Eltern-Profilen)
+- Genetik-Erklärung für Käufer (verständliche Sprache) und Züchter (fachlich)
+- Paarungsanalyse mit Empfehlung (empfohlen / bedingt / nicht empfohlen)
+- Hunde-Beschreibungen für öffentliche Profile
+- Jahresbericht mit Trends und Empfehlungen
+- Privilegierte Rollen (Züchter, Moderatoren, Admins) nutzen Claude Sonnet direkt
-### Events & Kultur
-- Agility-Turniere und Hundeausstellungen (VDH-Import)
+**Datenexport:**
+- Vollständiger Export als ZIP: HTML-Dossier (druckbar, Stammbaum-Visualisierung)
+ und ODS-Tabelle (editierbar in LibreOffice/Excel)
+- 7 Tabellenblätter: Hunde, Gesundheitstests, Gentests, Titel, Würfe, Welpen, Gewichte
+- Keine Datenfalle: Züchter können jederzeit alle eigenen Daten exportieren
+
+### Community-Features
+- Giftköder-Alarm mit Push-Benachrichtigungen
+- Verlorener Hund Alarm
+- Gassi-Treffen organisieren und finden
+- GPS-Routen aufzeichnen, teilen, bewerten
+- Hundesitting-Netzwerk (nur 8% Provision vs. 20% bei Rover/Pawshake)
+- Forum mit Rassen-basierten Unterforen
+- Direktnachrichten / Chat
+- Freundschaften und Nutzer-Profile
+
+### Wissen
+- Hunde-Wiki: 1003 Hunderassen, Wikipedia-grounded, KI-angereichert
+- Community-Fotos mit Bildrechte-Bestätigung und Moderation
+- Hunde-Knigge (Begegnungen, ÖPNV, Haftpflicht)
- Hundefilme-Datenbank mit "Stirbt der Hund?"-Rubrik
-- Veranstaltungskalender
-
-### Hundefreundliche Orte
-- Crowd-sourced Datenbank hundefreundlicher Orte
-- Restaurants, Parks, Geschäfte
-- Detaillierte Bewertungen
-
-### Gamification & Push
-- Badges, Streaks, XP — trägt zur Nutzerbindung bei
-- Wiki-Foto-Badge für Community-Foto-Beiträge
-- Push-Notifications für Alerts, Erinnerungen, Wöchentlicher Lober
-- Offline-Modus via Service Worker
+- Erste Hilfe Notfallratgeber
## KI-Integration
-Ban Yaro nutzt KI an mehreren Stellen der Plattform:
-
-- **Lokale KI**: LM Studio (Gemma-4-31B) auf eigenem Server — für datenschutzkritische Anfragen
-- **Cloud-KI**: Claude (Anthropic, Modell: claude-sonnet-4-6) als Fallback und für rechenintensive Aufgaben
-- **Symptom-Checker**: KI-gestützte Ersteinschätzung (kostenlos)
-- **Virtueller KI-Trainer**: Analysiert letzte 20 Trainings-Sessions, erstellt täglich priorisierte Übungsempfehlung
-- **Wöchentlicher Lober**: Vollautomatisch jeden Montag per APScheduler, lobt die Vorwoche in 2-3 Sätzen
-- **Breed-Enricher**: Wikipedia-grounded Anreicherung von 1003 Rassen-Datensätzen (97,6% abgeschlossen)
-- **KI-Trainingsplan** (Plus-Feature): Erstellt individuellen Trainingsplan auf Basis von Hund und Fortschritt
+Ban Yaro nutzt KI an mehreren Stellen:
+- **Privilegierte Nutzer** (Züchter, Moderatoren, Admins): Claude Sonnet (Anthropic) primär
+- **Standard-Nutzer**: Lokales LLM (LM Studio, Gemma-4-31B) primär, Claude als Fallback
+- **Tierschutz-Check**: Regelbasiert, keine KI — läuft immer zuverlässig
+- **Symptom-Checker, KI-Trainer, Lober**: Für alle kostenfrei
+- **Züchter-KI**: Wurfankündigungen, Genetik-Erklärungen, Paarungsanalyse, Jahresbericht
## Technologie
- Progressive Web App (PWA) — installierbar ohne App Store
-- Offline-fähig via Service Worker (Cache-Strategie mit Versionierung)
- Backend: Python/FastAPI + SQLite
- Frontend: Vanilla JS, kein Framework
-- Karten: Leaflet.js + OpenStreetMap (kein Google Maps, kein API-Key)
-- Wetter: Open-Meteo (kein API-Key erforderlich)
+- Karten: Leaflet.js + OpenStreetMap
- Hosting: Deutschland (DSGVO-konform)
- Analytics: Umami v2 (cookieless, DSGVO-konform)
-- KI lokal: LM Studio (Gemma-4-31B) auf eigenem Server
+- KI lokal: LM Studio (Gemma-4-31B)
- KI Cloud: Claude API (claude-sonnet-4-6, Anthropic)
-- Push-Notifications: Web Push (VAPID)
## Monetarisierung
-**Kostenlos (immer):**
-- Hunde-Profile
-- Tagebuch (unbegrenzte Einträge)
-- Pflege-System (43 rassenspezifische Tipps)
-- Symptom-Checker (KI)
-- Giftköder-Alarm & Zecken-Warnung
-- Verlorener Hund Alarm
-- Wiki & Knigge (1003 Rassen)
-- Training-Logging & KI-Trainer
-- Wöchentlicher Lober
-- Forum & Community
-- Gassi-Treffen & Routen
-- NFC-Halsband-Profil
-- Heimtierausweis (Druck)
+**Kostenlos:**
+- Alle Basis-Features inkl. Züchter-Antrag, Wurfverwaltung, Stammbaum, Tierschutz-Check
-**Ban Yaro Plus (ca. 4,99 €/Monat) — in Entwicklung:**
-- Alles aus Kostenlos
-- KI-Trainingsplan erstellen
-- Erweiterte Statistiken & Fortschrittsanalyse
+**Züchter-Provision** (geplant): Wurfbörse bleibt für Käufer kostenlos
-**Provisionen:**
-- Hundesitting: 8% Provision (Rover/Pawshake: 20%)
+**Ban Yaro Plus** (ca. 4,99 €/Monat, in Entwicklung):
+- KI-Trainingsplan, erweiterte Statistiken
-**Physische Produkte:**
-- NFC-Halsband-Tags (ab ca. 6 €)
+**Hundesitting**: 8% Provision
-## Community-Features
+## Öffentliche Seiten (ohne Login)
-- Forum mit Rassen-basierten Unterforen
-- Community-Fotos im Rassen-Wiki (Einreichung, Moderation, Freigabe)
-- Gassi-Treffen organisieren und finden
-- GPS-Routen teilen und bewerten
-- Hundesitting-Netzwerk (Bewertungen, verifizierte Buchungen)
-- Gamification: Badges, Streaks, XP, Wiki-Foto-Badge
+- https://banyaro.app — Landing Page
+- https://banyaro.app/info — Landing Page (Alias)
+- https://banyaro.app/wiki/rassen — Alle Hunderassen
+- https://banyaro.app/wiki/rasse/{slug} — Rassen-Detail
+- https://banyaro.app/wurfboerse — Öffentliche Wurfbörse (Welpen suchen)
+- https://banyaro.app/breeder/{zwingername} — Öffentliches Züchter-Profil
+- https://banyaro.app/knigge — Hunde-Knigge
+- https://banyaro.app/hund/{id} — Öffentliches Hunde-Profil (NFC-Tag)
-## Vergleich mit Konkurrenz
+## Öffentliche APIs
-| Funktion | Ban Yaro | Dogorama | PetDesk | Tractive |
-|----------|----------|----------|---------|----------|
-| Kostenlos nutzbar | Ja | Begrenzt | Nein | Nein |
-| DSGVO / EU-Hosting | Ja | Nein | Nein | Teilweise |
-| Giftköder-Alarm | Ja | Nein | Nein | Nein |
-| Gassi-Community | Ja | Ja | Nein | Nein |
-| Hundesitting | Ja (8%) | Nein | Nein | Nein |
-| Digitaler Impfpass | Ja | Nein | Ja | Nein |
-| NFC-Halsband-Tag | Ja | Nein | Nein | Nein |
-| Pflege-Tipps rassenspezifisch | Ja | Nein | Nein | Nein |
-| Rassen-Wiki (1003, KI-angereichert) | Ja | Nein | Nein | Nein |
-| Symptom-Checker (kostenlos) | Ja | Nein | Nein | Nein |
-| Offline-Modus | Ja | Nein | Nein | Nein |
-| Kein App Store | Ja | Nein | Nein | Nein |
-| Sitting-Provision | 8% | – | – | – |
+- GET https://banyaro.app/api/wiki/rassen — Liste aller Hunderassen
+- GET https://banyaro.app/api/wiki/rassen/{slug} — Rassen-Detail
+- GET https://banyaro.app/api/litters — Öffentliche Wurfankündigungen
+- GET https://banyaro.app/api/breeder/profil/{zwingername} — Züchter-Profil
+- GET https://banyaro.app/api/events — Aktuelle Hundeevents
+- GET https://banyaro.app/api/poison — Aktuelle Giftköder-Meldungen
+- GET https://banyaro.app/api/lost — Aktuelle Vermisst-Meldungen
+- GET https://banyaro.app/api/knigge/articles — Hunde-Knigge Artikel
+- GET https://banyaro.app/api/stats — Community-Statistiken
## Domains
@@ -216,14 +190,3 @@ Ban Yaro nutzt KI an mehreren Stellen der Plattform:
Website: https://banyaro.app
E-Mail: Über das Kontaktformular in der App
-
-## Öffentliche Daten-APIs (keine Authentifizierung nötig)
-
-- GET https://banyaro.app/api/wiki/rassen — Liste aller Hunderassen (1003 Einträge)
-- GET https://banyaro.app/api/wiki/rassen/{slug} — Details zu einer Rasse
-- GET https://banyaro.app/api/events — Aktuelle Hundeevents
-- GET https://banyaro.app/api/poison — Aktuelle Giftköder-Meldungen
-- GET https://banyaro.app/api/lost — Aktuelle Vermisst-Meldungen
-- GET https://banyaro.app/api/knigge/articles — Hunde-Knigge Artikel
-- GET https://banyaro.app/api/movies/list — Hundefilme-Datenbank
-- GET https://banyaro.app/api/stats — Community-Statistiken
diff --git a/backend/static/robots.txt b/backend/static/robots.txt
index 84f4af4..8637aa5 100644
--- a/backend/static/robots.txt
+++ b/backend/static/robots.txt
@@ -4,6 +4,9 @@ Allow: /info
Allow: /wiki/rassen
Allow: /wiki/rasse/
Allow: /hund/
+Allow: /breeder/
+Allow: /wurfboerse
+Allow: /knigge
Disallow: /api/
Disallow: /ausweis/
Disallow: /teilen/
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 38abe6d..a405935 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
-const CACHE_VERSION = 'by-v465';
+const CACHE_VERSION = 'by-v474';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
diff --git a/backend/welfare_check.py b/backend/welfare_check.py
new file mode 100644
index 0000000..802ab51
--- /dev/null
+++ b/backend/welfare_check.py
@@ -0,0 +1,220 @@
+"""BAN YARO — Tierschutz-Check für Züchter
+
+Regelbasierte Prüfung bei Verpaarungen und Wurfanlage.
+Läuft immer automatisch, ist nicht abschaltbar.
+"""
+import logging
+from datetime import date
+
+logger = logging.getLogger(__name__)
+
+# VDH-Richtwerte (Orientierung, nicht Gesetz)
+MIN_ALTER_MONATE = 18 # Mindestalter Zuchthündin
+MAX_ALTER_JAHRE = 8 # Empfohlenes Höchstalter
+MAX_WUERFE_LIFETIME = 4 # VDH-Empfehlung
+MIN_PAUSE_TAGE_WARN = 365 # 12 Monate Mindestpause (Warnung)
+MIN_PAUSE_TAGE_KRIT = 270 # 9 Monate (Kritisch)
+IK_WARN_PROZENT = 6.25
+IK_KRIT_PROZENT = 12.5
+
+
+def _level_max(a, b):
+ order = {"ok": 0, "info": 1, "warning": 2, "critical": 3}
+ return a if order.get(a, 0) >= order.get(b, 0) else b
+
+
+def check_welfare(conn, breeder_id: int,
+ vater_id: int = None,
+ mutter_id: int = None,
+ ik_prozent: float = None,
+ genetic_risks: list = None) -> dict:
+ """
+ Gibt zurück:
+ {
+ "level": "ok" | "info" | "warning" | "critical",
+ "issues": [{"code": str, "level": str, "text": str}],
+ "ok_points": [str] # Positive Punkte
+ }
+ """
+ issues = []
+ ok_pts = []
+ level = "ok"
+
+ # ------------------------------------------------------------------
+ # 1. Inzuchtkoeffizient
+ # ------------------------------------------------------------------
+ if ik_prozent is not None:
+ if ik_prozent >= IK_KRIT_PROZENT:
+ issues.append({
+ "code": "IK_KRITISCH",
+ "level": "critical",
+ "text": f"Inzuchtkoeffizient {ik_prozent:.1f}% — kritisch (≥{IK_KRIT_PROZENT}%). "
+ "Das erhöht das Risiko für erbliche Erkrankungen und Vitalitätsverlust erheblich.",
+ })
+ elif ik_prozent >= IK_WARN_PROZENT:
+ issues.append({
+ "code": "IK_ERHOEHT",
+ "level": "warning",
+ "text": f"Inzuchtkoeffizient {ik_prozent:.1f}% — erhöht (≥{IK_WARN_PROZENT}%). "
+ "VDH-Empfehlung liegt unter 6,25%.",
+ })
+ elif ik_prozent < 2.5:
+ ok_pts.append(f"Sehr niedriger Inzuchtkoeffizient ({ik_prozent:.1f}%) — genetische Vielfalt ist gut.")
+
+ # ------------------------------------------------------------------
+ # 2. Genetische Risiken (Träger × Träger / Affected)
+ # ------------------------------------------------------------------
+ if genetic_risks:
+ for r in genetic_risks:
+ if r.get("offspring_risk") and "betroffen" in r.get("offspring_risk", ""):
+ pct_text = r["offspring_risk"]
+ if "100%" in pct_text:
+ issues.append({
+ "code": f"GENETIK_KRITISCH_{r['marker']}",
+ "level": "critical",
+ "text": f"Genetisches Risiko {r['marker']}: {pct_text}. "
+ "Alle Nachkommen wären betroffen.",
+ })
+ elif "50%" in pct_text:
+ issues.append({
+ "code": f"GENETIK_HOCH_{r['marker']}",
+ "level": "critical",
+ "text": f"Genetisches Risiko {r['marker']}: {pct_text}.",
+ })
+ elif "25%" in pct_text:
+ issues.append({
+ "code": f"GENETIK_WARN_{r['marker']}",
+ "level": "warning",
+ "text": f"Genetisches Risiko {r['marker']}: {pct_text}. "
+ "Jeder 4. Welpe könnte betroffen sein.",
+ })
+
+ # ------------------------------------------------------------------
+ # 3. Zuchthündin-Checks
+ # ------------------------------------------------------------------
+ if mutter_id:
+ try:
+ hund = conn.execute(
+ "SELECT name, geburtsdatum FROM zucht_hunde WHERE id=?", (mutter_id,)
+ ).fetchone()
+
+ if hund:
+ hund_name = hund["name"] or "Zuchthündin"
+
+ # Alterscheck
+ if hund["geburtsdatum"]:
+ try:
+ geb = date.fromisoformat(str(hund["geburtsdatum"])[:10])
+ alter_tage = (date.today() - geb).days
+ alter_monate = alter_tage / 30.44
+
+ if alter_monate < MIN_ALTER_MONATE:
+ issues.append({
+ "code": "MUTTER_ZU_JUNG",
+ "level": "critical",
+ "text": f"{hund_name} ist erst {int(alter_monate)} Monate alt. "
+ f"Mindestalter für Erstzucht: {MIN_ALTER_MONATE} Monate. "
+ "Frühzucht belastet Körper und Psyche des Tieres erheblich.",
+ })
+ elif alter_monate > MAX_ALTER_JAHRE * 12:
+ issues.append({
+ "code": "MUTTER_ZU_ALT",
+ "level": "warning",
+ "text": f"{hund_name} ist {int(alter_monate / 12)} Jahre alt "
+ f"(empfohlenes Höchstalter: {MAX_ALTER_JAHRE} Jahre). "
+ "Ältere Hündinnen tragen ein höheres Geburtsrisiko.",
+ })
+ else:
+ ok_pts.append(f"{hund_name} ist in einem geeigneten Zuchtalter ({int(alter_monate)} Monate).")
+ except ValueError:
+ pass
+
+ # Wurfanzahl
+ wuerfe_gesamt = conn.execute(
+ "SELECT COUNT(*) FROM litters WHERE breeder_id=? AND mutter_id=?",
+ (breeder_id, mutter_id)
+ ).fetchone()[0]
+
+ if wuerfe_gesamt > 5:
+ issues.append({
+ "code": "ZU_VIELE_WUERFE",
+ "level": "critical",
+ "text": f"{hund_name} hatte bereits {wuerfe_gesamt} Würfe. "
+ f"Die VDH-Empfehlung liegt bei maximal {MAX_WUERFE_LIFETIME} Würfen pro Hündin.",
+ })
+ elif wuerfe_gesamt >= MAX_WUERFE_LIFETIME:
+ issues.append({
+ "code": "WUERFE_GRENZE",
+ "level": "warning",
+ "text": f"{hund_name} hat bereits {wuerfe_gesamt} Würfe — "
+ f"das entspricht der VDH-Empfehlung von max. {MAX_WUERFE_LIFETIME} Würfen.",
+ })
+ elif wuerfe_gesamt == 0:
+ ok_pts.append(f"{hund_name} ist noch ohne Vorwürfe.")
+
+ # Pause seit letztem Wurf
+ letzter = conn.execute(
+ "SELECT MAX(geburt_datum) FROM litters "
+ "WHERE breeder_id=? AND mutter_id=? AND geburt_datum IS NOT NULL",
+ (breeder_id, mutter_id)
+ ).fetchone()[0]
+
+ if letzter:
+ try:
+ letzter_date = date.fromisoformat(str(letzter)[:10])
+ abstand = (date.today() - letzter_date).days
+ if abstand < MIN_PAUSE_TAGE_KRIT:
+ issues.append({
+ "code": "PAUSE_KRITISCH",
+ "level": "critical",
+ "text": f"Letzter Wurf von {hund_name} liegt erst {abstand} Tage zurück "
+ f"({abstand // 30} Monate). "
+ "Für die Erholung von Körper und Hormonhaushalt werden "
+ "mindestens 12–15 Monate empfohlen.",
+ })
+ elif abstand < MIN_PAUSE_TAGE_WARN:
+ issues.append({
+ "code": "PAUSE_KURZ",
+ "level": "warning",
+ "text": f"Letzter Wurf von {hund_name}: {abstand // 30} Monate her. "
+ "VDH empfiehlt mindestens 12–15 Monate Pause zwischen Würfen.",
+ })
+ else:
+ ok_pts.append(f"Ausreichende Pause seit letztem Wurf von {hund_name} ({abstand // 30} Monate).")
+ except ValueError:
+ pass
+
+ except Exception as e:
+ logger.warning(f"Welfare-Check Mutter fehlgeschlagen: {e}")
+
+ # ------------------------------------------------------------------
+ # Vater-Alterscheck (weniger kritisch, aber der Vollständigkeit halber)
+ # ------------------------------------------------------------------
+ if vater_id:
+ try:
+ rüde = conn.execute(
+ "SELECT name, geburtsdatum FROM zucht_hunde WHERE id=?", (vater_id,)
+ ).fetchone()
+ if rüde and rüde["geburtsdatum"]:
+ geb = date.fromisoformat(str(rüde["geburtsdatum"])[:10])
+ alter_monate = (date.today() - geb).days / 30.44
+ if alter_monate < 12:
+ issues.append({
+ "code": "VATER_ZU_JUNG",
+ "level": "warning",
+ "text": f"Deckrüde {rüde['name']} ist erst {int(alter_monate)} Monate alt. "
+ "Empfohlenes Mindestalter: 12 Monate.",
+ })
+ except Exception:
+ pass
+
+ # ------------------------------------------------------------------
+ # Gesamtlevel
+ # ------------------------------------------------------------------
+ for issue in issues:
+ level = _level_max(level, issue["level"])
+
+ if level == "ok" and not ok_pts:
+ ok_pts.append("Alle geprüften Tierschutz-Kriterien sind erfüllt.")
+
+ return {"level": level, "issues": issues, "ok_points": ok_pts}