Karte (map.js):
- OSM Overpass API: Restaurants, Tierärzte, Parkplätze, Bänke, Wasserstellen
- Leaflet.markercluster für alle OSM-Layer
- Standort-Dot mit GPS-Genauigkeitskreis, Wake-Lock bei Aufzeichnung
- Community-Pins setzen/löschen, Meldungen, Crosshair-Placement
- Layer-Sichtbarkeit in localStorage (by_map_visible_v1)
Routen (routes.js + routen.py):
- Komoot-Stil: SVG-Track-Preview, Foto-Upload, Nearby-POIs im Detail-Modal
- Neue Felder: is_public, hunde_tauglichkeit, foto_urls
- Rate-Endpoint (POST /api/routes/{id}/rate)
- Foto-Upload (POST /api/routes/{id}/photo)
- Fix: json_extract $[-1] → $[#-1] (SQLite-kompatibler Pfad für letztes Element)
Backend (osm.py, database.py, scheduler.py):
- /api/osm/pois: OSM-Overpass-Cache mit Tile-Logik (14 Tage TTL)
- /api/osm/user-poi: Community-Marker CRUD
- /api/osm/report: Marker als ungültig melden
- Neue Tabellen: osm_pois, osm_tiles, user_map_pois, osm_reports
- Giftköder-Archiv-Job (täglich 03:00, soft-delete nach Ablauf)
- Giftköder-Archiv-Job als APScheduler-CronJob
UI: Orte-Menüpunkt entfernt (in Karte integriert), APP_VER auf 62
124 lines
4.4 KiB
Python
124 lines
4.4 KiB
Python
"""
|
|
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,
|
|
)
|
|
_scheduler.add_job(
|
|
_job_poison_archive,
|
|
CronTrigger(hour=3, minute=0), # täglich 03:00 Uhr (ruhige Zeit)
|
|
id="poison_archive",
|
|
replace_existing=True,
|
|
misfire_grace_time=3600,
|
|
)
|
|
_scheduler.start()
|
|
logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00.")
|
|
|
|
|
|
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.")
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# JOB: Abgelaufene Giftköder-Meldungen archivieren
|
|
# Abgelaufene, aber noch nicht manuell aufgelöste Einträge werden
|
|
# sauber als geloest=1 markiert — für spätere KI-Musteranalyse.
|
|
# Die Zeilen selbst werden NIE gelöscht.
|
|
# ------------------------------------------------------------------
|
|
async def _job_poison_archive():
|
|
"""
|
|
Findet Giftköder-Meldungen deren expires_at verstrichen ist
|
|
und die noch nicht als geloest markiert wurden.
|
|
Setzt geloest=1, geloest_grund='automatisch_abgelaufen'.
|
|
"""
|
|
from datetime import datetime
|
|
now = datetime.utcnow().isoformat()
|
|
with db() as conn:
|
|
result = conn.execute("""
|
|
UPDATE poison
|
|
SET geloest = 1,
|
|
geloest_at = datetime('now'),
|
|
geloest_grund = 'automatisch_abgelaufen'
|
|
WHERE geloest = 0
|
|
AND expires_at < ?
|
|
""", (now,))
|
|
count = result.rowcount
|
|
if count:
|
|
logger.info(f"Giftköder-Archiv: {count} abgelaufene Meldungen archiviert.")
|