diff --git a/api/main.py b/api/main.py
index 9f4718b..bb0c4b5 100644
--- a/api/main.py
+++ b/api/main.py
@@ -1,6 +1,6 @@
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional
from datetime import datetime, timedelta
import os
@@ -47,6 +47,8 @@ app.add_middleware(
# Pydantic Models
class WeatherData(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
id: int
datetime: datetime
temperature: Optional[float] = None
@@ -59,9 +61,6 @@ class WeatherData(BaseModel):
rain_rate: Optional[float] = None
received_at: datetime
- class Config:
- from_attributes = True
-
class WeatherStats(BaseModel):
avg_temperature: Optional[float] = None
@@ -343,6 +342,120 @@ async def get_rain_data(
conn.close()
+@app.get("/weather/hourly-aggregated", response_model=List[WeatherData], tags=["Aggregated Data"])
+async def get_hourly_aggregated_data(
+ days: int = Query(7, ge=1, le=60, description="Anzahl Tage zurück (max 60)")
+):
+ """Gibt stündlich aggregierte Wetterdaten zurück (Stundenmittel)"""
+ conn = get_db_connection()
+ try:
+ with conn.cursor() as cursor:
+ cursor.execute("""
+ SELECT
+ 0 as id,
+ date_trunc('hour', datetime) as datetime,
+ AVG(temperature) as temperature,
+ ROUND(AVG(humidity)) as humidity,
+ AVG(pressure) as pressure,
+ AVG(wind_speed * 1.60934) as wind_speed,
+ MAX(wind_gust * 1.60934) as wind_gust,
+ AVG(wind_dir) as wind_dir,
+ AVG(rain) as rain,
+ AVG(rain_rate) as rain_rate,
+ MAX(received_at) as received_at
+ FROM weather_data
+ WHERE datetime >= NOW() - make_interval(days => %s)
+ GROUP BY date_trunc('hour', datetime)
+ ORDER BY datetime ASC
+ """, (days,))
+ results = cursor.fetchall()
+
+ return [dict(row) for row in results]
+ finally:
+ conn.close()
+
+
+@app.get("/weather/daily-aggregated", response_model=List[WeatherData], tags=["Aggregated Data"])
+async def get_daily_aggregated_data(
+ days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück (max 730)")
+):
+ """Gibt täglich aggregierte Wetterdaten zurück (Tagesmittel)"""
+ conn = get_db_connection()
+ try:
+ with conn.cursor() as cursor:
+ cursor.execute("""
+ SELECT
+ 0 as id,
+ date_trunc('day', datetime) as datetime,
+ AVG(temperature) as temperature,
+ ROUND(AVG(humidity)) as humidity,
+ AVG(pressure) as pressure,
+ AVG(wind_speed * 1.60934) as wind_speed,
+ MAX(wind_gust * 1.60934) as wind_gust,
+ AVG(wind_dir) as wind_dir,
+ AVG(rain) as rain,
+ AVG(rain_rate) as rain_rate,
+ MAX(received_at) as received_at
+ FROM weather_data
+ WHERE datetime >= NOW() - make_interval(days => %s)
+ GROUP BY date_trunc('day', datetime)
+ ORDER BY datetime ASC
+ """, (days,))
+ results = cursor.fetchall()
+
+ return [dict(row) for row in results]
+ finally:
+ conn.close()
+
+
+@app.get("/weather/rain-daily", response_model=List[dict], tags=["Aggregated Data"])
+async def get_daily_rain_data(
+ days: int = Query(30, ge=1, le=365, description="Anzahl Tage zurück")
+):
+ """Gibt tägliche Regensummen zurück"""
+ conn = get_db_connection()
+ try:
+ with conn.cursor() as cursor:
+ cursor.execute("""
+ SELECT
+ date_trunc('day', datetime) as date,
+ SUM(rain) as total_rain
+ FROM weather_data
+ WHERE datetime >= NOW() - make_interval(days => %s)
+ GROUP BY date_trunc('day', datetime)
+ ORDER BY date ASC
+ """, (days,))
+ results = cursor.fetchall()
+
+ return [dict(row) for row in results]
+ finally:
+ conn.close()
+
+
+@app.get("/weather/rain-weekly", response_model=List[dict], tags=["Aggregated Data"])
+async def get_weekly_rain_data(
+ days: int = Query(365, ge=1, le=730, description="Anzahl Tage zurück")
+):
+ """Gibt wöchentliche Regensummen zurück (Woche = Mo-So)"""
+ conn = get_db_connection()
+ try:
+ with conn.cursor() as cursor:
+ cursor.execute("""
+ SELECT
+ date_trunc('week', datetime) as week_start,
+ SUM(rain) as total_rain
+ FROM weather_data
+ WHERE datetime >= NOW() - make_interval(days => %s)
+ GROUP BY date_trunc('week', datetime)
+ ORDER BY week_start ASC
+ """, (days,))
+ results = cursor.fetchall()
+
+ return [dict(row) for row in results]
+ finally:
+ conn.close()
+
+
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 3e05056..4d04010 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -4,35 +4,73 @@ import './App.css'
function App() {
const [weatherData, setWeatherData] = useState([])
+ const [rainData, setRainData] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [lastUpdate, setLastUpdate] = useState(null)
+ const [timeRange, setTimeRange] = useState('24h') // '24h', '7d', '30d', '365d'
useEffect(() => {
const fetchData = async () => {
try {
+ setLoading(true)
+
// Prüfe ob eingebettete Daten vorhanden sind (statischer Build)
- if (window.__WEATHER_DATA__) {
+ if (window.__WEATHER_DATA__ && timeRange === '24h') {
setWeatherData(window.__WEATHER_DATA__)
+ setRainData([])
setLastUpdate(new Date())
setLoading(false)
- } else {
- // Development oder Production: Daten von API holen
- // Im Development: localhost:8000
- // Im Production: /api/ (nginx proxy)
- const apiUrl = import.meta.env.DEV
- ? 'http://localhost:8000/weather/history?hours=24'
- : '/api/weather/history?hours=24'
-
- const response = await fetch(apiUrl)
- if (!response.ok) {
- throw new Error('API-Fehler: ' + response.status)
- }
- const data = await response.json()
- setWeatherData(data)
- setLastUpdate(new Date())
- setLoading(false)
+ return
}
+
+ // API-URLs basierend auf Zeitraum
+ let weatherUrl, rainUrl
+ const baseUrl = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
+
+ switch (timeRange) {
+ case '24h':
+ weatherUrl = `${baseUrl}/weather/history?hours=24`
+ rainUrl = null
+ break
+ case '7d':
+ weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=7`
+ rainUrl = `${baseUrl}/weather/rain-daily?days=7`
+ break
+ case '30d':
+ weatherUrl = `${baseUrl}/weather/hourly-aggregated?days=30`
+ rainUrl = `${baseUrl}/weather/rain-daily?days=30`
+ break
+ case '365d':
+ weatherUrl = `${baseUrl}/weather/daily-aggregated?days=365`
+ rainUrl = `${baseUrl}/weather/rain-weekly?days=365`
+ break
+ default:
+ weatherUrl = `${baseUrl}/weather/history?hours=24`
+ rainUrl = null
+ }
+
+ // Wetterdaten laden
+ const weatherResponse = await fetch(weatherUrl)
+ if (!weatherResponse.ok) {
+ throw new Error('API-Fehler: ' + weatherResponse.status)
+ }
+ const weatherDataResult = await weatherResponse.json()
+ setWeatherData(weatherDataResult)
+
+ // Regendaten laden (falls separater Endpunkt)
+ if (rainUrl) {
+ const rainResponse = await fetch(rainUrl)
+ if (rainResponse.ok) {
+ const rainDataResult = await rainResponse.json()
+ setRainData(rainDataResult)
+ }
+ } else {
+ setRainData([])
+ }
+
+ setLastUpdate(new Date())
+ setLoading(false)
} catch (err) {
setError(err.message)
setLoading(false)
@@ -41,12 +79,12 @@ function App() {
fetchData()
- // Automatisches Update alle 5 Minuten (nur im Entwicklungsmodus)
- if (!window.__WEATHER_DATA__) {
+ // Automatisches Update alle 5 Minuten (nur für 24h und ohne statische Daten)
+ if (!window.__WEATHER_DATA__ && timeRange === '24h') {
const interval = setInterval(fetchData, 5 * 60 * 1000)
return () => clearInterval(interval)
}
- }, [])
+ }, [timeRange])
if (loading) {
return (
@@ -98,7 +136,12 @@ function App() {
-
+
)
diff --git a/frontend/src/components/WeatherDashboard.css b/frontend/src/components/WeatherDashboard.css
index e8026f8..87f20cb 100644
--- a/frontend/src/components/WeatherDashboard.css
+++ b/frontend/src/components/WeatherDashboard.css
@@ -4,6 +4,48 @@
margin: 0 auto;
}
+.time-range-nav {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.time-range-nav button {
+ padding: 0.5rem 1.5rem;
+ background: white;
+ border: 2px solid #ddd;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: #333;
+ transition: all 0.2s ease;
+}
+
+.time-range-nav button:hover {
+ background: #f5f5f5;
+ border-color: #0066cc;
+}
+
+.time-range-nav button.active {
+ background: #0066cc;
+ border-color: #0066cc;
+ color: white;
+}
+
+.time-range-label {
+ text-align: center;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 1.5rem;
+ padding: 0.5rem;
+ background: #f8f9fa;
+ border-radius: 8px;
+}
+
.current-values {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
@@ -137,6 +179,15 @@
padding: 0 0.5rem;
}
+ .time-range-nav button {
+ padding: 0.4rem 1rem;
+ font-size: 0.85rem;
+ }
+
+ .time-range-label {
+ font-size: 1rem;
+ }
+
.version-short {
display: inline;
}
diff --git a/frontend/src/components/WeatherDashboard.jsx b/frontend/src/components/WeatherDashboard.jsx
index fe1c592..eca3208 100644
--- a/frontend/src/components/WeatherDashboard.jsx
+++ b/frontend/src/components/WeatherDashboard.jsx
@@ -20,63 +20,88 @@ Highcharts.setOptions({
}
})
-const WeatherDashboard = ({ data }) => {
+const WeatherDashboard = ({ data, rainData = [], timeRange = '24h', onTimeRangeChange }) => {
// Daten vorbereiten und nach Zeit sortieren (älteste zuerst)
const sortedData = useMemo(() => {
return [...data].sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
}, [data])
+
+ // Zeitraum-Label
+ const timeRangeLabel = useMemo(() => {
+ switch (timeRange) {
+ case '24h': return 'Die letzten 24 Stunden'
+ case '7d': return 'Die letzten 7 Tage'
+ case '30d': return 'Die letzten 30 Tage'
+ case '365d': return 'Die letzten 365 Tage'
+ default: return 'Die letzten 24 Stunden'
+ }
+ }, [timeRange])
- // Gemeinsame Chart-Optionen
- const getCommonOptions = () => ({
- chart: {
- height: '50%',
- animation: false,
- backgroundColor: 'transparent'
- },
- accessibility: {
- enabled: false
- },
- credits: {
- enabled: false
- },
- title: {
- text: null
- },
- legend: {
- enabled: false
- },
- tooltip: {
- shared: true,
- crosshairs: true,
- xDateFormat: '%d.%m.%Y %H:%M'
- },
- plotOptions: {
- series: {
- marker: {
- enabled: false,
- states: {
- hover: {
- enabled: true,
- radius: 5
+ // Gemeinsame Chart-Optionen (angepasst an Zeitraum)
+ const getCommonOptions = () => {
+ // X-Achsen-Konfiguration basierend auf Zeitraum
+ let xAxisConfig = {
+ type: 'datetime',
+ gridLineWidth: 1,
+ gridLineColor: 'rgba(0, 0, 0, 0.1)'
+ }
+
+ switch (timeRange) {
+ case '24h':
+ xAxisConfig.tickInterval = 4 * 3600 * 1000 // 4 Stunden
+ xAxisConfig.labels = { format: '{value:%H:%M}', align: 'center' }
+ break
+ case '7d':
+ case '30d':
+ xAxisConfig.labels = { format: '{value:%d.%m}', align: 'center' }
+ break
+ case '365d':
+ xAxisConfig.labels = { format: '{value:%b}', align: 'center' }
+ break
+ }
+
+ return {
+ chart: {
+ height: '50%',
+ animation: false,
+ backgroundColor: 'transparent'
+ },
+ accessibility: {
+ enabled: false
+ },
+ credits: {
+ enabled: false
+ },
+ title: {
+ text: null
+ },
+ legend: {
+ enabled: false
+ },
+ tooltip: {
+ shared: true,
+ crosshairs: true,
+ xDateFormat: timeRange === '24h' ? '%d.%m.%Y %H:%M' : '%d.%m.%Y'
+ },
+ plotOptions: {
+ series: {
+ marker: {
+ enabled: false,
+ states: {
+ hover: {
+ enabled: true,
+ radius: 5
+ }
}
}
}
- }
- },
- xAxis: {
- type: 'datetime',
- tickInterval: 4 * 3600 * 1000, // 4 Stunden in Millisekunden
- labels: {
- format: '{value:%H:%M}',
- align: 'center'
},
- gridLineWidth: 1,
- gridLineColor: 'rgba(0, 0, 0, 0.1)'
- },
- yAxis: {
- gridLineColor: 'rgba(0, 0, 0, 0.05)'
+ xAxis: xAxisConfig,
+ yAxis: {
+ gridLineColor: 'rgba(0, 0, 0, 0.05)'
+ }
}
- })
+ }
// Temperatur Chart
const temperatureOptions = useMemo(() => {
@@ -192,33 +217,68 @@ const WeatherDashboard = ({ data }) => {
}
}, [sortedData])
- // Regen Chart
- const rainOptions = useMemo(() => ({
- ...getCommonOptions(),
- yAxis: {
- ...getCommonOptions().yAxis,
- title: { text: 'Regen (mm) / Rate (mm/h)' }
- },
- series: [{
- name: 'Regen',
- data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
- color: 'rgb(54, 162, 235)',
- fillColor: 'rgba(54, 162, 235, 0.3)',
- type: 'area',
- tooltip: {
- valueSuffix: ' mm'
- }
- }, {
- name: 'Regenrate',
- data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain_rate]),
- color: 'rgb(59, 130, 246)',
- dashStyle: 'Dash',
- type: 'line',
- tooltip: {
- valueSuffix: ' mm/h'
- }
- }]
- }), [sortedData])
+ // Regen Chart (angepasst an Zeitraum)
+ const rainOptions = useMemo(() => {
+ let series = []
+ let yAxisTitle = 'Regen (mm) / Rate (mm/h)'
+
+ if (timeRange === '24h') {
+ // 24h: Area Chart mit Regen und Regenrate
+ yAxisTitle = 'Regen (mm) / Rate (mm/h)'
+ series = [{
+ name: 'Regen',
+ data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain]),
+ color: 'rgb(54, 162, 235)',
+ fillColor: 'rgba(54, 162, 235, 0.3)',
+ type: 'area',
+ tooltip: {
+ valueSuffix: ' mm'
+ }
+ }, {
+ name: 'Regenrate',
+ data: sortedData.map(item => [new Date(item.datetime).getTime(), item.rain_rate]),
+ color: 'rgb(59, 130, 246)',
+ dashStyle: 'Dash',
+ type: 'line',
+ tooltip: {
+ valueSuffix: ' mm/h'
+ }
+ }]
+ } else if (timeRange === '7d' || timeRange === '30d') {
+ // 7d/30d: Balkendiagramm mit täglichen Summen
+ yAxisTitle = 'Regen (mm pro Tag)'
+ series = [{
+ name: 'Regen',
+ data: rainData.map(item => [new Date(item.date).getTime(), item.total_rain || 0]),
+ color: 'rgb(54, 162, 235)',
+ type: 'column',
+ tooltip: {
+ valueSuffix: ' mm'
+ }
+ }]
+ } else if (timeRange === '365d') {
+ // 365d: Balkendiagramm mit wöchentlichen Summen
+ yAxisTitle = 'Regen (mm pro Woche)'
+ series = [{
+ name: 'Regen',
+ data: rainData.map(item => [new Date(item.week_start).getTime(), item.total_rain || 0]),
+ color: 'rgb(54, 162, 235)',
+ type: 'column',
+ tooltip: {
+ valueSuffix: ' mm'
+ }
+ }]
+ }
+
+ return {
+ ...getCommonOptions(),
+ yAxis: {
+ ...getCommonOptions().yAxis,
+ title: { text: yAxisTitle }
+ },
+ series
+ }
+ }, [sortedData, rainData, timeRange])
// Windgeschwindigkeit Chart
const windSpeedOptions = useMemo(() => ({
@@ -311,66 +371,97 @@ const WeatherDashboard = ({ data }) => {
// Aktuellste Werte für Übersicht
const current = sortedData[sortedData.length - 1] || {}
- // Berechne Min/Max für den aktuellen Tag
- const todayStats = useMemo(() => {
- const now = new Date()
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
-
- const todayData = sortedData.filter(item => {
- const itemDate = new Date(item.datetime)
- return itemDate >= todayStart
- })
+ // Berechne Min/Max für den gewählten Zeitraum
+ const periodStats = useMemo(() => {
+ // Für den gewählten Zeitraum alle Daten verwenden
+ const periodData = sortedData
- if (todayData.length === 0) {
+ if (periodData.length === 0) {
return {
minTemp: null, maxTemp: null, minTempTime: null, maxTempTime: null,
minHumidity: null, maxHumidity: null, minHumidityTime: null, maxHumidityTime: null,
minPressure: null, maxPressure: null, minPressureTime: null, maxPressureTime: null
}
}
+
+ // Zeitformat basierend auf Zeitraum
+ const timeFormat = timeRange === '24h' ? 'HH:mm' : 'dd.MM HH:mm'
// Temperatur
- const minTempItem = todayData.reduce((min, item) =>
+ const minTempItem = periodData.reduce((min, item) =>
item.temperature != null && (min === null || item.temperature < min.temperature) ? item : min, null)
- const maxTempItem = todayData.reduce((max, item) =>
+ const maxTempItem = periodData.reduce((max, item) =>
item.temperature != null && (max === null || item.temperature > max.temperature) ? item : max, null)
// Luftfeuchtigkeit
- const minHumidityItem = todayData.reduce((min, item) =>
+ const minHumidityItem = periodData.reduce((min, item) =>
item.humidity != null && (min === null || item.humidity < min.humidity) ? item : min, null)
- const maxHumidityItem = todayData.reduce((max, item) =>
+ const maxHumidityItem = periodData.reduce((max, item) =>
item.humidity != null && (max === null || item.humidity > max.humidity) ? item : max, null)
// Luftdruck
- const minPressureItem = todayData.reduce((min, item) =>
+ const minPressureItem = periodData.reduce((min, item) =>
item.pressure != null && (min === null || item.pressure < min.pressure) ? item : min, null)
- const maxPressureItem = todayData.reduce((max, item) =>
+ const maxPressureItem = periodData.reduce((max, item) =>
item.pressure != null && (max === null || item.pressure > max.pressure) ? item : max, null)
// Windgeschwindigkeit
- const maxWindGustItem = todayData.reduce((max, item) =>
+ const maxWindGustItem = periodData.reduce((max, item) =>
item.wind_gust != null && (max === null || item.wind_gust > max.wind_gust) ? item : max, null)
return {
minTemp: minTempItem?.temperature ?? null,
maxTemp: maxTempItem?.temperature ?? null,
- minTempTime: minTempItem ? format(new Date(minTempItem.datetime), 'HH:mm', { locale: de }) : null,
- maxTempTime: maxTempItem ? format(new Date(maxTempItem.datetime), 'HH:mm', { locale: de }) : null,
+ minTempTime: minTempItem ? format(new Date(minTempItem.datetime), timeFormat, { locale: de }) : null,
+ maxTempTime: maxTempItem ? format(new Date(maxTempItem.datetime), timeFormat, { locale: de }) : null,
minHumidity: minHumidityItem?.humidity ?? null,
maxHumidity: maxHumidityItem?.humidity ?? null,
- minHumidityTime: minHumidityItem ? format(new Date(minHumidityItem.datetime), 'HH:mm', { locale: de }) : null,
- maxHumidityTime: maxHumidityItem ? format(new Date(maxHumidityItem.datetime), 'HH:mm', { locale: de }) : null,
+ minHumidityTime: minHumidityItem ? format(new Date(minHumidityItem.datetime), timeFormat, { locale: de }) : null,
+ maxHumidityTime: maxHumidityItem ? format(new Date(maxHumidityItem.datetime), timeFormat, { locale: de }) : null,
minPressure: minPressureItem?.pressure ?? null,
maxPressure: maxPressureItem?.pressure ?? null,
- minPressureTime: minPressureItem ? format(new Date(minPressureItem.datetime), 'HH:mm', { locale: de }) : null,
- maxPressureTime: maxPressureItem ? format(new Date(maxPressureItem.datetime), 'HH:mm', { locale: de }) : null,
+ minPressureTime: minPressureItem ? format(new Date(minPressureItem.datetime), timeFormat, { locale: de }) : null,
+ maxPressureTime: maxPressureItem ? format(new Date(maxPressureItem.datetime), timeFormat, { locale: de }) : null,
maxWindGust: maxWindGustItem?.wind_gust ?? null,
- maxWindGustTime: maxWindGustItem ? format(new Date(maxWindGustItem.datetime), 'HH:mm', { locale: de }) : null
+ maxWindGustTime: maxWindGustItem ? format(new Date(maxWindGustItem.datetime), timeFormat, { locale: de }) : null
}
- }, [sortedData])
+ }, [sortedData, timeRange])
return (
+ {/* Navigation für Zeitraum-Auswahl */}
+
+
+
+
+
+
+
+ {/* Zeitraum-Beschreibung */}
+
+ {timeRangeLabel}
+
+
{/* Charts Grid */}
@@ -379,7 +470,7 @@ const WeatherDashboard = ({ data }) => {
- Min: {todayStats.minTemp?.toFixed(1) || '-'}°C ({todayStats.minTempTime || '-'}) | Max: {todayStats.maxTemp?.toFixed(1) || '-'}°C ({todayStats.maxTempTime || '-'})
+ Min: {periodStats.minTemp?.toFixed(1) || '-'}°C ({periodStats.minTempTime || '-'}) | Max: {periodStats.maxTemp?.toFixed(1) || '-'}°C ({periodStats.maxTempTime || '-'})
@@ -389,7 +480,7 @@ const WeatherDashboard = ({ data }) => {
- Min: {todayStats.minPressure?.toFixed(1) || '-'} hPa ({todayStats.minPressureTime || '-'}) | Max: {todayStats.maxPressure?.toFixed(1) || '-'} hPa ({todayStats.maxPressureTime || '-'})
+ Min: {periodStats.minPressure?.toFixed(1) || '-'} hPa ({periodStats.minPressureTime || '-'}) | Max: {periodStats.maxPressure?.toFixed(1) || '-'} hPa ({periodStats.maxPressureTime || '-'})
@@ -399,7 +490,7 @@ const WeatherDashboard = ({ data }) => {
- Min: {todayStats.minHumidity || '-'}% ({todayStats.minHumidityTime || '-'}) | Max: {todayStats.maxHumidity || '-'}% ({todayStats.maxHumidityTime || '-'})
+ Min: {periodStats.minHumidity || '-'}% ({periodStats.minHumidityTime || '-'}) | Max: {periodStats.maxHumidity || '-'}% ({periodStats.maxHumidityTime || '-'})
@@ -423,7 +514,7 @@ const WeatherDashboard = ({ data }) => {
- Max: {todayStats.maxWindGust?.toFixed(1) || '-'} km/h ({todayStats.maxWindGustTime || '-'})
+ Max: {periodStats.maxWindGust?.toFixed(1) || '-'} km/h ({periodStats.maxWindGustTime || '-'})