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
|
|
@ -164,8 +164,24 @@ def start():
|
|||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
# Täglich 09:00 Uhr — Jahrestags-Erinnerungen (Tagebuch-Einträge von heute vor X Jahren)
|
||||
_scheduler.add_job(
|
||||
_job_anniversary_reminders,
|
||||
CronTrigger(hour=9, minute=0),
|
||||
id="anniversary_reminders",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
# 1. des Monats 10:00 — Monatlicher Rückblick per Push
|
||||
_scheduler.add_job(
|
||||
_job_monthly_recap,
|
||||
CronTrigger(day=1, hour=10, minute=0),
|
||||
id="monthly_recap",
|
||||
replace_existing=True,
|
||||
misfire_grace_time=3600,
|
||||
)
|
||||
_scheduler.start()
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00. OSM-Cache: on-demand (kein Prewarm).")
|
||||
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00. OSM-Cache: on-demand (kein Prewarm).")
|
||||
|
||||
|
||||
def stop():
|
||||
|
|
@ -890,6 +906,8 @@ async def _job_status_report():
|
|||
"streak_reminder": "Streak-Erinnerung (täglich 19:00)",
|
||||
"recall_check": "Tierfutter-Rückrufe (RASFF, täglich 08:00)",
|
||||
"golden_gassi_hour": "Goldene Gassi-Stunde (täglich 07:00)",
|
||||
"anniversary_reminders": "Jahrestags-Erinnerungen (täglich 09:00)",
|
||||
"monthly_recap": "Monatlicher Rückblick (1. des Monats 10:00)",
|
||||
}
|
||||
job_rows_html = ""
|
||||
job_rows_txt = ""
|
||||
|
|
@ -1383,6 +1401,149 @@ async def _job_golden_gassi_hour():
|
|||
_log_job("golden_gassi_hour", "ok", f"{sent_total} Push an {len(users)} User")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Jahrestags-Erinnerungen (täglich 09:00)
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_anniversary_reminders():
|
||||
"""Prüft ob heute ein Jahrestag für diary-Einträge vorliegt und sendet Push."""
|
||||
today = datetime.now(tz=_TZ)
|
||||
today_md = today.strftime('%m-%d') # Monat-Tag ohne Jahr
|
||||
|
||||
logger.info(f"Jahrestags-Erinnerungen Job läuft für {today_md}")
|
||||
|
||||
with db() as conn:
|
||||
entries = conn.execute("""
|
||||
SELECT d.id, d.titel, d.datum, d.user_id, d.dog_id,
|
||||
(SELECT dm.url FROM diary_media dm
|
||||
WHERE dm.diary_id=d.id LIMIT 1) AS foto_url
|
||||
FROM diary d
|
||||
WHERE strftime('%m-%d', d.datum) = ?
|
||||
AND d.datum < date('now')
|
||||
AND d.titel IS NOT NULL
|
||||
AND d.is_milestone = 0
|
||||
""", (today_md,)).fetchall()
|
||||
|
||||
sent_total = 0
|
||||
for e in entries:
|
||||
try:
|
||||
jahre = today.year - int(e['datum'][:4])
|
||||
if jahre < 1:
|
||||
continue
|
||||
jahre_label = f"{jahre} Jahr" if jahre == 1 else f"{jahre} Jahren"
|
||||
send_push_to_user(e['user_id'], {
|
||||
'type': 'anniversary_reminder',
|
||||
'title': f'📅 Vor {jahre_label}: {(e["titel"] or "")[:40]}',
|
||||
'body': 'Erinnerung an diesen besonderen Tag mit deinem Hund',
|
||||
'data': {'page': 'diary'},
|
||||
'tag': f'anniversary-{e["id"]}-{today.year}',
|
||||
})
|
||||
sent_total += 1
|
||||
except Exception as ex:
|
||||
logger.warning(f"Jahrestag-Reminder: Fehler für Eintrag {e['id']}: {ex}")
|
||||
|
||||
logger.info(f"Jahrestags-Erinnerungen Job fertig — {len(entries)} Einträge geprüft, {sent_total} Push gesendet.")
|
||||
_log_job("anniversary_reminders", "ok", f"{sent_total} Push von {len(entries)} Einträgen")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# JOB: Monatlicher Rückblick (1. des Monats 10:00)
|
||||
# ------------------------------------------------------------------
|
||||
async def _job_monthly_recap():
|
||||
"""Sendet jedem User am 1. des Monats einen Rückblick des Vormonats."""
|
||||
today = datetime.now(tz=_TZ)
|
||||
first_this = today.replace(day=1)
|
||||
last_month_end = first_this - timedelta(days=1)
|
||||
last_month_start = last_month_end.replace(day=1)
|
||||
year_str = last_month_start.strftime('%Y')
|
||||
month_str = last_month_start.strftime('%m')
|
||||
month_label = last_month_start.strftime('%B %Y')
|
||||
|
||||
logger.info(f"Monatlicher Rückblick Job läuft für {month_label}")
|
||||
|
||||
with db() as conn:
|
||||
# Alle User mit mindestens einem Hund
|
||||
users = conn.execute(
|
||||
"SELECT DISTINCT user_id FROM dogs"
|
||||
).fetchall()
|
||||
|
||||
sent_total = 0
|
||||
for u in users:
|
||||
user_id = u["user_id"]
|
||||
try:
|
||||
with db() as conn:
|
||||
# Hunde des Users
|
||||
dog_rows = conn.execute(
|
||||
"SELECT id, name FROM dogs WHERE user_id=?", (user_id,)
|
||||
).fetchall()
|
||||
if not dog_rows:
|
||||
continue
|
||||
|
||||
dog_ids = [d["id"] for d in dog_rows]
|
||||
placeholders = ','.join('?' * len(dog_ids))
|
||||
|
||||
# km (Routen des Users im Vormonat)
|
||||
km_row = conn.execute(
|
||||
"SELECT ROUND(COALESCE(SUM(distanz_km),0),1) AS km FROM routes "
|
||||
"WHERE user_id=? AND strftime('%Y',created_at)=? AND strftime('%m',created_at)=?",
|
||||
(user_id, year_str, month_str)
|
||||
).fetchone()
|
||||
gesamt_km = km_row["km"] or 0.0
|
||||
|
||||
# Tagebucheinträge
|
||||
eintraege = conn.execute(
|
||||
f"SELECT COUNT(*) AS n FROM diary "
|
||||
f"WHERE dog_id IN ({placeholders}) AND strftime('%Y',datum)=? AND strftime('%m',datum)=?",
|
||||
(*dog_ids, year_str, month_str)
|
||||
).fetchone()["n"]
|
||||
|
||||
# Training-Sessions
|
||||
training = conn.execute(
|
||||
f"SELECT COUNT(*) AS n FROM training_sessions "
|
||||
f"WHERE dog_id IN ({placeholders}) AND strftime('%Y',created_at)=? AND strftime('%m',created_at)=?",
|
||||
(*dog_ids, year_str, month_str)
|
||||
).fetchone()["n"]
|
||||
|
||||
# Lieblingsfoto (erstes Foto im Vormonat)
|
||||
foto_row = conn.execute(
|
||||
f"SELECT dm.url FROM diary_media dm "
|
||||
f"JOIN diary d ON d.id=dm.diary_id "
|
||||
f"WHERE d.dog_id IN ({placeholders}) AND dm.media_type='image' "
|
||||
f"AND strftime('%Y',d.datum)=? AND strftime('%m',d.datum)=? "
|
||||
f"ORDER BY d.datum ASC LIMIT 1",
|
||||
(*dog_ids, year_str, month_str)
|
||||
).fetchone()
|
||||
foto_url = foto_row["url"] if foto_row else None
|
||||
|
||||
# Nur senden wenn mindestens eine Aktivität vorhanden
|
||||
if eintraege == 0 and training == 0 and gesamt_km == 0:
|
||||
continue
|
||||
|
||||
dog_name = dog_rows[0]["name"]
|
||||
parts = []
|
||||
if gesamt_km > 0:
|
||||
parts.append(f"{gesamt_km} km gelaufen")
|
||||
if eintraege > 0:
|
||||
parts.append(f"{eintraege} Tagebucheintr{'ä' if True else 'a'}ge")
|
||||
if training > 0:
|
||||
parts.append(f"{training} Training-Sessions")
|
||||
|
||||
body_text = " · ".join(parts)
|
||||
|
||||
send_push_to_user(user_id, {
|
||||
'type': 'monthly_recap',
|
||||
'title': f'📅 {month_label}: Rückblick für {dog_name}',
|
||||
'body': body_text,
|
||||
'data': {'page': 'diary'},
|
||||
'tag': f'monthly-recap-{year_str}-{month_str}',
|
||||
})
|
||||
sent_total += 1
|
||||
except Exception as ex:
|
||||
logger.error(f"Monatlicher Rückblick: Fehler für user {user_id}: {ex}")
|
||||
|
||||
logger.info(f"Monatlicher Rückblick Job fertig — {len(users)} User geprüft, {sent_total} Push gesendet.")
|
||||
_log_job("monthly_recap", "ok", f"{sent_total} Push für {month_label}")
|
||||
|
||||
|
||||
async def _fetch_hourly_weather(lat: float, lon: float) -> list[dict]:
|
||||
"""Holt stündliche Wetterdaten für heute von Open-Meteo."""
|
||||
import httpx
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue