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

View file

@ -108,8 +108,16 @@ window.Page_zuchthunde = (() => {
${UI.icon('plus')} Hund anlegen
</button>
<button class="btn btn-secondary btn-sm" id="zh-trial-btn">
${UI.icon('dna')} Probeverpaarung
${UI.icon('heart-fill')} Probeverpaarung
</button>
<a href="/api/breeder/export" download class="btn btn-ghost btn-sm" id="zh-export-btn"
title="Alle Daten herunterladen (HTML + ODS)">
${UI.icon('download-simple')} Export
</a>
${_appState?.user?.ki_zucht_jahresbericht !== 0 ? `
<a class="btn btn-ghost btn-sm" id="zh-jahresbericht-btn">
${UI.icon('chart-bar')} Jahresbericht
</a>` : ''}
</div>
<div style="padding:0 0 var(--space-3)">
<input class="form-control" id="zh-search" type="search"
@ -123,6 +131,7 @@ window.Page_zuchthunde = (() => {
document.getElementById('zh-new-btn')?.addEventListener('click', () => _showHundForm(null));
document.getElementById('zh-trial-btn')?.addEventListener('click', () => _showTrialMatingModal());
document.getElementById('zh-jahresbericht-btn')?.addEventListener('click', () => _showJahresbericht());
document.getElementById('zh-search')?.addEventListener('input', e => {
_query = e.target.value.toLowerCase().trim();
@ -215,6 +224,13 @@ window.Page_zuchthunde = (() => {
});
});
el.querySelectorAll('.zh-ki-desc-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id);
_showKiDesc(id);
});
});
// Offene Sektionen wiederherstellen
Object.entries(_openSections).forEach(([id, sec]) => {
if (sec) _openSection(parseInt(id), sec);
@ -259,6 +275,10 @@ window.Page_zuchthunde = (() => {
title="Stammbaum">
${UI.icon('tree-structure')} Stammbaum
</button>
${_appState.user?.ki_zucht_beschreibung !== 0 ? `
<button class="btn btn-ghost btn-sm zh-ki-desc-btn" data-id="${h.id}">
${UI.icon('sparkle')} Beschreibung
</button>` : ''}
<button class="btn btn-ghost btn-sm zh-link-btn" data-id="${h.id}"
title="Profil-Link kopieren">
${UI.icon('link-simple')}
@ -1134,6 +1154,39 @@ window.Page_zuchthunde = (() => {
}).join('')
: `<li style="color:var(--c-text-muted)">Keine gemeinsamen Vorfahren gefunden.</li>`;
const welfare = result.welfare;
let welfareHTML = '';
if (welfare) {
const wColor = { ok: '#16a34a', info: '#3b82f6', warning: '#f59e0b', critical: '#dc2626' }[welfare.level] || '#6b7280';
const wTitle = { ok: 'Alles prima', info: 'Hinweis', warning: 'Bitte beachten', critical: 'Kritischer Hinweis' }[welfare.level];
const wIcon = { ok: 'check-circle', info: 'info', warning: 'warning', critical: 'warning-circle' }[welfare.level];
const wIssueHTML = (welfare.issues || []).map(i => `
<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid rgba(0,0,0,.06)">
<span style="color:${wColor};flex-shrink:0">${UI.icon('warning')}</span>
<span style="font-size:var(--text-sm)">${_esc(i.text)}</span>
</div>`).join('');
const wOkHTML = (welfare.ok_points || []).map(p => `
<div style="display:flex;gap:8px;padding:4px 0">
<span style="color:#16a34a;flex-shrink:0">${UI.icon('check')}</span>
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">${_esc(p)}</span>
</div>`).join('');
welfareHTML = `
<div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);margin-bottom:var(--space-2);
color:${wColor}">
${UI.icon(wIcon)} Tierschutz-Check: ${wTitle}
</div>
<div style="background:${wColor}18;border:1.5px solid ${wColor}40;border-radius:var(--radius-md);
padding:var(--space-3)">
${wIssueHTML || ''}
${wOkHTML}
</div>
</div>`;
}
const body = `
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);
@ -1149,6 +1202,7 @@ window.Page_zuchthunde = (() => {
</div>
</div>
</div>
${welfareHTML}
<div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);margin-bottom:var(--space-2)">
Gemeinsame Vorfahren
@ -1159,11 +1213,22 @@ window.Page_zuchthunde = (() => {
</div>
</div>`;
const kiPaarungBtn = _appState?.user?.ki_zucht_paarung !== 0
? `<button type="button" class="btn btn-secondary btn-sm" id="trial-ki-btn">
${UI.icon('sparkle')} KI-Analyse anfordern
</button>`
: '';
const footer = `
<button type="button" class="btn btn-secondary flex-1" id="zhresult-back">
${UI.icon('arrow-left')} Zurück
</button>
<button type="button" class="btn btn-primary flex-1" id="zhresult-close">Schließen</button>`;
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
${kiPaarungBtn}
<div style="display:flex;gap:var(--space-2)">
<button type="button" class="btn btn-secondary flex-1" id="zhresult-back">
${UI.icon('arrow-left')} Zurück
</button>
<button type="button" class="btn btn-primary flex-1" id="zhresult-close">Schließen</button>
</div>
</div>`;
UI.modal.open({
title: `${UI.icon('dna')} Ergebnis Probeverpaarung`,
@ -1173,6 +1238,164 @@ window.Page_zuchthunde = (() => {
document.getElementById('zhresult-close')?.addEventListener('click', UI.modal.close);
document.getElementById('zhresult-back')?.addEventListener('click', () => _showTrialMatingModal());
document.getElementById('trial-ki-btn')?.addEventListener('click', () => _showKiPaarung(result));
}
// ----------------------------------------------------------
// KI: Hund-Beschreibung
// ----------------------------------------------------------
async function _showKiDesc(hundId) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">KI erstellt Beschreibung…</p>`,
footer: '',
});
let text = '';
try {
const result = await API.zuchtKi.hundBeschreibung(hundId);
text = result.text || result.content || result.beschreibung || JSON.stringify(result);
} catch (err) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<p style="color:var(--c-danger)">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`,
footer: `
<button class="btn btn-secondary flex-1" id="ki-desc-copy">
${UI.icon('clipboard-text')} Kopieren
</button>
<button class="btn btn-primary flex-1" data-modal-close>Schließen</button>`,
});
document.getElementById('ki-desc-copy')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(text);
UI.toast.success('Text kopiert.');
} catch {
UI.toast.error('Kopieren nicht möglich.');
}
});
}
// ----------------------------------------------------------
// KI: Jahresbericht
// ----------------------------------------------------------
async function _showJahresbericht() {
UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">KI analysiert deine Zuchtkartei…</p>`,
footer: '',
});
let text = '';
try {
const result = await API.zuchtKi.jahresbericht();
text = result.text || result.content || result.bericht || JSON.stringify(result);
} catch (err) {
UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
body: `<p style="color:var(--c-danger)">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`,
footer: `
<button class="btn btn-secondary flex-1" id="ki-bericht-copy">
${UI.icon('clipboard-text')} Kopieren
</button>
<button class="btn btn-primary flex-1" data-modal-close>Schließen</button>`,
});
document.getElementById('ki-bericht-copy')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(text);
UI.toast.success('Bericht kopiert.');
} catch {
UI.toast.error('Kopieren nicht möglich.');
}
});
}
// ----------------------------------------------------------
// KI: Paarungsanalyse
// ----------------------------------------------------------
async function _showKiPaarung(trialResult) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Paarungsanalyse`,
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">KI analysiert die Verpaarung…</p>`,
footer: '',
});
let result;
try {
result = await API.zuchtKi.paarungAnalyse(
trialResult.vater_id,
trialResult.mutter_id,
trialResult.ik_prozent,
trialResult.welfare?.level
);
} catch (err) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Paarungsanalyse`,
body: `<p style="color:var(--c-danger)">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
const empfehlung = result.empfehlung || result.recommendation || '';
const text = result.text || result.content || result.analyse || JSON.stringify(result);
const empfehlungColor = {
empfohlen: '#16a34a',
bedingt: '#f59e0b',
nicht_empfohlen: '#dc2626',
}[empfehlung] || '#6b7280';
const empfehlungLabel = {
empfohlen: 'Empfohlen',
bedingt: 'Bedingt empfohlen',
nicht_empfohlen: 'Nicht empfohlen',
}[empfehlung] || empfehlung;
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Paarungsanalyse`,
body: `
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
${empfehlung ? `
<div style="padding:var(--space-3);border-radius:var(--radius-md);
background:${empfehlungColor}18;border:1.5px solid ${empfehlungColor}40;
font-weight:var(--weight-semibold);color:${empfehlungColor}">
${UI.icon('check-circle')} ${_esc(empfehlungLabel)}
</div>` : ''}
<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>
</div>`,
footer: `
<button class="btn btn-secondary flex-1" id="ki-paarung-copy">
${UI.icon('clipboard-text')} Kopieren
</button>
<button class="btn btn-primary flex-1" data-modal-close>Schließen</button>`,
});
document.getElementById('ki-paarung-copy')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(text);
UI.toast.success('Analyse kopiert.');
} catch {
UI.toast.error('Kopieren nicht möglich.');
}
});
}
// ----------------------------------------------------------