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:
parent
031c6028ac
commit
742ad189e8
26 changed files with 5734 additions and 27 deletions
138
backend/routes/recalls.py
Normal file
138
backend/routes/recalls.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""BAN YARO — Rückruf-Alarm (Tierfutter)
|
||||
RASFF EU Rapid Alert System for Food and Feed
|
||||
"""
|
||||
|
||||
import logging
|
||||
import httpx
|
||||
from fastapi import APIRouter
|
||||
from database import db
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RASFF_URL = "https://webgate.ec.europa.eu/rasff-window/backend/public/notification/list/with-filters"
|
||||
RASFF_PARAMS = {
|
||||
"filters": '{"subject.product_category":["pet food and animal feed"]}',
|
||||
"pageNumber": 0,
|
||||
"pageSize": 20,
|
||||
"sortColumn": "notificationDate",
|
||||
"sortDirection": "DESC",
|
||||
}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/recalls — Letzte 50 Rückrufe
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("")
|
||||
async def list_recalls(q: str = ""):
|
||||
with db() as conn:
|
||||
if q:
|
||||
like = f"%{q}%"
|
||||
rows = conn.execute("""
|
||||
SELECT id, external_id, titel, produkt, gefahr, herkunft, datum, quelle, url, created_at
|
||||
FROM feed_recalls
|
||||
WHERE titel LIKE ? OR produkt LIKE ? OR gefahr LIKE ? OR herkunft LIKE ?
|
||||
ORDER BY datum DESC
|
||||
LIMIT 50
|
||||
""", (like, like, like, like)).fetchall()
|
||||
else:
|
||||
rows = conn.execute("""
|
||||
SELECT id, external_id, titel, produkt, gefahr, herkunft, datum, quelle, url, created_at
|
||||
FROM feed_recalls
|
||||
ORDER BY datum DESC
|
||||
LIMIT 50
|
||||
""").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Interne Hilfsfunktion: RASFF API abfragen
|
||||
# ------------------------------------------------------------------
|
||||
async def fetch_rasff_recalls() -> list[dict]:
|
||||
"""Fragt die RASFF API ab und gibt eine Liste normalisierter Einträge zurück."""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
resp = await client.get(RASFF_URL, params=RASFF_PARAMS)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
logger.error(f"RASFF API-Fehler: {e}")
|
||||
return []
|
||||
|
||||
entries = []
|
||||
try:
|
||||
items = data.get("data", {}).get("list", [])
|
||||
for item in items:
|
||||
reference = item.get("reference", "")
|
||||
if not reference:
|
||||
continue
|
||||
|
||||
# Datum
|
||||
datum_raw = item.get("notificationDate", "")
|
||||
datum = datum_raw[:10] if datum_raw else ""
|
||||
|
||||
# Produkt
|
||||
subject = item.get("subject") or {}
|
||||
produkt = subject.get("product", "") or ""
|
||||
|
||||
# Gefahr
|
||||
hazards = subject.get("hazard") or []
|
||||
gefahr = ""
|
||||
if hazards:
|
||||
gefahr = hazards[0].get("hazardDescription", "") or ""
|
||||
|
||||
# Herkunft
|
||||
origin = item.get("origin") or {}
|
||||
herkunft = origin.get("name", "") or ""
|
||||
|
||||
# URL zur RASFF-Seite
|
||||
url = f"https://webgate.ec.europa.eu/rasff-window/screen/notificationDetail?notifRef={reference}"
|
||||
|
||||
entries.append({
|
||||
"external_id": reference,
|
||||
"titel": produkt or reference,
|
||||
"produkt": produkt,
|
||||
"gefahr": gefahr,
|
||||
"herkunft": herkunft,
|
||||
"datum": datum,
|
||||
"quelle": "rasff",
|
||||
"url": url,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"RASFF Parsing-Fehler: {e}")
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Interne Hilfsfunktion: Neue Einträge in DB speichern
|
||||
# ------------------------------------------------------------------
|
||||
def save_new_recalls(entries: list[dict]) -> list[dict]:
|
||||
"""Speichert neue Einträge und gibt die Liste der neuen Einträge zurück."""
|
||||
new_entries = []
|
||||
for entry in entries:
|
||||
try:
|
||||
with db() as conn:
|
||||
exists = conn.execute(
|
||||
"SELECT id FROM feed_recalls WHERE external_id=?",
|
||||
(entry["external_id"],)
|
||||
).fetchone()
|
||||
if not exists:
|
||||
conn.execute("""
|
||||
INSERT INTO feed_recalls
|
||||
(external_id, titel, produkt, gefahr, herkunft, datum, quelle, url)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
entry["external_id"],
|
||||
entry["titel"],
|
||||
entry["produkt"],
|
||||
entry["gefahr"],
|
||||
entry["herkunft"],
|
||||
entry["datum"],
|
||||
entry["quelle"],
|
||||
entry["url"],
|
||||
))
|
||||
new_entries.append(entry)
|
||||
except Exception as e:
|
||||
logger.warning(f"Recall DB-Fehler für {entry.get('external_id')}: {e}")
|
||||
return new_entries
|
||||
Loading…
Add table
Add a link
Reference in a new issue