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:
parent
91340be5a3
commit
c8ae514c01
20 changed files with 2129 additions and 200 deletions
|
|
@ -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.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue