Feature: Tierschutz-Check, KI-Züchter-Features, Export, SEO-Update

Tierschutz-System (immer aktiv, nicht abschaltbar):
- welfare_check.py: regelbasierte Prüfung IK, Alter, Deckpause, Wurfanzahl, Genetik
- Grün/Gelb/Rot-Modal bei Wurf anlegen + Probeverpaarung
- Bei kritischem Befund + "Trotzdem fortfahren" → automatische Admin-Mail
- Tierschutz-Check nie durch Nutzer deaktivierbar

KI-Züchter-Features (pro User an/abschaltbar außer Tierschutz):
- routes/zucht_ki.py: 5 Endpunkte — Wurfankündigung, Genetik-Erklärung,
  Paarungsanalyse, Hund-Beschreibung, Jahresbericht
- Toggles in Einstellungen (ki_zucht_* Felder)
- KI-Buttons in litters.js + zuchthunde.js

KI-Routing: Privilegierte Rollen (Admin, Züchter, Moderator, Manager)
nutzen Claude Sonnet primär, lokales LLM als Fallback

Datenexport: routes/breeder_export.py — ZIP mit HTML-Dossier + ODS
(odfpy hinzugefügt in requirements.txt)

Admin-Profil: POST /admin/breeder/create-profile für Schnellprofil ohne
Antragsprozess; Admin-Rolle bleibt erhalten

Wurfformular: Dropdown aus Zuchtkartei für Vater/Mutter mit Auto-Fill;
litters.vater_id + mutter_id als FK auf zucht_hunde

Probeverpaarung: heart-fill Icon + Welfare-Block im Ergebnis

Landing Page: Züchter-Section + Feature-Gruppe, Meta-Tags, JSON-LD,
keywords, softwareVersion 2.1

SEO: llms.txt vollständig überarbeitet, robots.txt Züchter-Pfade,
sitemap.xml um Wurfbörse + Züchter-Profile erweitert

SW by-v474, APP_VER 451
This commit is contained in:
rene 2026-04-28 19:49:54 +02:00
parent 91340be5a3
commit c8ae514c01
20 changed files with 2129 additions and 200 deletions

220
backend/welfare_check.py Normal file
View file

@ -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 1215 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 1215 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}