Feature: Trauer-Feature, Futter-Verträglichkeit, Multi-Hund-Fixes, Wetter-Ort (Sprint 47)
- dog-profile.js: Verstorben-Button, Gedenkseite, KI-Abschiedstext - database.py: futter_eintraege/reaktionen, route_dogs, exercise_progress.dog_id - routes/ernaehrung.py: Futter-Verträglichkeit mit 20 Reaktionstypen + Analyse - routes/routen.py: route_dogs Many-to-Many, Routen editierbar - routes/training.py: exercise_progress per dog_id - routes/ki.py: /ki/abschied Trauer-KI - weather.py: Nominatim Ortsname parallel geladen - ui.js: dogChip/bindDogChip, visualViewport-Modal - api.js: gedenken, gedenkseite, futter-Methoden, route_dogs - worlds.js: Ortsname im Wetter-Chip - uebungen.js: _progressLoaded-Flag, dog-spezifischer Fortschritt - trainingsplaene.js: dog_id Unterstützung - diary.js/health.js: P-Badge Cleanup - map.js: Wetter-Ort-Anzeige entfernt - wetter.js: Ort in Wetter-Detail
This commit is contained in:
parent
1ce802c8dc
commit
bda61a0e40
16 changed files with 713 additions and 181 deletions
|
|
@ -361,3 +361,65 @@ Falls kein Hund erkennbar: ist_hund=false und leeres rassen-Array."""
|
|||
"hinweis": parsed.get("hinweis") or None,
|
||||
"verbleibende_anfragen": remaining_after,
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# POST /ki/abschied — Persönlicher Abschiedstext für verstorbenen Hund
|
||||
# ------------------------------------------------------------------
|
||||
class AbschiedRequest(BaseModel):
|
||||
dog_id: int
|
||||
name: str
|
||||
rasse: Optional[str] = None
|
||||
km_total: Optional[float] = None
|
||||
diary_count: Optional[int] = None
|
||||
gemeinsam_tage: Optional[int] = None
|
||||
last_entry_titel: Optional[str] = None
|
||||
|
||||
@router.post("/abschied")
|
||||
async def ki_abschied(req: AbschiedRequest, request: Request,
|
||||
user=Depends(get_current_user)):
|
||||
"""Persönlicher Abschiedstext — einmalig generiert, DB-gecacht."""
|
||||
with db() as conn:
|
||||
cached = conn.execute(
|
||||
"SELECT content FROM bday_ki_cache WHERE dog_id=? AND year=9999 AND mode='abschied'",
|
||||
(req.dog_id,)
|
||||
).fetchone()
|
||||
if cached:
|
||||
return {"text": cached["content"], "cached": True}
|
||||
|
||||
name = req.name.strip()[:40]
|
||||
rasse = req.rasse or ""
|
||||
km = f"{req.km_total:.0f} km" if req.km_total else None
|
||||
tage = f"{req.gemeinsam_tage} gemeinsame Tage" if req.gemeinsam_tage else None
|
||||
eintr = f"{req.diary_count} Tagebucheinträge" if req.diary_count else None
|
||||
|
||||
stats_str = ", ".join(filter(None, [km, tage, eintr]))
|
||||
rasse_str = f" ({rasse})" if rasse else ""
|
||||
|
||||
system = (
|
||||
"Du bist ein einfühlsamer Begleiter für Menschen in Trauer um ihren Hund. "
|
||||
"Schreibe warmherzig, persönlich und respektvoll auf Deutsch. "
|
||||
"Keine Floskeln, kein Kitsch — echte Wärme. "
|
||||
"Erwähne die Statistiken natürlich eingebunden."
|
||||
)
|
||||
prompt = (
|
||||
f"{name}{rasse_str} ist über die Regenbogenbrücke gegangen. "
|
||||
f"Schreibe einen kurzen, persönlichen Abschiedstext (ca. 80–100 Wörter) "
|
||||
f"der die Verbundenheit würdigt. "
|
||||
f"Statistiken: {stats_str or 'nicht bekannt'}. "
|
||||
f"Sei warm, nicht sentimental überladen. Schließe mit einem hoffnungsvollen Gedanken."
|
||||
)
|
||||
|
||||
try:
|
||||
text = await ki_module.complete(
|
||||
system=system, prompt=prompt, max_tokens=300,
|
||||
requires_premium=False, user_id=user["id"],
|
||||
)
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO bday_ki_cache (dog_id, year, mode, content) VALUES (?,9999,'abschied',?)",
|
||||
(req.dog_id, text)
|
||||
)
|
||||
return {"text": text, "cached": False}
|
||||
except Exception as e:
|
||||
raise HTTPException(503, str(e))
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ class RouteCreate(BaseModel):
|
|||
is_public: Optional[bool] = False
|
||||
hunde_tauglichkeit: Optional[str] = None # eingeschränkt | gut | sehr_gut | premium
|
||||
client_time: Optional[str] = None
|
||||
dog_ids: Optional[List[int]] = None # Welche Hunde mitgegangen sind
|
||||
|
||||
class RouteUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
|
@ -69,6 +70,9 @@ class RouteUpdate(BaseModel):
|
|||
is_public: Optional[bool] = None
|
||||
hunde_tauglichkeit: Optional[str] = None
|
||||
|
||||
class RouteDogs(BaseModel):
|
||||
dog_ids: List[int]
|
||||
|
||||
|
||||
def _simplify_track(track: list, max_pts: int = 40) -> list:
|
||||
"""Reduziert GPS-Track auf max_pts Punkte für Vorschau."""
|
||||
|
|
@ -168,7 +172,26 @@ async def create_route(data: RouteCreate, user=Depends(get_current_user)):
|
|||
int(data.is_public) if data.is_public is not None else 1,
|
||||
data.hunde_tauglichkeit, is_valid, ct,
|
||||
))
|
||||
row = conn.execute("SELECT * FROM routes WHERE id = ?", (cur.lastrowid,)).fetchone()
|
||||
route_id = cur.lastrowid
|
||||
row = conn.execute("SELECT * FROM routes WHERE id = ?", (route_id,)).fetchone()
|
||||
|
||||
# Hunde zuordnen — entweder explizit oder alle Hunde des Users
|
||||
dog_ids = data.dog_ids or []
|
||||
if not dog_ids:
|
||||
# Fallback: alle Hunde des Users
|
||||
all_dogs = conn.execute(
|
||||
"SELECT id FROM dogs WHERE user_id=?", (user['id'],)
|
||||
).fetchall()
|
||||
dog_ids = [d['id'] for d in all_dogs]
|
||||
for did in dog_ids:
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO route_dogs (route_id, dog_id) VALUES (?,?)",
|
||||
(route_id, did)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
update_streak(user['id'], conn)
|
||||
check_and_award(user['id'], conn)
|
||||
result = _parse(row)
|
||||
|
|
@ -317,9 +340,14 @@ async def get_route(route_id: int):
|
|||
"SELECT r.*, u.name AS user_name FROM routes r LEFT JOIN users u ON u.id = r.user_id WHERE r.id = ?",
|
||||
(route_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Route nicht gefunden.")
|
||||
return _parse(row)
|
||||
if not row:
|
||||
raise HTTPException(404, "Route nicht gefunden.")
|
||||
dog_rows = conn.execute(
|
||||
"SELECT dog_id FROM route_dogs WHERE route_id = ?", (route_id,)
|
||||
).fetchall()
|
||||
result = _parse(row)
|
||||
result['dog_ids'] = [r['dog_id'] for r in dog_rows]
|
||||
return result
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -346,6 +374,26 @@ async def update_route(route_id: int, data: RouteUpdate, user=Depends(get_curren
|
|||
return _parse(row)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PATCH /api/routes/{id}/dogs — Hunde der Route aktualisieren
|
||||
# ------------------------------------------------------------------
|
||||
@router.patch("/{route_id}/dogs")
|
||||
async def update_route_dogs(route_id: int, data: RouteDogs, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
row = conn.execute("SELECT user_id FROM routes WHERE id = ?", (route_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Route nicht gefunden.")
|
||||
if row['user_id'] != user['id']:
|
||||
raise HTTPException(403, "Nicht berechtigt.")
|
||||
conn.execute("DELETE FROM route_dogs WHERE route_id = ?", (route_id,))
|
||||
for did in data.dog_ids:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO route_dogs (route_id, dog_id) VALUES (?, ?)",
|
||||
(route_id, did)
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PATCH /api/routes/{id}/trim — Route kürzen (Datenschutz)
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -85,28 +85,43 @@ async def update_exercise(exercise_id: int, body: ExerciseUpdate, _=Depends(requ
|
|||
# ------------------------------------------------------------------
|
||||
class ProgressUpdate(BaseModel):
|
||||
exercise_id: str
|
||||
status: Optional[str] = None # null/noch-nicht/manchmal/meistens/sitzt
|
||||
status: Optional[str] = None
|
||||
dog_id: Optional[int] = None
|
||||
|
||||
@router.get("/progress")
|
||||
async def get_progress(user=Depends(get_current_user)):
|
||||
async def get_progress(dog_id: Optional[int] = None, 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()
|
||||
if dog_id:
|
||||
rows = conn.execute(
|
||||
"SELECT exercise_id, status, updated_at FROM exercise_progress WHERE dog_id=?",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
else:
|
||||
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))
|
||||
if body.dog_id:
|
||||
conn.execute("""
|
||||
INSERT INTO exercise_progress (user_id, dog_id, exercise_id, status)
|
||||
VALUES (?,?,?,?)
|
||||
ON CONFLICT(dog_id, exercise_id) DO UPDATE
|
||||
SET status=excluded.status, updated_at=datetime('now')
|
||||
""", (uid, body.dog_id, body.exercise_id, body.status))
|
||||
else:
|
||||
conn.execute("""
|
||||
INSERT INTO exercise_progress (user_id, exercise_id, status)
|
||||
VALUES (?,?,?)
|
||||
ON CONFLICT(dog_id, exercise_id) DO UPDATE
|
||||
SET status=excluded.status, updated_at=datetime('now')
|
||||
""", (uid, body.exercise_id, body.status))
|
||||
return {"ok": True}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -115,15 +130,22 @@ async def upsert_progress(body: ProgressUpdate, user=Depends(get_current_user)):
|
|||
class PlanProgress(BaseModel):
|
||||
item_key: str
|
||||
checked: bool
|
||||
dog_id: Optional[int] = None
|
||||
|
||||
@router.get("/plan-progress")
|
||||
async def get_plan_progress(user=Depends(get_current_user)):
|
||||
async def get_plan_progress(dog_id: Optional[int] = None, 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()
|
||||
if dog_id:
|
||||
rows = conn.execute(
|
||||
"SELECT item_key, checked FROM training_plan_progress WHERE dog_id=?",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
else:
|
||||
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")
|
||||
|
|
@ -132,13 +154,13 @@ async def upsert_plan_progress(body: PlanProgress, user=Depends(get_current_user
|
|||
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))
|
||||
INSERT OR REPLACE INTO training_plan_progress (user_id, dog_id, item_key, checked)
|
||||
VALUES (?,?,?,1)
|
||||
""", (uid, body.dog_id, body.item_key))
|
||||
else:
|
||||
conn.execute(
|
||||
"DELETE FROM training_plan_progress WHERE user_id=? AND item_key=?",
|
||||
(uid, body.item_key)
|
||||
"DELETE FROM training_plan_progress WHERE dog_id=? AND item_key=?",
|
||||
(body.dog_id, body.item_key)
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
|
@ -149,13 +171,19 @@ GRUNDKOMMANDOS_ORDER = ['Sitz', 'Platz', 'Bleib', 'Hier / Komm', 'Fuß', 'Aus /
|
|||
TRICKS_FIRST = ['Pfote / Schütteln', 'Dreh', 'Auf die Decke', 'Nasenarbeit / Suchen']
|
||||
|
||||
@router.get("/suggestions")
|
||||
async def get_suggestions(user=Depends(get_current_user)):
|
||||
async def get_suggestions(dog_id: Optional[int] = None, 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()
|
||||
if dog_id:
|
||||
rows = conn.execute(
|
||||
"SELECT exercise_id, status FROM exercise_progress WHERE dog_id=?",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
else:
|
||||
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}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue