Files
wetter_station/frontend/src/App.jsx
T
admin 9c2855fa98 V 1.6.0 fix: Tagesregen per MAX (kumulierter Tageszähler, Reset um Mitternacht)
Wochenwerte als Summe täglicher Maxima; /weather/stats mit Subquery über tägliche Maxima.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:31:07 +02:00

230 lines
7.7 KiB
React

import { useState, useEffect, useRef } from 'react'
import WeatherDashboard from './components/WeatherDashboard'
import './App.css'
// API-Basis-URL: in Dev direkt auf Backend, in Prod ueber Nginx-Proxy
const API_BASE = import.meta.env.DEV ? 'http://localhost:8000' : '/api'
// 24-Stunden-URL fuer "Aktuell"-Anzeige (auch bei laengeren Zeitraeumen gebraucht)
const CURRENT_URL = `${API_BASE}/weather/history?hours=24&limit=5000`
// JSON-Fetch-Helfer: liefert {ok, data} oder wirft bei Netzfehler.
// Per signal kann der Request abgebrochen werden, wenn timeRange wechselt.
async function fetchJson(url, signal) {
const res = await fetch(url, { signal })
if (!res.ok) throw new Error(`HTTP ${res.status} bei ${url}`)
return res.json()
}
// Bestimmt die URLs fuer den gewaehlten Zeitbereich.
// Returns: { weatherUrl, rainUrl, needsCurrent }
function buildUrls(timeRange) {
// Custom-Range
if (typeof timeRange === 'object' && timeRange.type === 'custom') {
const start = encodeURIComponent(timeRange.start)
const end = encodeURIComponent(timeRange.end)
const days = timeRange.days || 1
const path = days >= 7 ? 'daily-aggregated-range' : 'hourly-aggregated-range'
return {
weatherUrl: `${API_BASE}/weather/${path}?start=${start}&end=${end}`,
rainUrl: days < 7 ? `${API_BASE}/weather/daily-aggregated-range?start=${start}&end=${end}` : null,
needsCurrent: true,
}
}
switch (timeRange) {
case '24h':
return {
weatherUrl: `${API_BASE}/weather/history?hours=24&limit=5000`,
rainUrl: null,
needsCurrent: false, // Hauptdaten SIND die aktuellen 24h-Daten
}
case '7d':
return {
weatherUrl: `${API_BASE}/weather/daily-with-minmax?days=7`,
rainUrl: `${API_BASE}/weather/rain-daily?days=7`,
needsCurrent: true,
}
case '30d':
return {
weatherUrl: `${API_BASE}/weather/daily-with-minmax?days=30`,
rainUrl: `${API_BASE}/weather/rain-daily?days=30`,
needsCurrent: true,
}
case '365d':
return {
weatherUrl: `${API_BASE}/weather/daily-aggregated?days=365`,
rainUrl: `${API_BASE}/weather/rain-weekly?days=365`,
needsCurrent: true,
}
default:
return {
weatherUrl: `${API_BASE}/weather/history?hours=24`,
rainUrl: null,
needsCurrent: false,
}
}
}
function App() {
const [weatherData, setWeatherData] = useState([])
const [currentWeatherData, setCurrentWeatherData] = 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')
const [showTable, setShowTable] = useState(false)
// Erster-Lade-Flag: nur beim allerersten Fetch zeigen wir den Spinner.
// Bei spaeteren Re-Fetches (Auto-Refresh, Time-Range-Wechsel) bleiben die
// alten Daten sichtbar, bis die neuen da sind — flackert weniger.
const isInitialLoadRef = useRef(true)
const handleTimeRangeChange = (range, customParams) => {
if (range === 'custom' && customParams) {
const start = new Date(customParams.start)
const end = new Date(customParams.end)
const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24))
setTimeRange({ type: 'custom', start: customParams.start, end: customParams.end, days })
} else {
setTimeRange(range)
}
}
useEffect(() => {
// Statische Daten: kein Fetch noetig
if (window.__WEATHER_DATA__ && timeRange === '24h') {
setWeatherData(window.__WEATHER_DATA__)
setCurrentWeatherData(window.__WEATHER_DATA__)
setRainData([])
setLastUpdate(new Date())
setLoading(false)
return
}
const controller = new AbortController()
const fetchData = async () => {
if (isInitialLoadRef.current) setLoading(true)
const { weatherUrl, rainUrl, needsCurrent } = buildUrls(timeRange)
// Alle drei Requests parallel starten (statt sequentiell wie vorher).
// allSettled, damit ein Fehler bei rain/current die Hauptdaten nicht blockiert.
const requests = [
fetchJson(weatherUrl, controller.signal), // [0] weather - Pflicht
needsCurrent ? fetchJson(CURRENT_URL, controller.signal) : null, // [1] current - optional
rainUrl ? fetchJson(rainUrl, controller.signal) : null, // [2] rain - optional
]
const results = await Promise.allSettled(requests.map(p => p ?? Promise.resolve(null)))
// AbortError ignorieren — passiert, wenn timeRange waehrend des Requests
// gewechselt hat. Der nachfolgende Effekt-Lauf macht den richtigen Fetch.
const aborted = results.some(
r => r.status === 'rejected' && r.reason?.name === 'AbortError'
)
if (aborted) return
// Hauptdaten-Fehler ist fatal; ohne die zeigen wir nichts an.
if (results[0].status === 'rejected') {
setError(results[0].reason?.message || 'Unbekannter Fehler')
setLoading(false)
isInitialLoadRef.current = false
return
}
const weatherResult = results[0].value
const currentResult = results[1].status === 'fulfilled' ? results[1].value : null
const rainResult = results[2].status === 'fulfilled' ? results[2].value : null
setError(null)
setWeatherData(weatherResult)
// Wenn 24h gewaehlt ist, sind weather und current dieselben Daten
setCurrentWeatherData(needsCurrent ? (currentResult ?? []) : weatherResult)
setRainData(rainResult ?? [])
setLastUpdate(new Date())
setLoading(false)
isInitialLoadRef.current = false
}
fetchData()
// Auto-Refresh nur bei 24h, nur wenn keine statischen Daten
let interval = null
if (!window.__WEATHER_DATA__ && timeRange === '24h') {
interval = setInterval(fetchData, 5 * 60 * 1000)
}
return () => {
controller.abort()
if (interval) clearInterval(interval)
}
}, [timeRange])
if (loading) {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>Lade Wetterdaten...</p>
</div>
)
}
if (error) {
return (
<div className="error-container">
<h2>Fehler beim Laden der Daten</h2>
<p>{error}</p>
</div>
)
}
// Aktuelle Zeit formatieren
const now = new Date()
const dateStr = now.toLocaleDateString('de-DE', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
const timeStr = now.toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
})
// TODO: Sonnenauf-/untergang und Mondphase berechnen
// Aktuell Platzhalter - benoetigt Bibliothek wie 'suncalc'
const sunrise = "06:45"
const sunset = "18:30"
const moonPhase = "abnehmend 50%"
return (
<div className="app">
<header className="app-header">
<h1>Aktuelle Wetterdaten</h1>
<div className="header-datetime">{dateStr} - {timeStr} Uhr</div>
<div className="header-spacer"></div>
<div className="header-coordinates">48.6 N - 9.6 E - 574 m NN</div>
<div className="header-astro">
Sonnen-Aufgang: {sunrise} - Untergang: {sunset} &nbsp;&nbsp; Mond-Phase: {moonPhase}
</div>
</header>
<main className="app-main">
<WeatherDashboard
data={weatherData}
currentData={currentWeatherData}
rainData={rainData}
timeRange={timeRange}
onTimeRangeChange={handleTimeRangeChange}
showTable={showTable}
onToggleTable={() => setShowTable(v => !v)}
/>
</main>
</div>
)
}
export default App