Grafik 2 spaltig auch bein iFrame
Übertragen nach sternwarte funktioniert
This commit is contained in:
155
DEPLOY.md
Normal file
155
DEPLOY.md
Normal file
@@ -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
|
||||||
|
<iframe
|
||||||
|
src="https://ihre-domain.de/wetterstation/"
|
||||||
|
width="100%"
|
||||||
|
height="1200"
|
||||||
|
frameborder="0"
|
||||||
|
style="border: none;">
|
||||||
|
</iframe>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
59
frontend/src/App-static.jsx
Normal file
59
frontend/src/App-static.jsx
Normal file
@@ -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 (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<header className="app-header">
|
||||||
|
<h1>🌤️ Wetterstation</h1>
|
||||||
|
{lastUpdate && (
|
||||||
|
<p className="last-update">
|
||||||
|
Letzte Aktualisierung: {lastUpdate.toLocaleTimeString('de-DE')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="app-main">
|
||||||
|
<WeatherDashboard data={weatherData} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
@@ -8,63 +8,16 @@ function App() {
|
|||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [lastUpdate, setLastUpdate] = useState(null)
|
const [lastUpdate, setLastUpdate] = useState(null)
|
||||||
|
|
||||||
const fetchWeatherData = async () => {
|
useEffect(() => {
|
||||||
try {
|
// Prüfe ob eingebettete Daten vorhanden sind
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || '/api'
|
if (window.__WEATHER_DATA__) {
|
||||||
const response = await fetch(`${apiUrl}/weather/history?hours=24`)
|
setWeatherData(window.__WEATHER_DATA__)
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Fehler beim Laden der Daten')
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
setWeatherData(data)
|
|
||||||
setLastUpdate(new Date())
|
setLastUpdate(new Date())
|
||||||
setError(null)
|
setLoading(false)
|
||||||
} catch (err) {
|
} else {
|
||||||
setError(err.message)
|
setError('Keine Wetterdaten verfügbar')
|
||||||
console.error('Fehler beim Laden der Wetterdaten:', err)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
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) {
|
if (loading) {
|
||||||
@@ -81,7 +34,6 @@ function App() {
|
|||||||
<div className="error-container">
|
<div className="error-container">
|
||||||
<h2>Fehler beim Laden der Daten</h2>
|
<h2>Fehler beim Laden der Daten</h2>
|
||||||
<p>{error}</p>
|
<p>{error}</p>
|
||||||
<button onClick={fetchWeatherData}>Erneut versuchen</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
107
frontend/src/App.jsx_org
Normal file
107
frontend/src/App.jsx_org
Normal file
@@ -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 (
|
||||||
|
<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>
|
||||||
|
<button onClick={fetchWeatherData}>Erneut versuchen</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<header className="app-header">
|
||||||
|
<h1>🌤️ Wetterstation</h1>
|
||||||
|
{lastUpdate && (
|
||||||
|
<p className="last-update">
|
||||||
|
Letzte Aktualisierung: {lastUpdate.toLocaleTimeString('de-DE')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="app-main">
|
||||||
|
<WeatherDashboard data={weatherData} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
.dashboard {
|
.dashboard {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 795px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.current-values {
|
.current-values {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-card {
|
.value-card {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -20,13 +21,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.value-label {
|
.value-label {
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-number {
|
.value-number {
|
||||||
font-size: 2rem;
|
font-size: 1.5rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
@@ -34,47 +35,23 @@
|
|||||||
.charts-grid {
|
.charts-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 1.5rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 1.5rem;
|
padding: 1rem;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container h3 {
|
.chart-container h3 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 0.75rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 1.2rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-wrapper {
|
.chart-wrapper {
|
||||||
height: 300px;
|
height: 250px;
|
||||||
position: relative;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
data: sortedData.map(item => item.temperature),
|
data: sortedData.map(item => item.temperature),
|
||||||
borderColor: 'rgb(255, 99, 132)',
|
borderColor: 'rgb(255, 99, 132)',
|
||||||
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
||||||
fill: true,
|
fill: 'start',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -254,9 +254,11 @@ const WeatherDashboard = ({ data }) => {
|
|||||||
label: 'Windrichtung (°)',
|
label: 'Windrichtung (°)',
|
||||||
data: sortedData.map(item => item.wind_dir),
|
data: sortedData.map(item => item.wind_dir),
|
||||||
borderColor: 'rgb(255, 205, 86)',
|
borderColor: 'rgb(255, 205, 86)',
|
||||||
backgroundColor: 'rgba(255, 205, 86, 0.1)',
|
backgroundColor: 'rgb(255, 205, 86)',
|
||||||
fill: true,
|
pointRadius: 4,
|
||||||
tension: 0,
|
pointHoverRadius: 6,
|
||||||
|
showLine: false,
|
||||||
|
fill: false,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
base: './',
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 3000,
|
port: 3000,
|
||||||
|
|||||||
112
generate-static.py
Executable file
112
generate-static.py
Executable file
@@ -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 </body> Tag ein
|
||||||
|
data_script = f"""
|
||||||
|
<script>
|
||||||
|
// Eingebettete Wetterdaten - generiert am {datetime.now().isoformat()}
|
||||||
|
window.__WEATHER_DATA__ = {json.dumps(weather_data)};
|
||||||
|
</script>
|
||||||
|
</body>"""
|
||||||
|
|
||||||
|
html_content = html_content.replace('</body>', 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()
|
||||||
41
setup-cronjob.sh
Executable file
41
setup-cronjob.sh
Executable file
@@ -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
|
||||||
Reference in New Issue
Block a user