Session 2026-04-22: Training, Fixes, KI-Cloud, Dark-Mode

Training-System:
- Einheit-Dialog Bugs behoben (UI.toast callable, _dogId via _appState, activeDog.id)
- Virtueller Trainer (rein statistisch): üben/festigen/entdecken/levelup
  Empfehlungen auf Basis exercise_progress + sessions, Prognose bis 80%
- Stand erfassen Modal: alle Übungen auf einmal setzen (onboarding)
- Erfolgsindikatoren auf Karten: Ø-Quote + Trend-Pfeil + Anzahl Sessions
- exercise_progress → synthetische Stats im Trainer (ohne Sessions nutzbar)
- Levelup: Tricks empfehlen wenn ≥4 Grundkommandos sitzen
- Kommandos & Fähigkeiten im Hundeprofil + öffentlichem Profil
- 2 neue Problemverhalten-Übungen: Bellen/Kläffen, Enttriggern

Mobile/UI-Fixes:
- Übungskarten: Name + Difficulty oben, Buttons eigene Zeile (kein Umbruch)
- Trainingsgrundlagen: Padding in allen Karten, Hinweis-Boxen Dark-Mode-sicher
- Tab-Sichtbarkeit: Trainer/Suggestions nur auf Übungs-Tabs
- Tagebuch FAB (Neu-Eintrag Button) + Quick-Add Eintrag
- FAB Abstand fix (nav-bottom-height + safe-bottom)
- Suggestion-Karten rgba (Dark-Mode)
- routes.js + uebungen.js: alle Hellfarben → rgba (Dark-Mode-sicher)
- ui.js: UI.toast als callable Function-Object (war nur plain Object)

KI & Backend:
- KI_MODE=cloud + ANTHROPIC_API_KEY gesetzt
- ki.py: Cloud-Fallback wenn local nicht erreichbar + KI_MODE=cloud
- KI-Trainer Tageslimit 10 Anfragen/User + ki_daily_calls Tabelle
- Admin-Panel: KI-Nutzung (heute/Monat/User)
- Status-Report Fix (lost-Tabelle) → 06:00 + 18:00 täglich
- Wiki-Anreicherung läuft jetzt (50 Rassen Startup, 20/Nacht)
- landing.html: Trainings-Features in JSON-LD + Feature-Karten
This commit is contained in:
rene 2026-04-22 19:41:22 +02:00
parent 2b442ebd98
commit 44081a6b9d
16 changed files with 938 additions and 117 deletions

View file

@ -218,20 +218,80 @@ async def delete_photo(dog_id: int, user=Depends(get_current_user)):
)
# ------------------------------------------------------------------
# Fähigkeiten / Kommandos (für Profil + öffentliche Seite)
# ------------------------------------------------------------------
def _parse_exercise_name(exercise_id: str) -> str:
"""grundkommandos_Hier__Komm → 'Hier / Komm'"""
parts = exercise_id.split("_", 1)
if len(parts) < 2:
return exercise_id
return parts[1].replace("__", " / ").replace("_", " ")
def _load_skills(conn, dog_id: int, user_id: int) -> list:
"""Gibt Übungen mit Status 'sitzt' oder 'meistens' zurück, die mit diesem Hund trainiert wurden."""
rows = conn.execute(
"""
SELECT ep.exercise_id, ep.status,
(SELECT ts.exercise_name FROM training_sessions ts
WHERE ts.user_id = ep.user_id AND ts.dog_id = ?
AND ts.exercise_id = ep.exercise_id
ORDER BY ts.datum DESC, ts.created_at DESC LIMIT 1) AS exercise_name
FROM exercise_progress ep
WHERE ep.user_id = ?
AND ep.status IN ('sitzt', 'meistens')
AND EXISTS (SELECT 1 FROM training_sessions ts2
WHERE ts2.user_id = ep.user_id AND ts2.dog_id = ?
AND ts2.exercise_id = ep.exercise_id)
ORDER BY ep.status DESC, ep.exercise_id
""",
(dog_id, user_id, dog_id)
).fetchall()
return [
{
"exercise_id": r["exercise_id"],
"exercise_name": r["exercise_name"] or _parse_exercise_name(r["exercise_id"]),
"status": r["status"],
"tab": r["exercise_id"].split("_")[0],
}
for r in rows
]
@router.get("/{dog_id}/skills")
async def get_dog_skills(dog_id: int, user=Depends(get_current_user)):
uid = user["id"]
with db() as conn:
dog = conn.execute(
"SELECT id, user_id FROM dogs WHERE id=? AND (user_id=? OR id IN (SELECT dog_id FROM sitting_access WHERE friend_id=? AND expires_at > datetime('now')))",
(dog_id, uid, uid)
).fetchone()
if not dog:
raise HTTPException(404, "Hund nicht gefunden.")
return _load_skills(conn, dog_id, dog["user_id"])
# Öffentliches Profil (für NFC-Tag, kein Login nötig)
@router.get("/public/{dog_id}")
async def public_dog_profile(dog_id: int):
with db() as conn:
dog = conn.execute(
"""SELECT d.id, d.name, d.rasse, d.geburtstag, d.foto_url, d.bio,
u.name as besitzer_name
d.user_id, u.name as besitzer_name
FROM dogs d JOIN users u ON d.user_id=u.id
WHERE d.id=? AND d.is_public=1""",
(dog_id,)
).fetchone()
if not dog:
raise HTTPException(404, "Profil nicht gefunden oder nicht öffentlich.")
return dict(dog)
if not dog:
raise HTTPException(404, "Profil nicht gefunden oder nicht öffentlich.")
skills = _load_skills(conn, dog_id, dog["user_id"])
result = dict(dog)
result.pop("user_id", None)
result["skills"] = skills
return result
class FoundReport(BaseModel):