This commit is contained in:
rxf
2026-03-22 20:09:44 +01:00
parent 0b9d21c24c
commit c471c0e33a
4 changed files with 426 additions and 128 deletions

View File

@@ -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() {
</header>
<main className="app-main">
<WeatherDashboard data={weatherData} />
<WeatherDashboard
data={weatherData}
rainData={rainData}
timeRange={timeRange}
onTimeRangeChange={setTimeRange}
/>
</main>
</div>
)

View File

@@ -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;
}

View File

@@ -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 (
<div className="dashboard">
{/* Navigation für Zeitraum-Auswahl */}
<div className="time-range-nav">
<button
className={timeRange === '24h' ? 'active' : ''}
onClick={() => onTimeRangeChange('24h')}
>
24 Stunden
</button>
<button
className={timeRange === '7d' ? 'active' : ''}
onClick={() => onTimeRangeChange('7d')}
>
7 Tage
</button>
<button
className={timeRange === '30d' ? 'active' : ''}
onClick={() => onTimeRangeChange('30d')}
>
30 Tage
</button>
<button
className={timeRange === '365d' ? 'active' : ''}
onClick={() => onTimeRangeChange('365d')}
>
365 Tage
</button>
</div>
{/* Zeitraum-Beschreibung */}
<div className="time-range-label">
{timeRangeLabel}
</div>
{/* Charts Grid */}
<div className="charts-grid">
<div className="chart-container">
@@ -379,7 +470,7 @@ const WeatherDashboard = ({ data }) => {
<HighchartsReact highcharts={Highcharts} options={temperatureOptions} />
</div>
<div className="chart-stats">
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 || '-'})
</div>
</div>
@@ -389,7 +480,7 @@ const WeatherDashboard = ({ data }) => {
<HighchartsReact highcharts={Highcharts} options={pressureOptions} />
</div>
<div className="chart-stats">
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 || '-'})
</div>
</div>
@@ -399,7 +490,7 @@ const WeatherDashboard = ({ data }) => {
<HighchartsReact highcharts={Highcharts} options={humidityOptions} />
</div>
<div className="chart-stats">
Min: {todayStats.minHumidity || '-'}% ({todayStats.minHumidityTime || '-'}) | Max: {todayStats.maxHumidity || '-'}% ({todayStats.maxHumidityTime || '-'})
Min: {periodStats.minHumidity || '-'}% ({periodStats.minHumidityTime || '-'}) | Max: {periodStats.maxHumidity || '-'}% ({periodStats.maxHumidityTime || '-'})
</div>
</div>
@@ -423,7 +514,7 @@ const WeatherDashboard = ({ data }) => {
<HighchartsReact highcharts={Highcharts} options={windSpeedOptions} />
</div>
<div className="chart-stats">
Max: {todayStats.maxWindGust?.toFixed(1) || '-'} km/h ({todayStats.maxWindGustTime || '-'})
Max: {periodStats.maxWindGust?.toFixed(1) || '-'} km/h ({periodStats.maxWindGustTime || '-'})
</div>
</div>