Feature: Ban Yaro Wrapped + Jahrestags- und Monatsrückblick (SW by-v699)
- GET /api/dogs/{id}/wrapped?year= aggregiert km, Gassi-Tage, Fotos,
Lieblingsmonat/-aktivität, Training, Gesundheit, Wetter-Stats aus SQLite
- Frontend: Wrapped-Fullscreen-Modal in dog-profile.js — 5 Cards mit
Swipe/Klick-Navigation, Dots, ESC-Taste, Copy-to-Clipboard auf Share-Card
- Scheduler: _job_anniversary_reminders (täglich 09:00) sendet Push wenn
heute ein Tagebucheintrag von vor 1+ Jahren existiert
- Scheduler: _job_monthly_recap (1. des Monats 10:00) sendet Vormonat-
Zusammenfassung (km, Einträge, Training) per Push an alle User
- Beide Jobs im Status-Report-Log und Scheduler-Start-Log vermerkt
- SW by-v699, APP_VER 699
This commit is contained in:
parent
0fdc32eaf4
commit
20a4936397
3 changed files with 464 additions and 4 deletions
|
|
@ -181,18 +181,29 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
|||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
# Zufälliges Foto aus den letzten 100 Tagebuchbildern
|
||||
# Alle Querformat-Fotos (breiter als hoch) des Hundes, stabile Reihenfolge
|
||||
photos = conn.execute(
|
||||
"""SELECT dm.url FROM diary_media dm
|
||||
JOIN diary d ON d.id = dm.diary_id
|
||||
WHERE d.dog_id=? AND dm.media_type='image'
|
||||
ORDER BY d.datum DESC LIMIT 100""",
|
||||
AND dm.img_width IS NOT NULL AND dm.img_width > dm.img_height
|
||||
ORDER BY d.datum DESC, d.id DESC, dm.id ASC""",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
# Fallback: alle Fotos ohne Maß-Filter (Bilder vor dem Backfill)
|
||||
if not photos:
|
||||
photos = conn.execute(
|
||||
"""SELECT dm.url FROM diary_media dm
|
||||
JOIN diary d ON d.id = dm.diary_id
|
||||
WHERE d.dog_id=? AND dm.media_type='image'
|
||||
ORDER BY d.datum DESC, d.id DESC, dm.id ASC""",
|
||||
(dog_id,)
|
||||
).fetchall()
|
||||
random_photo = None
|
||||
if photos:
|
||||
import datetime as _dt2
|
||||
day_num = (_dt2.date.today() - _dt2.date(2024, 1, 1)).days
|
||||
chosen_url = photos[day_num % len(photos)]["url"]
|
||||
tick = (_dt2.date.today() - _dt2.date(2024, 1, 1)).days
|
||||
chosen_url = photos[tick % len(photos)]["url"]
|
||||
random_photo = {
|
||||
"url": chosen_url,
|
||||
"preview_url": preview_url_from(chosen_url),
|
||||
|
|
@ -294,6 +305,137 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
|||
}
|
||||
|
||||
|
||||
@router.get("/{dog_id}/wrapped")
|
||||
async def get_dog_wrapped(dog_id: int, year: int = None, user=Depends(get_current_user)):
|
||||
"""Jahresrückblick ('Wrapped') für einen Hund."""
|
||||
import json as _json
|
||||
from datetime import date as _date
|
||||
|
||||
if year is None:
|
||||
year = _date.today().year
|
||||
|
||||
with db() as conn:
|
||||
dog = conn.execute(
|
||||
"SELECT id, name, user_id FROM dogs WHERE id=? AND user_id=?",
|
||||
(dog_id, user["id"])
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
|
||||
# km gelaufen (eigene Routen des Users)
|
||||
gesamt_km_row = conn.execute(
|
||||
"SELECT ROUND(COALESCE(SUM(distanz_km),0),1) AS km FROM routes "
|
||||
"WHERE user_id=? AND strftime('%Y', created_at)=?",
|
||||
(user["id"], str(year))
|
||||
).fetchone()
|
||||
gesamt_km = gesamt_km_row["km"] or 0.0
|
||||
|
||||
# Gassi-Tage (Distinct Datum in Diary)
|
||||
gassi_tage = conn.execute(
|
||||
"SELECT COUNT(DISTINCT datum) AS n FROM diary "
|
||||
"WHERE dog_id=? AND strftime('%Y', datum)=?",
|
||||
(dog_id, str(year))
|
||||
).fetchone()["n"]
|
||||
|
||||
# Gesamte Einträge
|
||||
eintraege_gesamt = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM diary "
|
||||
"WHERE dog_id=? AND strftime('%Y', datum)=?",
|
||||
(dog_id, str(year))
|
||||
).fetchone()["n"]
|
||||
|
||||
# Fotos gesamt
|
||||
fotos_gesamt = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM diary_media dm "
|
||||
"JOIN diary d ON d.id=dm.diary_id "
|
||||
"WHERE d.dog_id=? AND strftime('%Y', d.datum)=? AND dm.media_type='image'",
|
||||
(dog_id, str(year))
|
||||
).fetchone()["n"]
|
||||
|
||||
# Beste Route (längste distanz)
|
||||
beste_route_row = conn.execute(
|
||||
"SELECT MAX(distanz_km) AS km FROM routes "
|
||||
"WHERE user_id=? AND strftime('%Y', created_at)=?",
|
||||
(user["id"], str(year))
|
||||
).fetchone()
|
||||
beste_route = beste_route_row["km"] or 0.0
|
||||
|
||||
# Lieblingsmonat (meiste diary-Einträge)
|
||||
monat_rows = conn.execute(
|
||||
"SELECT strftime('%m', datum) AS monat, COUNT(*) AS n FROM diary "
|
||||
"WHERE dog_id=? AND strftime('%Y', datum)=? "
|
||||
"GROUP BY monat ORDER BY n DESC LIMIT 1",
|
||||
(dog_id, str(year))
|
||||
).fetchone()
|
||||
lieblings_monat = None
|
||||
if monat_rows:
|
||||
_MONATE = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez']
|
||||
try:
|
||||
lieblings_monat = _MONATE[int(monat_rows["monat"]) - 1]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Lieblingsaktivität (häufigster typ)
|
||||
typ_row = conn.execute(
|
||||
"SELECT typ, COUNT(*) AS n FROM diary "
|
||||
"WHERE dog_id=? AND strftime('%Y', datum)=? "
|
||||
"GROUP BY typ ORDER BY n DESC LIMIT 1",
|
||||
(dog_id, str(year))
|
||||
).fetchone()
|
||||
lieblings_aktivitaet = typ_row["typ"] if typ_row else None
|
||||
|
||||
# Training-Sessions
|
||||
training_sessions = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM training_sessions "
|
||||
"WHERE dog_id=? AND strftime('%Y', created_at)=?",
|
||||
(dog_id, str(year))
|
||||
).fetchone()["n"]
|
||||
|
||||
# Gesundheits-Einträge
|
||||
gesundheit_eintraege = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM health "
|
||||
"WHERE dog_id=? AND strftime('%Y', datum)=?",
|
||||
(dog_id, str(year))
|
||||
).fetchone()["n"]
|
||||
|
||||
# Wetter-Tapferkeit: Tagebuch-Einträge mit weather_json
|
||||
wetter_kalt = 0
|
||||
wetter_warm = 0
|
||||
wetter_rows = conn.execute(
|
||||
"SELECT weather_json FROM diary "
|
||||
"WHERE dog_id=? AND strftime('%Y', datum)=? AND weather_json IS NOT NULL",
|
||||
(dog_id, str(year))
|
||||
).fetchall()
|
||||
for wr in wetter_rows:
|
||||
try:
|
||||
wj = _json.loads(wr["weather_json"])
|
||||
temp = wj.get("temp_c") or wj.get("temperature") or wj.get("temp")
|
||||
if temp is not None:
|
||||
if float(temp) < 5:
|
||||
wetter_kalt += 1
|
||||
elif float(temp) > 25:
|
||||
wetter_warm += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"dog_id": dog_id,
|
||||
"dog_name": dog["name"],
|
||||
"year": year,
|
||||
"gesamt_km": gesamt_km,
|
||||
"gassi_tage": gassi_tage,
|
||||
"eintraege_gesamt": eintraege_gesamt,
|
||||
"fotos_gesamt": fotos_gesamt,
|
||||
"beste_route": beste_route,
|
||||
"lieblings_monat": lieblings_monat,
|
||||
"lieblings_aktivitaet": lieblings_aktivitaet,
|
||||
"training_sessions": training_sessions,
|
||||
"gesundheit_eintraege": gesundheit_eintraege,
|
||||
"wetter_kalt": wetter_kalt,
|
||||
"wetter_warm": wetter_warm,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{dog_id}")
|
||||
async def get_dog(dog_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue