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 ( +
+
+

🌤️ Wetterstation

+ {lastUpdate && ( +

+ Letzte Aktualisierung: {lastUpdate.toLocaleTimeString('de-DE')} +

+ )} +
+ +
+ +
+
+ ) +} + +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 ( +
+
+

🌤️ Wetterstation

+ {lastUpdate && ( +

+ Letzte Aktualisierung: {lastUpdate.toLocaleTimeString('de-DE')} +

+ )} +
+ +
+ +
+
+ ) +} + +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 Tag ein + data_script = f""" + + """ + + html_content = html_content.replace('', data_script) + + with open(index_file, 'w', encoding='utf-8') as f: + f.write(html_content) + + print(f"✓ Daten in HTML eingebettet ({len(weather_data)} Datensätze)") + +def upload_to_server(): + """Lädt die Dateien per rsync auf den Server""" + print(f"Lade Dateien auf {REMOTE_SERVER}...") + + try: + subprocess.run([ + "rsync", + "-avz", + "--delete", + f"{DIST_DIR}/", + f"{REMOTE_SERVER}:{REMOTE_PATH}/" + ], check=True) + print(f"✓ Upload erfolgreich") + except subprocess.CalledProcessError as e: + print(f"Fehler beim Upload: {e}") + sys.exit(1) + +def main(): + print(f"=== Wetterstation Static Generator ===") + print(f"Start: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + + # 1. Daten von API holen + print("Lade Wetterdaten...") + weather_data = fetch_weather_data() + print(f"✓ {len(weather_data)} Datensätze geladen\n") + + # 2. Frontend bauen + build_frontend() + print() + + # 3. Daten in HTML einbetten + inject_data_into_html(weather_data) + print() + + # 4. Auf Server hochladen + upload_to_server() + + print(f"\n✓ Fertig: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + +if __name__ == "__main__": + main() diff --git a/setup-cronjob.sh b/setup-cronjob.sh new file mode 100755 index 0000000..2d7cfe7 --- /dev/null +++ b/setup-cronjob.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Setup Cronjob für automatisches Upload alle 4 Minuten + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON_SCRIPT="$SCRIPT_DIR/generate-static.py" +VENV_PYTHON="$SCRIPT_DIR/.venv/bin/python" + +# Cronjob-Eintrag +CRON_ENTRY="*/4 * * * * $VENV_PYTHON $PYTHON_SCRIPT >> $SCRIPT_DIR/upload.log 2>&1" + +echo "=== Cronjob Setup ===" +echo "" +echo "Dieser Cronjob wird alle 4 Minuten ausgeführt:" +echo "$CRON_ENTRY" +echo "" +echo "Möchten Sie den Cronjob jetzt installieren? (j/n)" +read -r response + +if [[ "$response" =~ ^[Jj]$ ]]; then + # Prüfe ob Cronjob bereits existiert + if crontab -l 2>/dev/null | grep -q "$PYTHON_SCRIPT"; then + echo "Cronjob existiert bereits!" + else + # Füge Cronjob hinzu + (crontab -l 2>/dev/null; echo "$CRON_ENTRY") | crontab - + echo "✓ Cronjob installiert" + fi + + echo "" + echo "Cronjobs:" + crontab -l | grep generate-static + echo "" + echo "Logs finden Sie in: $SCRIPT_DIR/upload.log" +else + echo "Abgebrochen" + echo "" + echo "Um den Cronjob manuell hinzuzufügen:" + echo "1. crontab -e" + echo "2. Folgende Zeile einfügen:" + echo " $CRON_ENTRY" +fi