diff --git a/DEPLOY.md b/DEPLOY.md
new file mode 100644
index 0000000..8202d42
--- /dev/null
+++ b/DEPLOY.md
@@ -0,0 +1,155 @@
+# Deployment auf externen Webserver
+
+## Übersicht
+
+Das System generiert alle 4 Minuten statische HTML-Dateien mit aktuellen Wetterdaten und lädt sie auf einen externen Webserver hoch.
+
+## Voraussetzungen
+
+1. **SSH-Zugang** zum Webserver (passwortlos mit SSH-Key empfohlen)
+2. **rsync** installiert (auf macOS bereits vorhanden)
+3. **Python 3** mit venv
+4. **API läuft lokal** (localhost:8000)
+
+## Setup
+
+### 1. Python-Abhängigkeiten installieren
+
+```bash
+source .venv/bin/activate
+pip install requests
+```
+
+### 2. Konfiguration anpassen
+
+Bearbeiten Sie `generate-static.py`:
+
+```python
+REMOTE_SERVER = "user@ihr-server.de" # Ihr SSH-Zugang
+REMOTE_PATH = "/var/www/html/wetterstation" # Pfad auf dem Server
+```
+
+### 3. SSH-Key Setup (einmalig)
+
+Für automatisches Upload ohne Passwort-Eingabe:
+
+```bash
+# SSH-Key generieren (falls noch nicht vorhanden)
+ssh-keygen -t ed25519
+
+# Public Key auf Server kopieren
+ssh-copy-id user@ihr-server.de
+
+# Test
+ssh user@ihr-server.de "echo 'Verbindung OK'"
+```
+
+### 4. Frontend für statische Nutzung vorbereiten
+
+```bash
+# App.jsx durch statische Version ersetzen
+cp frontend/src/App-static.jsx frontend/src/App.jsx
+```
+
+### 5. Manueller Test
+
+```bash
+python generate-static.py
+```
+
+Dies sollte:
+- Wetterdaten laden ✓
+- Frontend bauen ✓
+- Daten einbetten ✓
+- Dateien hochladen ✓
+
+### 6. Cronjob installieren
+
+```bash
+chmod +x setup-cronjob.sh
+./setup-cronjob.sh
+```
+
+Der Cronjob läuft dann automatisch alle 4 Minuten.
+
+## Zeitplan
+
+Der Cronjob läuft zu folgenden Zeiten:
+- 00:00, 00:04, 00:08, 00:12, ...
+- 01:00, 01:04, 01:08, 01:12, ...
+- etc.
+
+Um ihn z.B. immer 1 Minute nach dem 5-Minuten-Schritt zu starten:
+```cron
+1,6,11,16,21,26,31,36,41,46,51,56 * * * * /pfad/zum/script
+```
+
+## Logs überwachen
+
+```bash
+# Live-Logs anzeigen
+tail -f upload.log
+
+# Letzte Uploads anzeigen
+tail -20 upload.log
+```
+
+## iframe in bestehende Webseite einbauen
+
+```html
+
+```
+
+## Fehlerbehebung
+
+### Upload schlägt fehl
+```bash
+# SSH-Verbindung testen
+ssh user@ihr-server.de
+
+# rsync manuell testen
+rsync -avz frontend/dist/ user@ihr-server.de:/var/www/html/wetterstation/
+```
+
+### API nicht erreichbar
+```bash
+# API-Status prüfen
+curl http://localhost:8000/health
+```
+
+### Cronjob läuft nicht
+```bash
+# Cronjob-Log prüfen
+tail -f upload.log
+
+# Cronjobs anzeigen
+crontab -l
+
+# Script manuell ausführen
+python generate-static.py
+```
+
+## Cronjob entfernen
+
+```bash
+crontab -e
+# Zeile mit generate-static.py löschen
+```
+
+## Verzeichnisstruktur auf dem Server
+
+Nach dem Upload sollte der Server folgende Struktur haben:
+```
+/var/www/html/wetterstation/
+├── index.html (mit eingebetteten Daten)
+├── assets/
+│ ├── index-xxx.js
+│ └── index-xxx.css
+└── ...
+```
diff --git a/frontend/src/App-static.jsx b/frontend/src/App-static.jsx
new file mode 100644
index 0000000..df54f0e
--- /dev/null
+++ b/frontend/src/App-static.jsx
@@ -0,0 +1,59 @@
+import { useState, useEffect } from 'react'
+import WeatherDashboard from './components/WeatherDashboard'
+import './App.css'
+
+function App() {
+ const [weatherData, setWeatherData] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [lastUpdate, setLastUpdate] = useState(null)
+
+ useEffect(() => {
+ // Prüfe ob eingebettete Daten vorhanden sind
+ if (window.__WEATHER_DATA__) {
+ setWeatherData(window.__WEATHER_DATA__)
+ setLastUpdate(new Date())
+ setLoading(false)
+ } else {
+ setError('Keine Wetterdaten verfügbar')
+ setLoading(false)
+ }
+ }, [])
+
+ if (loading) {
+ return (
+
+
+
Lade Wetterdaten...
+
+ )
+ }
+
+ if (error) {
+ return (
+
+
Fehler beim Laden der Daten
+
{error}
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+export default App
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 1d80d99..df54f0e 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -8,63 +8,16 @@ function App() {
const [error, setError] = useState(null)
const [lastUpdate, setLastUpdate] = useState(null)
- const fetchWeatherData = async () => {
- try {
- const apiUrl = import.meta.env.VITE_API_URL || '/api'
- const response = await fetch(`${apiUrl}/weather/history?hours=24`)
-
- if (!response.ok) {
- throw new Error('Fehler beim Laden der Daten')
- }
-
- const data = await response.json()
- setWeatherData(data)
+ useEffect(() => {
+ // Prüfe ob eingebettete Daten vorhanden sind
+ if (window.__WEATHER_DATA__) {
+ setWeatherData(window.__WEATHER_DATA__)
setLastUpdate(new Date())
- setError(null)
- } catch (err) {
- setError(err.message)
- console.error('Fehler beim Laden der Wetterdaten:', err)
- } finally {
+ setLoading(false)
+ } else {
+ setError('Keine Wetterdaten verfügbar')
setLoading(false)
}
- }
-
- useEffect(() => {
- fetchWeatherData()
-
- // Berechne Zeit bis zum nächsten 5-Min-Schritt + 1 Minute
- const scheduleNextRefresh = () => {
- const now = new Date()
- const minutes = now.getMinutes()
- const seconds = now.getSeconds()
- const milliseconds = now.getMilliseconds()
-
- // Nächster 5-Minuten-Schritt
- const nextFiveMinStep = Math.ceil(minutes / 5) * 5
- // Plus 1 Minute
- const targetMinute = (nextFiveMinStep + 1) % 60
-
- let targetTime = new Date(now)
- targetTime.setMinutes(targetMinute, 0, 0)
-
- // Wenn die Zielzeit in der Vergangenheit liegt, füge eine Stunde hinzu
- if (targetTime <= now) {
- targetTime.setHours(targetTime.getHours() + 1)
- }
-
- const timeUntilRefresh = targetTime - now
-
- console.log(`Nächster Refresh: ${targetTime.toLocaleTimeString('de-DE')} (in ${Math.round(timeUntilRefresh / 1000)}s)`)
-
- return setTimeout(() => {
- fetchWeatherData()
- scheduleNextRefresh()
- }, timeUntilRefresh)
- }
-
- const timeout = scheduleNextRefresh()
-
- return () => clearTimeout(timeout)
}, [])
if (loading) {
@@ -81,7 +34,6 @@ function App() {
Fehler beim Laden der Daten
{error}
-
)
}
diff --git a/frontend/src/App.jsx_org b/frontend/src/App.jsx_org
new file mode 100644
index 0000000..1d80d99
--- /dev/null
+++ b/frontend/src/App.jsx_org
@@ -0,0 +1,107 @@
+import { useState, useEffect } from 'react'
+import WeatherDashboard from './components/WeatherDashboard'
+import './App.css'
+
+function App() {
+ const [weatherData, setWeatherData] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [lastUpdate, setLastUpdate] = useState(null)
+
+ const fetchWeatherData = async () => {
+ try {
+ const apiUrl = import.meta.env.VITE_API_URL || '/api'
+ const response = await fetch(`${apiUrl}/weather/history?hours=24`)
+
+ if (!response.ok) {
+ throw new Error('Fehler beim Laden der Daten')
+ }
+
+ const data = await response.json()
+ setWeatherData(data)
+ setLastUpdate(new Date())
+ setError(null)
+ } catch (err) {
+ setError(err.message)
+ console.error('Fehler beim Laden der Wetterdaten:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ fetchWeatherData()
+
+ // Berechne Zeit bis zum nächsten 5-Min-Schritt + 1 Minute
+ const scheduleNextRefresh = () => {
+ const now = new Date()
+ const minutes = now.getMinutes()
+ const seconds = now.getSeconds()
+ const milliseconds = now.getMilliseconds()
+
+ // Nächster 5-Minuten-Schritt
+ const nextFiveMinStep = Math.ceil(minutes / 5) * 5
+ // Plus 1 Minute
+ const targetMinute = (nextFiveMinStep + 1) % 60
+
+ let targetTime = new Date(now)
+ targetTime.setMinutes(targetMinute, 0, 0)
+
+ // Wenn die Zielzeit in der Vergangenheit liegt, füge eine Stunde hinzu
+ if (targetTime <= now) {
+ targetTime.setHours(targetTime.getHours() + 1)
+ }
+
+ const timeUntilRefresh = targetTime - now
+
+ console.log(`Nächster Refresh: ${targetTime.toLocaleTimeString('de-DE')} (in ${Math.round(timeUntilRefresh / 1000)}s)`)
+
+ return setTimeout(() => {
+ fetchWeatherData()
+ scheduleNextRefresh()
+ }, timeUntilRefresh)
+ }
+
+ const timeout = scheduleNextRefresh()
+
+ return () => clearTimeout(timeout)
+ }, [])
+
+ if (loading) {
+ return (
+
+
+
Lade Wetterdaten...
+
+ )
+ }
+
+ if (error) {
+ return (
+
+
Fehler beim Laden der Daten
+
{error}
+
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+export default App
diff --git a/frontend/src/components/WeatherDashboard.css b/frontend/src/components/WeatherDashboard.css
index 4c13c84..c991f10 100644
--- a/frontend/src/components/WeatherDashboard.css
+++ b/frontend/src/components/WeatherDashboard.css
@@ -1,18 +1,19 @@
.dashboard {
width: 100%;
+ max-width: 795px;
}
.current-values {
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 1rem;
- margin-bottom: 2rem;
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
}
.value-card {
background: white;
- padding: 1.5rem;
- border-radius: 12px;
+ padding: 1rem;
+ border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
@@ -20,13 +21,13 @@
}
.value-label {
- font-size: 0.9rem;
+ font-size: 0.8rem;
color: #666;
font-weight: 500;
}
.value-number {
- font-size: 2rem;
+ font-size: 1.5rem;
font-weight: bold;
color: #333;
}
@@ -34,47 +35,23 @@
.charts-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
- gap: 1.5rem;
+ gap: 1rem;
}
.chart-container {
background: white;
- padding: 1.5rem;
- border-radius: 12px;
+ padding: 1rem;
+ border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.chart-container h3 {
- margin-bottom: 1rem;
+ margin-bottom: 0.75rem;
color: #333;
- font-size: 1.2rem;
+ font-size: 1rem;
}
.chart-wrapper {
- height: 300px;
+ height: 250px;
position: relative;
}
-
-@media (max-width: 1024px) {
- .charts-grid {
- grid-template-columns: 1fr;
- }
-
- .chart-container.chart-full {
- grid-column: 1;
- }
-}
-
-@media (max-width: 768px) {
- .current-values {
- grid-template-columns: repeat(2, 1fr);
- }
-
- .value-number {
- font-size: 1.5rem;
- }
-
- .chart-wrapper {
- height: 250px;
- }
-}
diff --git a/frontend/src/components/WeatherDashboard.jsx b/frontend/src/components/WeatherDashboard.jsx
index 91c40a6..48095e5 100644
--- a/frontend/src/components/WeatherDashboard.jsx
+++ b/frontend/src/components/WeatherDashboard.jsx
@@ -93,7 +93,7 @@ const WeatherDashboard = ({ data }) => {
data: sortedData.map(item => item.temperature),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
- fill: true,
+ fill: 'start',
tension: 0.4,
}
]
@@ -254,9 +254,11 @@ const WeatherDashboard = ({ data }) => {
label: 'Windrichtung (°)',
data: sortedData.map(item => item.wind_dir),
borderColor: 'rgb(255, 205, 86)',
- backgroundColor: 'rgba(255, 205, 86, 0.1)',
- fill: true,
- tension: 0,
+ backgroundColor: 'rgb(255, 205, 86)',
+ pointRadius: 4,
+ pointHoverRadius: 6,
+ showLine: false,
+ fill: false,
}
]
}
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 03d414e..8bf5f9c 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
+ base: './',
server: {
host: '0.0.0.0',
port: 3000,
diff --git a/generate-static.py b/generate-static.py
new file mode 100755
index 0000000..33d5cf7
--- /dev/null
+++ b/generate-static.py
@@ -0,0 +1,112 @@
+#!/Users/rxf/Projekte/wetterstation/.venv/bin/python
+"""
+Generiert statische HTML-Dateien mit aktuellen Wetterdaten und lädt sie auf den Server hoch
+"""
+import json
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+import requests
+from datetime import datetime
+
+# Konfiguration
+API_URL = "http://localhost:8000"
+FRONTEND_DIR = Path(__file__).parent / "frontend"
+DIST_DIR = FRONTEND_DIR / "dist"
+REMOTE_SERVER = "ssh-310927-rxf@sternwarte-welzheim.de" # SSH-Zugang zum Webserver
+REMOTE_PATH = "webroot/wetter/wetterstation" # Pfad auf dem Webserver
+
+def fetch_weather_data():
+ """Holt aktuelle Wetterdaten von der API"""
+ try:
+ response = requests.get(f"{API_URL}/weather/history?hours=24", timeout=10)
+ response.raise_for_status()
+ return response.json()
+ except Exception as e:
+ print(f"Fehler beim Laden der Daten: {e}")
+ sys.exit(1)
+
+def build_frontend():
+ """Baut das Frontend"""
+ print("Baue Frontend...")
+ try:
+ subprocess.run(
+ ["npm", "run", "build"],
+ cwd=FRONTEND_DIR,
+ check=True,
+ capture_output=True
+ )
+ print("✓ Frontend gebaut")
+ except subprocess.CalledProcessError as e:
+ print(f"Fehler beim Build: {e.stderr.decode()}")
+ sys.exit(1)
+
+def inject_data_into_html(weather_data):
+ """Fügt Wetterdaten in die index.html ein"""
+ index_file = DIST_DIR / "index.html"
+
+ if not index_file.exists():
+ print("index.html nicht gefunden!")
+ sys.exit(1)
+
+ with open(index_file, 'r', encoding='utf-8') as f:
+ html_content = f.read()
+
+ # Füge Daten als inline Script vor dem schließenden