Session 2026-04-21: SEO, Wiki-Anreicherung, Training, Lober
SEO & Crawler:
- robots.txt, llms.txt, sitemap.xml (508 Seiten bei Google)
- SSR-Seiten: /info, /wiki/rassen, /wiki/rasse/{slug}, /knigge
- Open Graph, JSON-LD, Breadcrumbs in index.html
Navigation:
- Training unter "Mein Hund", Wissen collapsible
- Welcome-Seite und Landing-Page auf 5-Gruppen-Struktur
Wiki:
- KI-Anreicherung (Claude API): beschreibung, vorkommen_de, Steckbrief
- "So einen hab ich" / Züchter-Verzeichnis
- Scheduler: 50 Rassen beim Start, 20/Nacht
Training:
- Session-Logging (Erfolgsquote, Stimmung, Zufriedenheit)
- Virtueller KI-Trainer (6h-Cache)
- Trainingskalender (Habit-Tracker)
- Top-Training → automatischer Tagebucheintrag
- Gamification ohne Druck: Badges, Streak, Stats
Fortschritts-Lober:
- Jeden Montag 09:00: Claude schreibt Lob-Text pro Hund
- Push + Karte im Tagebuch
Monitoring:
- 4× täglich Status-Mail mit Scheduler-Status + Wiki-Fortschritt
This commit is contained in:
parent
65d1cf6c7f
commit
180de32e57
22 changed files with 4351 additions and 189 deletions
|
|
@ -86,6 +86,9 @@ class UserPatch(BaseModel):
|
|||
is_banned: Optional[int] = None
|
||||
ban_reason: Optional[str] = None
|
||||
|
||||
class WikiEnrichBody(BaseModel):
|
||||
limit: int = 10
|
||||
|
||||
class ThreadAdminPatch(BaseModel):
|
||||
is_pinned: Optional[int] = None
|
||||
is_locked: Optional[int] = None
|
||||
|
|
@ -550,3 +553,33 @@ async def get_analytics(user=Depends(require_mod)):
|
|||
"pageviews": r_pv.json(),
|
||||
"top_pages": _to_list(r_pages),
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/admin/wiki/enrich — KI-Rassen-Anreicherung anstoßen
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/wiki/enrich")
|
||||
async def wiki_enrich(data: WikiEnrichBody, user=Depends(require_mod)):
|
||||
from scraper.breed_enricher import enrich_breeds
|
||||
limit = max(1, min(data.limit, 100))
|
||||
enriched = await enrich_breeds(limit)
|
||||
with db() as conn:
|
||||
remaining = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_rassen WHERE ki_enriched=0"
|
||||
).fetchone()[0]
|
||||
return {"enriched": enriched, "remaining": remaining}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/admin/wiki/zuchter/{id} — Züchter-Eintrag löschen (Admin/Mod)
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/wiki/zuchter/{zuchter_id}", status_code=204)
|
||||
async def admin_delete_zuchter(zuchter_id: int, user=Depends(require_mod)):
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Züchter nicht gefunden.")
|
||||
conn.execute("DELETE FROM wiki_zuchter WHERE id=?", (zuchter_id,))
|
||||
_audit(conn, user, "wiki_zuchter_delete", f"zuchter:{zuchter_id}")
|
||||
|
|
|
|||
35
backend/routes/praise.py
Normal file
35
backend/routes/praise.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""BAN YARO — Fortschritts-Lober"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from auth import get_current_user
|
||||
from database import db
|
||||
from datetime import date
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def _current_week_key():
|
||||
d = date.today()
|
||||
return f"{d.isocalendar()[0]}-W{d.isocalendar()[1]:02d}"
|
||||
|
||||
@router.get("/current")
|
||||
async def get_current_praise(dog_id: int = Query(...), user=Depends(get_current_user)):
|
||||
"""Gibt den Lob-Text der aktuellen Woche zurück."""
|
||||
week_key = _current_week_key()
|
||||
with db() as conn:
|
||||
# Sicherheitscheck: Hund gehört dem User?
|
||||
dog = conn.execute(
|
||||
"""SELECT d.id, d.name FROM dogs d
|
||||
LEFT JOIN dog_shares ds ON ds.dog_id=d.id AND ds.shared_with_id=?
|
||||
WHERE d.id=? AND (d.user_id=? OR ds.id IS NOT NULL)""",
|
||||
(user["id"], dog_id, user["id"])
|
||||
).fetchone()
|
||||
if not dog:
|
||||
return {"praise": None}
|
||||
|
||||
row = conn.execute(
|
||||
"SELECT praise_text, generated_at FROM weekly_praise WHERE dog_id=? AND week_key=?",
|
||||
(dog_id, week_key)
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
return {"praise": None, "week_key": week_key}
|
||||
return {"praise": row["praise_text"], "week_key": week_key, "generated_at": row["generated_at"]}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
"""BAN YARO — Übungs- & Trainingsfortschritt"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
import datetime
|
||||
import ki
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
|
||||
|
|
@ -154,3 +156,459 @@ async def get_suggestions(user=Depends(get_current_user)):
|
|||
})
|
||||
|
||||
return suggestions[:2] # max 2 Empfehlungen
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Training: Session-Protokoll
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
STIMMUNGS_LABELS = {
|
||||
"aufmerksam": "aufmerksam",
|
||||
"muede": "müde",
|
||||
"abgelenkt": "abgelenkt",
|
||||
"super": "super motiviert",
|
||||
}
|
||||
|
||||
TRAINING_BADGES = [
|
||||
("training_first", 1, "Erste Trainingseinheit",
|
||||
"Ihr habt gemeinsam die erste Einheit abgeschlossen \U0001f43e"),
|
||||
("training_5", 5, "5 Einheiten",
|
||||
"5 Trainingseinheiten \u2014 ihr seid dabei!"),
|
||||
("training_10", 10, "10 Einheiten",
|
||||
"10 Einheiten! {name} macht gro\u00dfartige Fortschritte."),
|
||||
("training_25", 25, "25 Einheiten",
|
||||
"25 Einheiten \u2014 eine echte Trainingspartnerschaft!"),
|
||||
("training_50", 50, "50 Einheiten",
|
||||
"50 Einheiten! {name} und du seid ein echtes Team."),
|
||||
("training_top_5", 5, "5 Top-Trainings",
|
||||
"5 Top-Trainings \u2014 {name} ist ein Schnellerner!"),
|
||||
]
|
||||
|
||||
|
||||
def _check_badges(conn, user_id: int, dog_name: str) -> list:
|
||||
"""Prüft und vergibt Trainings-Badges. Gibt neu verdiente Badges zurück."""
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE user_id=?",
|
||||
(user_id,)
|
||||
).fetchone()[0]
|
||||
top_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE user_id=? AND ist_top=1",
|
||||
(user_id,)
|
||||
).fetchone()[0]
|
||||
|
||||
existing = {
|
||||
r[0] for r in conn.execute(
|
||||
"SELECT badge_id FROM user_badges WHERE user_id=?", (user_id,)
|
||||
).fetchall()
|
||||
}
|
||||
|
||||
new_badges = []
|
||||
for badge_id, threshold, title, desc_tpl in TRAINING_BADGES:
|
||||
if badge_id in existing:
|
||||
continue
|
||||
count = top_count if badge_id == "training_top_5" else total
|
||||
if count >= threshold:
|
||||
desc = desc_tpl.format(name=dog_name)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO user_badges (user_id, badge_id) VALUES (?,?)",
|
||||
(user_id, badge_id)
|
||||
)
|
||||
new_badges.append({"id": badge_id, "title": title, "desc": desc})
|
||||
|
||||
return new_badges
|
||||
|
||||
|
||||
class SessionCreate(BaseModel):
|
||||
dog_id: int
|
||||
exercise_id: str
|
||||
exercise_name: str
|
||||
datum: Optional[str] = None
|
||||
wiederholungen: int = 1
|
||||
erfolgsquote: int = 50
|
||||
hund_stimmung: str = "aufmerksam"
|
||||
zufriedenheit: int = 3
|
||||
notiz: Optional[str] = None
|
||||
tagebuch_eintrag: bool = False
|
||||
|
||||
|
||||
@router.post("/sessions")
|
||||
async def log_session(body: SessionCreate, user=Depends(get_current_user)):
|
||||
uid = user["id"]
|
||||
|
||||
with db() as conn:
|
||||
# Hund-Zugehörigkeit prüfen
|
||||
dog = conn.execute(
|
||||
"SELECT id, name FROM dogs WHERE id=? AND user_id=?",
|
||||
(body.dog_id, uid)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
dog_name = dog["name"]
|
||||
datum = body.datum or datetime.date.today().isoformat()
|
||||
ist_top = int(body.erfolgsquote >= 80 and body.zufriedenheit >= 4)
|
||||
|
||||
cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO training_sessions
|
||||
(user_id, dog_id, exercise_id, exercise_name, datum,
|
||||
wiederholungen, erfolgsquote, hund_stimmung, zufriedenheit,
|
||||
notiz, ist_top)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
""",
|
||||
(uid, body.dog_id, body.exercise_id, body.exercise_name, datum,
|
||||
body.wiederholungen, body.erfolgsquote, body.hund_stimmung,
|
||||
body.zufriedenheit, body.notiz, ist_top)
|
||||
)
|
||||
session_id = cur.lastrowid
|
||||
|
||||
# Badges prüfen
|
||||
new_badges = _check_badges(conn, uid, dog_name)
|
||||
|
||||
# Tagebucheintrag erstellen?
|
||||
diary_entry_id = None
|
||||
if body.tagebuch_eintrag or ist_top:
|
||||
stimmung_label = STIMMUNGS_LABELS.get(body.hund_stimmung, body.hund_stimmung)
|
||||
if ist_top:
|
||||
titel = f"\U0001f3af {body.exercise_name} \u2014 Top-Training!"
|
||||
else:
|
||||
titel = f"\U0001f3af Training: {body.exercise_name}"
|
||||
text_parts = [
|
||||
f"{body.wiederholungen} Wiederholungen \u00b7 "
|
||||
f"Erfolgsquote: {body.erfolgsquote}% \u00b7 "
|
||||
f"Stimmung: {stimmung_label}"
|
||||
]
|
||||
if body.notiz:
|
||||
text_parts.append(f"\n\n{body.notiz}")
|
||||
eintrag_text = "".join(text_parts)
|
||||
|
||||
diary_cur = conn.execute(
|
||||
"""
|
||||
INSERT INTO diary (dog_id, datum, typ, titel, text)
|
||||
VALUES (?,?,?,?,?)
|
||||
""",
|
||||
(body.dog_id, datum, "training", titel, eintrag_text)
|
||||
)
|
||||
diary_entry_id = diary_cur.lastrowid
|
||||
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id) VALUES (?,?)",
|
||||
(diary_entry_id, body.dog_id)
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
"UPDATE training_sessions SET diary_entry_id=? WHERE id=?",
|
||||
(diary_entry_id, session_id)
|
||||
)
|
||||
|
||||
session = {
|
||||
"id": session_id,
|
||||
"user_id": uid,
|
||||
"dog_id": body.dog_id,
|
||||
"exercise_id": body.exercise_id,
|
||||
"exercise_name": body.exercise_name,
|
||||
"datum": datum,
|
||||
"wiederholungen": body.wiederholungen,
|
||||
"erfolgsquote": body.erfolgsquote,
|
||||
"hund_stimmung": body.hund_stimmung,
|
||||
"zufriedenheit": body.zufriedenheit,
|
||||
"notiz": body.notiz,
|
||||
"ist_top": bool(ist_top),
|
||||
"diary_entry_id": diary_entry_id,
|
||||
}
|
||||
|
||||
return {
|
||||
"session": session,
|
||||
"ist_top": bool(ist_top),
|
||||
"badges": new_badges,
|
||||
"diary_entry_id": diary_entry_id,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/sessions")
|
||||
async def get_sessions(
|
||||
dog_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
uid = user["id"]
|
||||
with db() as conn:
|
||||
dog = conn.execute(
|
||||
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, uid)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT * FROM training_sessions
|
||||
WHERE user_id=? AND dog_id=?
|
||||
ORDER BY datum DESC, created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
(uid, dog_id, limit, offset)
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/calendar")
|
||||
async def get_calendar(
|
||||
dog_id: int,
|
||||
year: Optional[int] = None,
|
||||
month: Optional[int] = None,
|
||||
user=Depends(get_current_user)
|
||||
):
|
||||
uid = user["id"]
|
||||
today = datetime.date.today()
|
||||
year = year or today.year
|
||||
month = month or today.month
|
||||
|
||||
month_str = f"{year:04d}-{month:02d}"
|
||||
|
||||
with db() as conn:
|
||||
dog = conn.execute(
|
||||
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, uid)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT datum,
|
||||
COUNT(*) AS count,
|
||||
MAX(ist_top) AS top
|
||||
FROM training_sessions
|
||||
WHERE user_id=? AND dog_id=?
|
||||
AND datum LIKE ?
|
||||
GROUP BY datum
|
||||
""",
|
||||
(uid, dog_id, month_str + "-%")
|
||||
).fetchall()
|
||||
|
||||
days = {
|
||||
r["datum"]: {"count": r["count"], "top": bool(r["top"])}
|
||||
for r in rows
|
||||
}
|
||||
return {"days": days}
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_stats(dog_id: int, user=Depends(get_current_user)):
|
||||
uid = user["id"]
|
||||
with db() as conn:
|
||||
dog = conn.execute(
|
||||
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, uid)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
total_sessions = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE user_id=? AND dog_id=?",
|
||||
(uid, dog_id)
|
||||
).fetchone()[0]
|
||||
|
||||
total_exercises = conn.execute(
|
||||
"SELECT COUNT(DISTINCT exercise_id) FROM training_sessions WHERE user_id=? AND dog_id=?",
|
||||
(uid, dog_id)
|
||||
).fetchone()[0]
|
||||
|
||||
avg_row = conn.execute(
|
||||
"SELECT AVG(erfolgsquote) FROM training_sessions WHERE user_id=? AND dog_id=?",
|
||||
(uid, dog_id)
|
||||
).fetchone()
|
||||
avg_erfolgsquote = round(avg_row[0], 1) if avg_row[0] is not None else 0.0
|
||||
|
||||
best_row = conn.execute(
|
||||
"""
|
||||
SELECT exercise_name, AVG(erfolgsquote) AS avg_e
|
||||
FROM training_sessions
|
||||
WHERE user_id=? AND dog_id=?
|
||||
GROUP BY exercise_id
|
||||
ORDER BY avg_e DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(uid, dog_id)
|
||||
).fetchone()
|
||||
best_exercise = (
|
||||
{"name": best_row["exercise_name"], "avg_erfolg": round(best_row["avg_e"], 1)}
|
||||
if best_row else None
|
||||
)
|
||||
|
||||
training_days = conn.execute(
|
||||
"SELECT COUNT(DISTINCT datum) FROM training_sessions WHERE user_id=? AND dog_id=?",
|
||||
(uid, dog_id)
|
||||
).fetchone()[0]
|
||||
|
||||
top_sessions = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE user_id=? AND dog_id=? AND ist_top=1",
|
||||
(uid, dog_id)
|
||||
).fetchone()[0]
|
||||
|
||||
today = datetime.date.today()
|
||||
week_start = (today - datetime.timedelta(days=today.weekday())).isoformat()
|
||||
this_week = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE user_id=? AND dog_id=? AND datum >= ?",
|
||||
(uid, dog_id, week_start)
|
||||
).fetchone()[0]
|
||||
|
||||
month_start = today.replace(day=1).isoformat()
|
||||
this_month = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE user_id=? AND dog_id=? AND datum >= ?",
|
||||
(uid, dog_id, month_start)
|
||||
).fetchone()[0]
|
||||
|
||||
# Streak: aufeinanderfolgende Tage bis heute
|
||||
day_rows = conn.execute(
|
||||
"""
|
||||
SELECT DISTINCT datum FROM training_sessions
|
||||
WHERE user_id=? AND dog_id=?
|
||||
ORDER BY datum DESC
|
||||
""",
|
||||
(uid, dog_id)
|
||||
).fetchall()
|
||||
streak_days = 0
|
||||
check = today
|
||||
for row in day_rows:
|
||||
d = datetime.date.fromisoformat(row["datum"])
|
||||
if d == check:
|
||||
streak_days += 1
|
||||
check -= datetime.timedelta(days=1)
|
||||
else:
|
||||
break
|
||||
|
||||
return {
|
||||
"total_sessions": total_sessions,
|
||||
"total_exercises": total_exercises,
|
||||
"avg_erfolgsquote": avg_erfolgsquote,
|
||||
"best_exercise": best_exercise,
|
||||
"training_days": training_days,
|
||||
"top_sessions": top_sessions,
|
||||
"this_week": this_week,
|
||||
"this_month": this_month,
|
||||
"streak_days": streak_days,
|
||||
}
|
||||
|
||||
|
||||
class KiFeedbackRequest(BaseModel):
|
||||
dog_id: int
|
||||
|
||||
|
||||
@router.post("/ki-feedback")
|
||||
async def ki_feedback(body: KiFeedbackRequest, user=Depends(get_current_user)):
|
||||
uid = user["id"]
|
||||
|
||||
with db() as conn:
|
||||
dog = conn.execute(
|
||||
"SELECT id, name, rasse, geburtstag FROM dogs WHERE id=? AND user_id=?",
|
||||
(body.dog_id, uid)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
# Cache prüfen
|
||||
cache_row = conn.execute(
|
||||
"SELECT feedback, generated_at FROM training_ki_cache WHERE dog_id=?",
|
||||
(body.dog_id,)
|
||||
).fetchone()
|
||||
|
||||
if cache_row:
|
||||
generated_at = datetime.datetime.fromisoformat(cache_row["generated_at"])
|
||||
age_hours = (datetime.datetime.utcnow() - generated_at).total_seconds() / 3600
|
||||
|
||||
# Neue Sessions seit letzter Generierung?
|
||||
new_since = conn.execute(
|
||||
"SELECT COUNT(*) FROM training_sessions WHERE dog_id=? AND created_at > ?",
|
||||
(body.dog_id, cache_row["generated_at"])
|
||||
).fetchone()[0]
|
||||
|
||||
if age_hours < 6 and new_since == 0:
|
||||
return {"feedback": cache_row["feedback"], "cached": True}
|
||||
|
||||
# Hund-Alter berechnen
|
||||
alter_str = "unbekannt"
|
||||
if dog["geburtstag"]:
|
||||
try:
|
||||
geb = datetime.date.fromisoformat(dog["geburtstag"])
|
||||
alter_jahre = (datetime.date.today() - geb).days // 365
|
||||
alter_str = f"{alter_jahre} Jahre"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Letzte 20 Sessions laden
|
||||
sessions = conn.execute(
|
||||
"""
|
||||
SELECT datum, exercise_name, wiederholungen, erfolgsquote,
|
||||
hund_stimmung, zufriedenheit, notiz, ist_top
|
||||
FROM training_sessions
|
||||
WHERE dog_id=?
|
||||
ORDER BY datum DESC, created_at DESC
|
||||
LIMIT 20
|
||||
""",
|
||||
(body.dog_id,)
|
||||
).fetchall()
|
||||
|
||||
# Übungsfortschritt laden
|
||||
progress_rows = conn.execute(
|
||||
"SELECT exercise_id, status FROM exercise_progress WHERE user_id=?",
|
||||
(uid,)
|
||||
).fetchall()
|
||||
|
||||
# Prompt zusammenbauen
|
||||
dog_name = dog["name"]
|
||||
dog_rasse = dog["rasse"] or "unbekannter Rasse"
|
||||
|
||||
sessions_text = "\n".join(
|
||||
f" {r['datum']}: {r['exercise_name']}, "
|
||||
f"{r['wiederholungen']}x, {r['erfolgsquote']}% Erfolg, "
|
||||
f"Stimmung: {STIMMUNGS_LABELS.get(r['hund_stimmung'], r['hund_stimmung'])}, "
|
||||
f"Zufriedenheit: {r['zufriedenheit']}/5"
|
||||
+ (f", Notiz: {r['notiz']}" if r["notiz"] else "")
|
||||
+ (" [TOP]" if r["ist_top"] else "")
|
||||
for r in sessions
|
||||
) or " (noch keine Sessions)"
|
||||
|
||||
progress_text = "\n".join(
|
||||
f" {r['exercise_id']}: {r['status']}"
|
||||
for r in progress_rows
|
||||
) or " (kein Fortschritt erfasst)"
|
||||
|
||||
prompt = (
|
||||
f"Hund: {dog_name}, {dog_rasse}, {alter_str}\n"
|
||||
f"Letzte Sessions:\n{sessions_text}\n"
|
||||
f"Übungsfortschritt:\n{progress_text}\n\n"
|
||||
"Antworte mit maximal 3 kurzen Abschnitten:\n"
|
||||
"1. Was gut läuft (1-2 Sätze, immer positiv beginnen)\n"
|
||||
"2. Konkrete Empfehlung (1-2 Sätze, spezifisch auf die Daten bezogen)\n"
|
||||
"3. Kleiner motivierender Abschluss (1 Satz)\n\n"
|
||||
"Sprich über den Hund, nicht über den Besitzer. Kein Druck, keine Forderungen."
|
||||
)
|
||||
system = (
|
||||
"Du bist ein einfühlsamer, positiver Hundetrainer. "
|
||||
"Analysiere die Trainingshistorie und gib kurzes, motivierendes Feedback auf Deutsch."
|
||||
)
|
||||
|
||||
try:
|
||||
feedback_text = await ki.complete(
|
||||
prompt,
|
||||
system=system,
|
||||
max_tokens=400,
|
||||
requires_premium=False,
|
||||
user_is_premium=user.get("is_premium", False),
|
||||
)
|
||||
except (ki.KIUnavailableError, ki.KIPremiumRequired) as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
||||
# Cache speichern
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO training_ki_cache (dog_id, feedback, generated_at)
|
||||
VALUES (?,?,datetime('now'))
|
||||
ON CONFLICT(dog_id) DO UPDATE
|
||||
SET feedback=excluded.feedback, generated_at=excluded.generated_at
|
||||
""",
|
||||
(body.dog_id, feedback_text)
|
||||
)
|
||||
|
||||
return {"feedback": feedback_text, "cached": False}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import logging
|
|||
from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
|
||||
from pydantic import BaseModel
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
from auth import get_current_user, get_current_user_optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
||||
|
|
@ -437,3 +437,241 @@ async def review_submission(sub_id: int, data: ReviewModel, user=Depends(get_cur
|
|||
pass
|
||||
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hilfsfunktion: Stats für eine Rasse zusammenstellen
|
||||
# ------------------------------------------------------------------
|
||||
def _build_stats(conn, rasse_slug: str, user_id=None) -> dict:
|
||||
dogs_count = conn.execute(
|
||||
"SELECT COUNT(DISTINCT user_id) FROM dogs WHERE LOWER(rasse) = LOWER(?)",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
hat_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_breed_interest WHERE rasse_slug=? AND typ='hat'",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
will_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_breed_interest WHERE rasse_slug=? AND typ='will'",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
zuchter_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_zuchter WHERE rasse_slug=? AND verified=1",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
berichte_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_berichte WHERE rasse=?",
|
||||
(rasse_slug,),
|
||||
).fetchone()[0]
|
||||
|
||||
user_interest = None
|
||||
if user_id:
|
||||
row = conn.execute(
|
||||
"SELECT typ FROM wiki_breed_interest WHERE user_id=? AND rasse_slug=?",
|
||||
(user_id, rasse_slug),
|
||||
).fetchone()
|
||||
if row:
|
||||
user_interest = row["typ"]
|
||||
|
||||
return {
|
||||
"dogs_count": dogs_count,
|
||||
"hat_count": hat_count,
|
||||
"will_count": will_count,
|
||||
"zuchter_count": zuchter_count,
|
||||
"berichte_count":berichte_count,
|
||||
"user_interest": user_interest,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/wiki/rassen/{slug}/stats
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/rassen/{slug}/stats")
|
||||
async def get_rasse_stats(slug: str, user=Depends(get_current_user_optional)):
|
||||
with db() as conn:
|
||||
rasse = conn.execute(
|
||||
"SELECT slug FROM wiki_rassen WHERE slug=?", (slug,)
|
||||
).fetchone()
|
||||
if not rasse:
|
||||
raise HTTPException(404, "Rasse nicht gefunden.")
|
||||
return _build_stats(conn, slug, user["id"] if user else None)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Schemas für Interesse und Züchter
|
||||
# ------------------------------------------------------------------
|
||||
class InteresseCreate(BaseModel):
|
||||
typ: str # "hat" oder "will"
|
||||
|
||||
class ZuchterCreate(BaseModel):
|
||||
rasse_slug: str
|
||||
name: str
|
||||
zwingername: str = ""
|
||||
ort: str = ""
|
||||
plz: str = ""
|
||||
bundesland: str = ""
|
||||
vdh_mitglied: int = 0
|
||||
website: str = ""
|
||||
telefon: str = ""
|
||||
beschreibung: str = ""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/wiki/rassen/{slug}/interesse
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/rassen/{slug}/interesse")
|
||||
async def set_interesse(slug: str, data: InteresseCreate, user=Depends(get_current_user)):
|
||||
if data.typ not in ("hat", "will"):
|
||||
raise HTTPException(400, "typ muss 'hat' oder 'will' sein.")
|
||||
with db() as conn:
|
||||
rasse = conn.execute(
|
||||
"SELECT slug FROM wiki_rassen WHERE slug=?", (slug,)
|
||||
).fetchone()
|
||||
if not rasse:
|
||||
raise HTTPException(404, "Rasse nicht gefunden.")
|
||||
conn.execute(
|
||||
"""INSERT INTO wiki_breed_interest (user_id, rasse_slug, typ)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(user_id, rasse_slug) DO UPDATE SET typ=excluded.typ""",
|
||||
(user["id"], slug, data.typ),
|
||||
)
|
||||
return _build_stats(conn, slug, user["id"])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/wiki/rassen/{slug}/interesse
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/rassen/{slug}/interesse")
|
||||
async def delete_interesse(slug: str, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"DELETE FROM wiki_breed_interest WHERE user_id=? AND rasse_slug=?",
|
||||
(user["id"], slug),
|
||||
)
|
||||
return _build_stats(conn, slug, user["id"])
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/wiki/rassen/{slug}/zuchter
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/rassen/{slug}/zuchter")
|
||||
async def get_zuchter_fuer_rasse(
|
||||
slug: str,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
):
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT z.id, z.rasse_slug, z.name, z.zwingername, z.ort, z.plz,
|
||||
z.bundesland, z.vdh_mitglied, z.website, z.telefon,
|
||||
z.beschreibung, z.created_at
|
||||
FROM wiki_zuchter z
|
||||
WHERE z.rasse_slug=? AND z.verified=1
|
||||
ORDER BY z.bundesland ASC, z.ort ASC
|
||||
LIMIT ? OFFSET ?""",
|
||||
(slug, limit, offset),
|
||||
).fetchall()
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*) FROM wiki_zuchter WHERE rasse_slug=? AND verified=1",
|
||||
(slug,),
|
||||
).fetchone()[0]
|
||||
return {"zuchter": [dict(r) for r in rows], "total": total}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /api/wiki/zuchter — Züchter einreichen
|
||||
# ------------------------------------------------------------------
|
||||
@router.post("/zuchter", status_code=201)
|
||||
async def create_zuchter(data: ZuchterCreate, user=Depends(get_current_user)):
|
||||
if not data.name.strip():
|
||||
raise HTTPException(400, "Name darf nicht leer sein.")
|
||||
with db() as conn:
|
||||
rasse = conn.execute(
|
||||
"SELECT slug FROM wiki_rassen WHERE slug=?", (data.rasse_slug,)
|
||||
).fetchone()
|
||||
if not rasse:
|
||||
raise HTTPException(400, "Ungültige Rasse.")
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO wiki_zuchter
|
||||
(rasse_slug, name, zwingername, ort, plz, bundesland,
|
||||
vdh_mitglied, website, telefon, beschreibung, verified, user_id)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,0,?)""",
|
||||
(
|
||||
data.rasse_slug, data.name.strip(),
|
||||
data.zwingername.strip() or None,
|
||||
data.ort.strip() or None,
|
||||
data.plz.strip() or None,
|
||||
data.bundesland.strip() or None,
|
||||
data.vdh_mitglied,
|
||||
data.website.strip() or None,
|
||||
data.telefon.strip() or None,
|
||||
data.beschreibung.strip() or None,
|
||||
user["id"],
|
||||
),
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM wiki_zuchter WHERE id=?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
return dict(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DELETE /api/wiki/zuchter/{id} — eigene Einreichung löschen
|
||||
# ------------------------------------------------------------------
|
||||
@router.delete("/zuchter/{zuchter_id}")
|
||||
async def delete_zuchter(zuchter_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id, user_id FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Züchter nicht gefunden.")
|
||||
is_admin = user.get("rolle") == "admin" or user.get("is_moderator")
|
||||
if row["user_id"] != user["id"] and not is_admin:
|
||||
raise HTTPException(403, "Nicht erlaubt.")
|
||||
conn.execute("DELETE FROM wiki_zuchter WHERE id=?", (zuchter_id,))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/wiki/zuchter/pending — unverifizierte Einreichungen (Mod/Admin)
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/zuchter/pending")
|
||||
async def list_zuchter_pending(user=Depends(get_current_user)):
|
||||
if not (user.get("is_moderator") or user.get("rolle") in ("admin", "moderator")):
|
||||
raise HTTPException(403, "Nur Moderatoren.")
|
||||
with db() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT z.*, u.name AS user_name
|
||||
FROM wiki_zuchter z
|
||||
LEFT JOIN users u ON u.id = z.user_id
|
||||
WHERE z.verified=0
|
||||
ORDER BY z.created_at ASC""",
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PATCH /api/wiki/zuchter/{id}/verify — Züchter freigeben (Mod/Admin)
|
||||
# ------------------------------------------------------------------
|
||||
@router.patch("/zuchter/{zuchter_id}/verify")
|
||||
async def verify_zuchter(zuchter_id: int, user=Depends(get_current_user)):
|
||||
if not (user.get("is_moderator") or user.get("rolle") in ("admin", "moderator")):
|
||||
raise HTTPException(403, "Nur Moderatoren.")
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Züchter nicht gefunden.")
|
||||
conn.execute(
|
||||
"UPDATE wiki_zuchter SET verified=1 WHERE id=?", (zuchter_id,)
|
||||
)
|
||||
result = conn.execute(
|
||||
"SELECT * FROM wiki_zuchter WHERE id=?", (zuchter_id,)
|
||||
).fetchone()
|
||||
return dict(result)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue