banyaro/backend/routes/lost.py
rene 06bd8525ed Sprint 15: Zeitzone-Fix, Gewichts-Sync, Öffnungszeiten, KI-Bericht, POI-Moderation — SW by-v432, APP_VER 411
- client_time: Browser-Lokalzeit bei allen Creates mitschicken (Tagebuch, Notizen,
  Forum, Verlorener Hund, Routen) — kein UTC-Versatz mehr bei Einträgen
- Gewicht-Sync: health typ=gewicht schreibt dogs.gewicht_kg, einmalige Migration
- Praxen: opening_hours + lat/lon/osm_id in tieraerzte-Tabelle, OSM-Nearby-Lookup,
  Öffnungszeiten in Karte und Detailansicht
- KI-Gesundheitsbericht: alle 2 Wochen automatisch, ki_health_reports-Tabelle,
  Frontend-Banner mit Archiv (letzten 5 Berichte)
- POI-Korrekturen: User schlägt Öffnungszeiten-Änderung vor, Moderatoren-Tab
  genehmigt/lehnt ab, user_edited-Flag schützt vor Overpass-Überschreibung
- timeutils.py: safe_client_time() zentral für alle Routen
2026-04-26 15:38:50 +02:00

177 lines
6.3 KiB
Python

"""BAN YARO — Verlorener Hund Routes"""
import os, uuid, math
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user
from timeutils import safe_client_time
from routes.push import send_push_to_all
from media_utils import convert_media
router = APIRouter()
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
# ------------------------------------------------------------------
# Haversine-Distanz in Metern
# ------------------------------------------------------------------
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6_371_000
p1 = math.radians(lat1)
p2 = math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
class LostDogCreate(BaseModel):
name: str
rasse: Optional[str] = None
beschreibung: str
lat: float
lon: float
dog_id: Optional[int] = None
client_time: Optional[str] = None
# ------------------------------------------------------------------
# GET /api/lost — aktive Meldungen (optional nach Distanz gefiltert)
# ------------------------------------------------------------------
@router.get("")
async def list_lost(lat: Optional[float] = None, lon: Optional[float] = None,
radius_km: float = 25):
with db() as conn:
rows = conn.execute(
"""SELECT l.*, u.name AS melder_name
FROM lost_dogs l
LEFT JOIN users u ON u.id = l.user_id
WHERE l.is_active = 1
ORDER BY l.created_at DESC"""
).fetchall()
results = []
for r in rows:
entry = dict(r)
if lat is not None and lon is not None:
dist = _haversine(lat, lon, entry["lat"], entry["lon"])
if dist > radius_km * 1000:
continue
entry["distanz_m"] = round(dist)
results.append(entry)
if lat is not None and lon is not None:
results.sort(key=lambda x: x.get("distanz_m", 0))
return results
# ------------------------------------------------------------------
# POST /api/lost — Hund vermisst melden (Login erforderlich)
# ------------------------------------------------------------------
@router.post("", status_code=201)
async def report_lost(data: LostDogCreate, user=Depends(get_current_user)):
with db() as conn:
ct = safe_client_time(data.client_time)
conn.execute(
"""INSERT INTO lost_dogs (user_id, dog_id, name, rasse, beschreibung, lat, lon, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(user["id"], data.dog_id, data.name, data.rasse,
data.beschreibung, data.lat, data.lon, ct)
)
row = conn.execute(
"SELECT * FROM lost_dogs WHERE user_id=? ORDER BY id DESC LIMIT 1",
(user["id"],)
).fetchone()
entry = dict(row)
send_push_to_all({
"type": "lost_dog_alert",
"title": f"🔍 {data.name} wird vermisst!",
"body": f"{data.rasse or 'Hund'} in deiner Nähe vermisst. Hilf bei der Suche!",
"tag": f"lost-{entry['id']}",
"data": {"page": "lost"},
})
return entry
# ------------------------------------------------------------------
# POST /api/lost/{id}/foto — Foto hochladen (Login, eigene Meldung)
# ------------------------------------------------------------------
@router.post("/{lost_id}/foto")
async def upload_foto(
lost_id: int,
file: UploadFile = File(...),
user=Depends(get_current_user),
):
with db() as conn:
entry = conn.execute(
"SELECT id FROM lost_dogs WHERE id=? AND user_id=?",
(lost_id, user["id"])
).fetchone()
if not entry:
raise HTTPException(404, "Meldung nicht gefunden oder keine Berechtigung.")
data, ext = convert_media(await file.read(), file.filename or "")
if not ext:
ext = ".jpg"
filename = f"lost_{lost_id}_{uuid.uuid4().hex[:8]}{ext}"
path = os.path.join(MEDIA_DIR, "lost", filename)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(data)
foto_url = f"/media/lost/{filename}"
with db() as conn:
conn.execute("UPDATE lost_dogs SET foto_url=? WHERE id=?", (foto_url, lost_id))
return {"foto_url": foto_url}
# ------------------------------------------------------------------
# POST /api/lost/{id}/found — als gefunden markieren (Login, eigene Meldung)
# ------------------------------------------------------------------
@router.post("/{lost_id}/found")
async def mark_found(lost_id: int, user=Depends(get_current_user)):
with db() as conn:
entry = conn.execute(
"SELECT * FROM lost_dogs WHERE id=?", (lost_id,)
).fetchone()
if not entry:
raise HTTPException(404, "Meldung nicht gefunden.")
e = dict(entry)
if e["user_id"] != user["id"] and user.get("rolle") != "admin":
raise HTTPException(403, "Keine Berechtigung.")
conn.execute(
"""UPDATE lost_dogs
SET is_active=0, gefunden_at=datetime('now')
WHERE id=?""",
(lost_id,)
)
return {"ok": True}
# ------------------------------------------------------------------
# DELETE /api/lost/{id} — eigene Meldung löschen (Login)
# ------------------------------------------------------------------
@router.delete("/{lost_id}", status_code=204)
async def delete_lost(lost_id: int, user=Depends(get_current_user)):
with db() as conn:
entry = conn.execute(
"SELECT * FROM lost_dogs WHERE id=?", (lost_id,)
).fetchone()
if not entry:
raise HTTPException(404, "Meldung nicht gefunden.")
e = dict(entry)
if e["user_id"] != user["id"] and user.get("rolle") != "admin":
raise HTTPException(403, "Keine Berechtigung.")
conn.execute("DELETE FROM lost_dogs WHERE id=?", (lost_id,))
return None