From 5518064be36f42ecba4aaa484ebd226df51aad2a Mon Sep 17 00:00:00 2001 From: rene Date: Mon, 13 Apr 2026 20:49:49 +0200 Subject: [PATCH] =?UTF-8?q?Feat:=20APScheduler=20=E2=80=94=20t=C3=A4glich?= =?UTF-8?q?=20Push=20f=C3=BCr=20Health-Erinnerungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apscheduler==3.10.4 in requirements.txt - scheduler.py: AsyncIOScheduler, täglich 08:00 Uhr (Europe/Berlin) - Job prüft naechstes IN (heute, in 7 Tagen, gestern): heute → "Heute fällig", 7 Tage → Vorwarnung, gestern → Überfällig - Nur Impfung, Entwurmung, Medikament - misfire_grace_time=3600 (robust nach Container-Neustart) - Scheduler start/stop im FastAPI lifespan --- backend/main.py | 3 ++ backend/requirements.txt | 1 + backend/scheduler.py | 89 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 backend/scheduler.py diff --git a/backend/main.py b/backend/main.py index 9769399..ef48703 100644 --- a/backend/main.py +++ b/backend/main.py @@ -11,6 +11,7 @@ from contextlib import asynccontextmanager from database import init_db import ki +import scheduler as sched logging.basicConfig( level = logging.INFO, @@ -27,7 +28,9 @@ async def lifespan(app: FastAPI): logger.info("Ban Yaro startet...") init_db() logger.info(f"KI-Modus: {ki.KI_MODE}") + sched.start() yield + sched.stop() logger.info("Ban Yaro beendet.") diff --git a/backend/requirements.txt b/backend/requirements.txt index 29e57bc..fbcd007 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,4 @@ httpx==0.27.2 openai==1.50.0 anthropic==0.34.0 pywebpush==2.0.0 +apscheduler==3.10.4 diff --git a/backend/scheduler.py b/backend/scheduler.py new file mode 100644 index 0000000..09c7a85 --- /dev/null +++ b/backend/scheduler.py @@ -0,0 +1,89 @@ +""" +BAN YARO — Hintergrund-Scheduler +Täglich: Gesundheits-Erinnerungen per Push versenden. +""" + +import logging +from datetime import date, timedelta +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.cron import CronTrigger + +from database import db +from routes.push import send_push_to_user + +logger = logging.getLogger(__name__) + +_scheduler = AsyncIOScheduler(timezone="Europe/Berlin") + + +def start(): + _scheduler.add_job( + _job_health_reminders, + CronTrigger(hour=8, minute=0), # täglich 08:00 Uhr + id="health_reminders", + replace_existing=True, + misfire_grace_time=3600, # bis zu 1h Verzug ok (z.B. nach Neustart) + ) + _scheduler.start() + logger.info("Scheduler gestartet — Health-Reminder täglich 08:00 Uhr.") + + +def stop(): + _scheduler.shutdown(wait=False) + logger.info("Scheduler gestoppt.") + + +# ------------------------------------------------------------------ +# JOB: Gesundheits-Erinnerungen +# ------------------------------------------------------------------ +async def _job_health_reminders(): + """ + Findet alle Health-Einträge mit `naechstes`-Datum: + - genau heute → sofortige Erinnerung + - in 7 Tagen → Vorwarnung + - gestern → Überfällig-Erinnerung (nur einmal, 1 Tag nach Fälligkeit) + Schickt jeweils eine Push-Notification an den Hundebesitzer. + """ + today = date.today() + in7 = today + timedelta(days=7) + yesterday = today - timedelta(days=1) + + logger.info(f"Health-Reminder Job läuft für {today}") + + with db() as conn: + # Alle fälligen Einträge der nächsten 7 Tage + gestrige (überfällig) + rows = conn.execute(""" + SELECT h.id, h.typ, h.bezeichnung, h.naechstes, + d.user_id, d.name AS hund_name + FROM health h + JOIN dogs d ON d.id = h.dog_id + WHERE h.naechstes IN (?, ?, ?) + AND h.typ IN ('impfung', 'entwurmung', 'medikament') + """, (str(today), str(in7), str(yesterday))).fetchall() + + sent_total = 0 + for r in rows: + naechstes = date.fromisoformat(r["naechstes"]) + delta = (naechstes - today).days + + if delta == 7: + title = f"⏰ Erinnerung: {r['bezeichnung']}" + body = f"In 7 Tagen fällig für {r['hund_name']}." + elif delta == 0: + title = f"📅 Heute fällig: {r['bezeichnung']}" + body = f"Bitte heute erledigen — {r['hund_name']} wartet." + else: # delta == -1 → gestern überfällig + title = f"⚠️ Überfällig: {r['bezeichnung']}" + body = f"War gestern fällig für {r['hund_name']} — bitte bald erledigen." + + sent = send_push_to_user(r["user_id"], { + "type": "health_reminder", + "title": title, + "body": body, + "data": {"page": "health"}, + }) + sent_total += sent + if sent: + logger.info(f"Reminder Push: user={r['user_id']} entry={r['id']} delta={delta}d") + + logger.info(f"Health-Reminder Job fertig — {len(rows)} Einträge, {sent_total} Push gesendet.")