Routen-Navigation:
- POI-Marker: farbige Kreise mit Phosphor-Icons (wie Hauptkarte)
- Screensaver: Navi-Pfeil dreht sich via DeviceOrientationEvent (iOS+Android)
- Pfeil-Dämpfung: EMA α=0.12 mit Wrap-Around
- GPS-Distanz-Bug: Fortschritt nur wenn <500m zur Route
- fitBounds: User-Position nur wenn <20km von Route
- Screensaver: "zur Route" vs "verbleibend" kontextabhängig
- Richtungspfeile entlang Route (blau, max 7 Stück)
- Umkehren ins Route-Detail verschoben, Detail-Map rebuildet sich
- rk-header z-index:10 (Leaflet-Tiles liefen drüber)
- 2-Sek. Screensaver-Entsperrung
km-Tracking:
- route_walks Tabelle
- POST /api/routes/{id}/walked (≥50%)
- total_km = erstellte Routes + gelaufene route_walks
- Toast bei neuem Badge
Übungsfortschritt:
- exercise_progress + training_plan_progress Tabellen
- GET/POST /api/training/progress, /plan-progress, /suggestions
- uebungen.js: API-first + localStorage-Fallback + Auto-Migration
- Empfehlungs-Banner (regelbasiert)
- Toast bei "sitzt"
156 lines
6 KiB
Python
156 lines
6 KiB
Python
"""BAN YARO — Übungs- & Trainingsfortschritt"""
|
||
|
||
from fastapi import APIRouter, Depends
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
from database import db
|
||
from auth import get_current_user
|
||
|
||
router = APIRouter()
|
||
|
||
# ------------------------------------------------------------------
|
||
# Übungs-Status
|
||
# ------------------------------------------------------------------
|
||
class ProgressUpdate(BaseModel):
|
||
exercise_id: str
|
||
status: Optional[str] = None # null/noch-nicht/manchmal/meistens/sitzt
|
||
|
||
@router.get("/progress")
|
||
async def get_progress(user=Depends(get_current_user)):
|
||
uid = user["id"]
|
||
with db() as conn:
|
||
rows = conn.execute(
|
||
"SELECT exercise_id, status, updated_at FROM exercise_progress WHERE user_id=?",
|
||
(uid,)
|
||
).fetchall()
|
||
return [dict(r) for r in rows]
|
||
|
||
@router.post("/progress")
|
||
async def upsert_progress(body: ProgressUpdate, user=Depends(get_current_user)):
|
||
uid = user["id"]
|
||
with db() as conn:
|
||
conn.execute("""
|
||
INSERT INTO exercise_progress (user_id, exercise_id, status)
|
||
VALUES (?,?,?)
|
||
ON CONFLICT(user_id, exercise_id) DO UPDATE
|
||
SET status=excluded.status, updated_at=datetime('now')
|
||
""", (uid, body.exercise_id, body.status))
|
||
return {"ok": True}
|
||
|
||
# ------------------------------------------------------------------
|
||
# Trainingsplan-Checkboxen
|
||
# ------------------------------------------------------------------
|
||
class PlanProgress(BaseModel):
|
||
item_key: str
|
||
checked: bool
|
||
|
||
@router.get("/plan-progress")
|
||
async def get_plan_progress(user=Depends(get_current_user)):
|
||
uid = user["id"]
|
||
with db() as conn:
|
||
rows = conn.execute(
|
||
"SELECT item_key, checked FROM training_plan_progress WHERE user_id=?",
|
||
(uid,)
|
||
).fetchall()
|
||
return [dict(r) for r in rows]
|
||
|
||
@router.post("/plan-progress")
|
||
async def upsert_plan_progress(body: PlanProgress, user=Depends(get_current_user)):
|
||
uid = user["id"]
|
||
with db() as conn:
|
||
if body.checked:
|
||
conn.execute("""
|
||
INSERT OR REPLACE INTO training_plan_progress (user_id, item_key, checked)
|
||
VALUES (?,?,1)
|
||
""", (uid, body.item_key))
|
||
else:
|
||
conn.execute(
|
||
"DELETE FROM training_plan_progress WHERE user_id=? AND item_key=?",
|
||
(uid, body.item_key)
|
||
)
|
||
return {"ok": True}
|
||
|
||
# ------------------------------------------------------------------
|
||
# Empfehlungen (rule-based)
|
||
# ------------------------------------------------------------------
|
||
GRUNDKOMMANDOS_ORDER = ['Sitz', 'Platz', 'Bleib', 'Hier / Komm', 'Fuß', 'Aus / Lass es', 'Warte']
|
||
TRICKS_FIRST = ['Pfote / Schütteln', 'Dreh', 'Auf die Decke', 'Nasenarbeit / Suchen']
|
||
|
||
@router.get("/suggestions")
|
||
async def get_suggestions(user=Depends(get_current_user)):
|
||
uid = user["id"]
|
||
with db() as conn:
|
||
rows = conn.execute(
|
||
"SELECT exercise_id, status FROM exercise_progress WHERE user_id=?",
|
||
(uid,)
|
||
).fetchall()
|
||
|
||
progress = {r["exercise_id"]: r["status"] for r in rows}
|
||
|
||
def key(name): return f"grundkommandos_{name.replace(' ', '_').replace('/', '')}"
|
||
def tkey(name): return f"tricks_{name.replace(' ', '_').replace('/', '')}"
|
||
|
||
suggestions = []
|
||
|
||
# Noch-nicht Übungen — direkte Hilfe
|
||
stuck = [n for n in GRUNDKOMMANDOS_ORDER if progress.get(key(n)) == 'noch-nicht']
|
||
if stuck:
|
||
suggestions.append({
|
||
"type": "help",
|
||
"icon": "warning",
|
||
"title": f'\u201e{stuck[0]}\u201c klappt noch nicht',
|
||
"text": "Mach einen Schritt zurück: Kürzere Einheiten, mehr Leckerlis, weniger Ablenkung. Schau dir die Trainingsgrundlagen an.",
|
||
"action_tab": "grundkommandos",
|
||
"action_name": stuck[0],
|
||
})
|
||
|
||
# Manchmal-Übungen — intensivieren
|
||
almost = [n for n in GRUNDKOMMANDOS_ORDER if progress.get(key(n)) == 'manchmal']
|
||
if almost:
|
||
suggestions.append({
|
||
"type": "boost",
|
||
"icon": "fire",
|
||
"title": f'Fast da: \u201e{almost[0]}\u201c',
|
||
"text": "Du bist auf dem richtigen Weg! Übe täglich 3–5 Minuten — dann sitzt es bald.",
|
||
"action_tab": "grundkommandos",
|
||
"action_name": almost[0],
|
||
})
|
||
|
||
# Nächste ungestartete Grundkommando
|
||
grundk_mastered = [n for n in GRUNDKOMMANDOS_ORDER if progress.get(key(n)) == 'sitzt']
|
||
next_up = next((n for n in GRUNDKOMMANDOS_ORDER
|
||
if progress.get(key(n)) in (None, 'noch-nicht') and n not in stuck), None)
|
||
|
||
if len(grundk_mastered) == len(GRUNDKOMMANDOS_ORDER):
|
||
# Alle Grundkommandos gemeistert → Tricks empfehlen
|
||
next_trick = next((n for n in TRICKS_FIRST if progress.get(tkey(n)) is None), None)
|
||
if next_trick:
|
||
suggestions.append({
|
||
"type": "next",
|
||
"icon": "star",
|
||
"title": "Alle Grundkommandos sitzen! 🎉",
|
||
"text": f'Zeit für Tricks! Starte mit \u201e{next_trick}\u201c \u2014 Spaß garantiert.',
|
||
"action_tab": "tricks",
|
||
"action_name": next_trick,
|
||
})
|
||
elif next_up and not almost:
|
||
suggestions.append({
|
||
"type": "next",
|
||
"icon": "arrow-right",
|
||
"title": f"Bereit für den nächsten Schritt?",
|
||
"text": f'Starte jetzt mit \u201e{next_up}\u201c. {"Du hast bereits " + str(len(grundk_mastered)) + " Grundkommandos gemeistert!" if grundk_mastered else "Es ist die perfekte Basis für alles Weitere."}',
|
||
"action_tab": "grundkommandos",
|
||
"action_name": next_up,
|
||
})
|
||
elif not progress:
|
||
# Noch gar kein Fortschritt
|
||
suggestions.append({
|
||
"type": "start",
|
||
"icon": "flag",
|
||
"title": "Womit fangen wir an?",
|
||
"text": "\u201eSitz\u201c ist das erste Kommando für jeden Hund \u2014 einfach, schnell erlernt und die Basis für alles.",
|
||
"action_tab": "grundkommandos",
|
||
"action_name": "Sitz",
|
||
})
|
||
|
||
return suggestions[:2] # max 2 Empfehlungen
|