Feature: Sprint31 — 9 Features merged (Streak, Ausgaben, KI-Tierarzt, Rückrufe, Adoption, Vet+Befunde, Hundepass, Playdate, Rassenerkennung)

- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
This commit is contained in:
rene 2026-05-02 09:29:48 +02:00
parent 031c6028ac
commit 742ad189e8
26 changed files with 5734 additions and 27 deletions

View file

@ -132,8 +132,24 @@ def start():
replace_existing=True,
misfire_grace_time=3600,
)
# Täglich 19:00 Uhr — Streak-Erinnerung
_scheduler.add_job(
_job_streak_reminder,
CronTrigger(hour=19, minute=0),
id="streak_reminder",
replace_existing=True,
misfire_grace_time=3600,
)
# Täglich 08:00 Uhr — Tierfutter-Rückrufe prüfen (RASFF)
_scheduler.add_job(
_job_recall_check,
CronTrigger(hour=8, minute=0),
id="recall_check",
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. 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. OSM-Cache: on-demand (kein Prewarm).")
def stop():
@ -855,6 +871,8 @@ async def _job_status_report():
"weekly_praise": "Wöchentlicher Lober (Mo 09:00)",
"ki_health_report": "KI-Gesundheitsberichte",
"quarterly_report": "Quartalsbericht (1. Feb/Mai/Aug/Nov)",
"streak_reminder": "Streak-Erinnerung (täglich 19:00)",
"recall_check": "Tierfutter-Rückrufe (RASFF, täglich 08:00)",
}
job_rows_html = ""
job_rows_txt = ""
@ -1172,3 +1190,79 @@ async def _job_hdm_winner():
logger.info(f"HdM-Winner {monat}: Hund {winner['dog_id']} ('{winner['name']}', {winner['stimmen']} Stimmen) eingetragen.")
_log_job("hdm_winner", "ok", f"{monat}: {winner['name']} ({winner['stimmen']} Stimmen)")
# ------------------------------------------------------------------
# JOB: Streak-Erinnerung (täglich 19:00)
# ------------------------------------------------------------------
async def _job_streak_reminder():
"""
Findet alle User die heute noch nicht trainiert haben (last_training_date < heute)
und deren current_streak > 0. Sendet einen motivierenden Push pro Hund.
"""
today = str(date.today())
logger.info(f"Streak-Reminder Job läuft für {today}")
with db() as conn:
rows = conn.execute("""
SELECT ts.user_id, ts.dog_id, ts.current_streak, d.name AS dog_name
FROM training_streaks ts
JOIN dogs d ON d.id = ts.dog_id
WHERE ts.current_streak > 0
AND (ts.last_training_date IS NULL OR ts.last_training_date < ?)
""", (today,)).fetchall()
sent_total = 0
for r in rows:
n = r["current_streak"]
sent = send_push_to_user(r["user_id"], {
"type": "streak_reminder",
"title": f"🔥 {r['dog_name']} wartet auf sein Training!",
"body": f"Streak: {n} {'Tag' if n == 1 else 'Tage'} — nicht jetzt aufhören.",
"data": {"page": "uebungen"},
"tag": f"streak-{r['dog_id']}-{today}",
})
sent_total += sent
logger.info(f"Streak-Reminder Job fertig — {len(rows)} Hunde geprüft, {sent_total} Push gesendet.")
_log_job("streak_reminder", "ok", f"{sent_total} Push an {len(rows)} Hunde")
# ------------------------------------------------------------------
# JOB: Tierfutter-Rückrufe prüfen (RASFF, täglich 08:00)
# ------------------------------------------------------------------
async def _job_recall_check():
"""
Fragt täglich die RASFF EU-API nach neuen Tierfutter-Rückrufen ab.
Neue Einträge werden in DB gespeichert, für jeden wird ein Push
an alle abonnierten User gesendet.
"""
logger.info("Rückruf-Check Job läuft")
try:
from routes.recalls import fetch_rasff_recalls, save_new_recalls
entries = await fetch_rasff_recalls()
if not entries:
logger.info("Rückruf-Check: Keine Einträge von RASFF erhalten (API-Fehler oder leer).")
_log_job("recall_check", "ok", "0 neue Rückrufe (API leer)")
return
new_entries = save_new_recalls(entries)
logger.info(f"Rückruf-Check: {len(new_entries)} neue von {len(entries)} geprüften Einträgen.")
for entry in new_entries:
produkt = entry.get("produkt") or entry.get("titel") or "Unbekanntes Produkt"
gefahr = entry.get("gefahr") or "Bitte Produktdetails prüfen"
ext_id = entry["external_id"]
body = f"{produkt}{gefahr[:80]}"
send_push_to_all({
"title": "⚠️ Tierfutter-Rückruf",
"body": body,
"data": {"page": "recalls"},
"tag": f"recall-{ext_id}",
})
logger.info(f"Rückruf-Push gesendet: {ext_id}{produkt}")
_log_job("recall_check", "ok", f"{len(new_entries)} neue Rückrufe")
except Exception as e:
logger.error(f"Rückruf-Check: unerwarteter Fehler: {e}")
_log_job("recall_check", "error", str(e))