Feature: Trauer-Feature, Futter-Verträglichkeit, Multi-Hund-Fixes, Wetter-Ort (Sprint 47)

- dog-profile.js: Verstorben-Button, Gedenkseite, KI-Abschiedstext
- database.py: futter_eintraege/reaktionen, route_dogs, exercise_progress.dog_id
- routes/ernaehrung.py: Futter-Verträglichkeit mit 20 Reaktionstypen + Analyse
- routes/routen.py: route_dogs Many-to-Many, Routen editierbar
- routes/training.py: exercise_progress per dog_id
- routes/ki.py: /ki/abschied Trauer-KI
- weather.py: Nominatim Ortsname parallel geladen
- ui.js: dogChip/bindDogChip, visualViewport-Modal
- api.js: gedenken, gedenkseite, futter-Methoden, route_dogs
- worlds.js: Ortsname im Wetter-Chip
- uebungen.js: _progressLoaded-Flag, dog-spezifischer Fortschritt
- trainingsplaene.js: dog_id Unterstützung
- diary.js/health.js: P-Badge Cleanup
- map.js: Wetter-Ort-Anzeige entfernt
- wetter.js: Ort in Wetter-Detail
This commit is contained in:
rene 2026-05-11 19:28:38 +02:00
parent 1ce802c8dc
commit bda61a0e40
16 changed files with 713 additions and 181 deletions

View file

@ -4,6 +4,7 @@ BAN YARO — Wetter via Open-Meteo
- get_weather_for_location(): API-Endpoint, beliebiger Standort mit TTL-Cache
"""
import asyncio
import time
import logging
import httpx
@ -62,9 +63,28 @@ async def get_weather_for_location(lat: float, lon: float) -> dict:
"&timezone=Europe%2FBerlin&forecast_days=1"
)
async with httpx.AsyncClient(timeout=8.0) as client:
resp = await client.get(url)
resp.raise_for_status()
raw = resp.json()
resp, geo_resp = await asyncio.gather(
client.get(url),
client.get(
f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json&zoom=10",
headers={"User-Agent": "BanYaro/1.0 support@banyaro.app"}
),
return_exceptions=True,
)
resp.raise_for_status()
raw = resp.json()
# Ortsname aus Reverse-Geocoding
location_name = None
try:
if not isinstance(geo_resp, Exception) and geo_resp.status_code == 200:
geo = geo_resp.json()
addr = geo.get("address", {})
location_name = (addr.get("city") or addr.get("town") or
addr.get("village") or addr.get("municipality") or
addr.get("county") or geo.get("name"))
except Exception:
pass
cur = raw.get('current', {})
daily = raw.get('daily', {})
@ -130,9 +150,10 @@ async def get_weather_for_location(lat: float, lon: float) -> dict:
'precip_prob': precip,
'uv_index': uv,
'is_day': bool(is_day),
'zecken_warnung': zecken,
'next_rain_time': next_rain_time,
'zecken_warnung': zecken,
'next_rain_time': next_rain_time,
'rain_warning_time': rain_warning_time,
'location_name': location_name,
}
_location_cache[key] = (now, data)
return data
@ -272,9 +293,23 @@ async def get_forecast(lat: float, lon: float) -> dict:
async with httpx.AsyncClient(timeout=10.0) as client:
forecast_task = client.get(forecast_url)
pollen_task = client.get(pollen_url)
forecast_resp, pollen_resp = await asyncio.gather(
forecast_task, pollen_task, return_exceptions=True
geo_task = client.get(
f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json&zoom=10",
headers={"User-Agent": "BanYaro/1.0 support@banyaro.app"}
)
forecast_resp, pollen_resp, geo_resp_fc = await asyncio.gather(
forecast_task, pollen_task, geo_task, return_exceptions=True
)
location_name_fc = None
try:
if not isinstance(geo_resp_fc, Exception) and geo_resp_fc.status_code == 200:
addr = geo_resp_fc.json().get("address", {})
location_name_fc = (addr.get("city") or addr.get("town") or
addr.get("village") or addr.get("municipality") or
addr.get("county"))
except Exception:
pass
# --- Forecast (required) ---
if isinstance(forecast_resp, Exception):
@ -421,7 +456,7 @@ async def get_forecast(lat: float, lon: float) -> dict:
'hourly': _hourly_by_day.get(date_str, []),
})
result = {'timezone': timezone, 'days': days}
result = {'timezone': timezone, 'days': days, 'location_name': location_name_fc}
_forecast_cache[key] = (now, result)
_log_forecast(round(lat, 1), round(lon, 1), days)
return result