Add: München als 3. Wetter-Location + Wetter-Detail-Modal

- München als tertiärer Standort (iris-Akzent) hinzugefügt
- Klick auf WeatherCard öffnet Detail-Modal mit:
  - 24h stündliche Prognose (horizontal scrollbar)
  - 7-Tage-Vorhersage mit Temperaturbalken
  - Wind, Feuchte, Sonnenauf/-untergang
- Backend: 7-Tage statt 3-Tage Forecast, 24 Hourly-Slots pro Standort
- Backend: forecast_3day → forecast Feldname-Konsistenz
- Dashboard: 3-Spalten Wetter-Grid statt 4 (HourlyForecast → Modal)
- Admin: Tertiärer Standort konfigurierbar
- THERMAL Design: iris glow, modal animation, Portal-basiertes Modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam 2026-03-03 01:13:49 +01:00
parent d9626108e6
commit 2f56be835e
13 changed files with 379 additions and 36 deletions

View file

@ -44,6 +44,7 @@ async def seed_if_empty() -> None:
"config": {
"location": os.getenv("WEATHER_LOCATION", "Leverkusen"),
"location_secondary": os.getenv("WEATHER_LOCATION_SECONDARY", "Rab,Croatia"),
"location_tertiary": os.getenv("WEATHER_LOCATION_TERTIARY", "München"),
},
"enabled": True,
"display_order": 0,

View file

@ -213,7 +213,7 @@ async def fetch_weather(location: str) -> Dict[str, Any]:
"wind_kmh": 0,
"description": "Nicht verfügbar",
"icon": "",
"forecast_3day": [],
"forecast": [],
"error": None,
}
@ -235,7 +235,7 @@ async def fetch_weather(location: str) -> Dict[str, Any]:
"current": "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
"daily": "weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset",
"timezone": geo["timezone"],
"forecast_days": 3,
"forecast_days": 7,
},
)
resp.raise_for_status()
@ -266,36 +266,42 @@ async def fetch_weather(location: str) -> Dict[str, Any]:
"error": None,
}
# Step 4: Parse 3-day forecast
# Step 4: Parse 7-day forecast
daily = data.get("daily", {})
dates = daily.get("time", [])
forecast_3day: List[Dict[str, Any]] = []
forecast: List[Dict[str, Any]] = []
for i, date_str in enumerate(dates[:3]):
code = int(daily.get("weather_code", [0] * 3)[i])
forecast_3day.append({
for i, date_str in enumerate(dates):
codes_list = daily.get("weather_code", [])
code = int(codes_list[i]) if i < len(codes_list) else 0
max_temps = daily.get("temperature_2m_max", [])
min_temps = daily.get("temperature_2m_min", [])
sunrises = daily.get("sunrise", [])
sunsets = daily.get("sunset", [])
forecast.append({
"date": date_str,
"max_temp": round(daily.get("temperature_2m_max", [0] * 3)[i]),
"min_temp": round(daily.get("temperature_2m_min", [0] * 3)[i]),
"max_temp": round(max_temps[i]) if i < len(max_temps) else 0,
"min_temp": round(min_temps[i]) if i < len(min_temps) else 0,
"icon": _wmo_icon(code),
"description": _wmo_description(code),
"sunrise": daily.get("sunrise", [""] * 3)[i].split("T")[-1] if daily.get("sunrise") else "",
"sunset": daily.get("sunset", [""] * 3)[i].split("T")[-1] if daily.get("sunset") else "",
"sunrise": sunrises[i].split("T")[-1] if i < len(sunrises) and sunrises[i] else "",
"sunset": sunsets[i].split("T")[-1] if i < len(sunsets) and sunsets[i] else "",
})
result["forecast_3day"] = forecast_3day
result["forecast"] = forecast
logger.info("[WEATHER] Fetched '%s': %d°C, %s", geo["name"], result["temp"], result["description"])
return result
async def fetch_hourly_forecast(location: str) -> List[Dict[str, Any]]:
"""Fetch hourly forecast for the next 8 hours from Open-Meteo.
async def fetch_hourly_forecast(location: str, max_slots: int = 8) -> List[Dict[str, Any]]:
"""Fetch hourly forecast from Open-Meteo.
Args:
location: City name or coordinates.
max_slots: Maximum number of hourly slots to return.
Returns:
List of hourly forecast dicts (max 8 entries).
List of hourly forecast dicts.
"""
geo = await _geocode(location)
if geo is None:
@ -354,7 +360,7 @@ async def fetch_hourly_forecast(location: str) -> List[Dict[str, Any]]:
"wind_kmh": round(winds[i]) if i < len(winds) else 0,
})
if len(upcoming) >= 8:
if len(upcoming) >= max_slots:
break
logger.debug("[WEATHER] Hourly for '%s': %d slots", location, len(upcoming))