docs: STATE.md Updates - FuenfVorAcht Review-Schritt entfernt, Redakteur RSS-Integration, Flugpreisscanner
- fuenfvoracht/STATE.md: Review-Flow entfernt, direkt approved, neuer Changelog - arakava-news/STATE.md: Aktualisiert - redax-wp/STATE.md + src/app.py: Aktualisiert - flugpreisscanner/STATE.md: Aktualisiert - infrastructure/STATE.md: Aktualisiert - fuenfvoracht READMEs und Kurzuebersichten hinzugefuegt Made-with: Cursor
This commit is contained in:
parent
5e1397edee
commit
e607b1fba0
16 changed files with 2619 additions and 12 deletions
|
|
@ -24,9 +24,14 @@ $8.88 verbleibend
|
||||||
|
|
||||||
## URLs
|
## URLs
|
||||||
- Blog: https://arakava-news-2.orbitalo.net
|
- Blog: https://arakava-news-2.orbitalo.net
|
||||||
- Admin: https://arakava-news-2.orbitalo.net/wp-admin (admin / eJIyhW0p5PFacjvvKGufKeXS)
|
- Admin: https://arakava-news-2.orbitalo.net/wp-admin (admin / eJIyhW0p5PFacjvvKGufKeXS) — ⚠️ nur via Tailscale erreichbar!
|
||||||
- RSS Manager: http://100.113.244.101:8080 (admin / astral66)
|
- RSS Manager: https://rss-manager.orbitalo.net (astral66) — Cloudflare Tunnel
|
||||||
|
- RSS Manager (Tailscale): http://100.113.244.101:8080
|
||||||
- Matomo: https://matomo.orbitalo.net (admin / astral66)
|
- Matomo: https://matomo.orbitalo.net (admin / astral66)
|
||||||
|
- Google Search Console: https://search.google.com/search-console?resource_id=https://arakava-news-2.orbitalo.net/
|
||||||
|
- Google-Konto: Mila.Dek1968@gmail.com / Sicherlich-neue-658@1
|
||||||
|
- Verifizierung: HTML-Datei (google248e38a1e3540863.html im WP-Root)
|
||||||
|
- Sitemap: https://arakava-news-2.orbitalo.net/sitemap_index.xml
|
||||||
|
|
||||||
## Container
|
## Container
|
||||||
| CT | Dienst | Tailscale |
|
| CT | Dienst | Tailscale |
|
||||||
|
|
@ -54,10 +59,58 @@ $8.88 verbleibend
|
||||||
| 16 | Antispiegel | 08:30/14:30/20:30 |
|
| 16 | Antispiegel | 08:30/14:30/20:30 |
|
||||||
| 17 | Riehle News | 09:00 Uhr |
|
| 17 | Riehle News | 09:00 Uhr |
|
||||||
|
|
||||||
|
## SEO (Rank Math)
|
||||||
|
- Plugin: Rank Math SEO (CT 101, WordPress)
|
||||||
|
- Sitemap: /sitemap_index.xml (automatisch, alle Posts)
|
||||||
|
- Open Graph + Twitter Cards: aktiv für alle Beiträge
|
||||||
|
- wp-login.php: öffentlich gesperrt (.htaccess → 403), nur Tailscale (100.x.x.x)
|
||||||
|
- Google-Verifizierung: /google248e38a1e3540863.html
|
||||||
|
- Phishing-Review: beantragt 27.02.2026 (wp-login war öffentlich sichtbar)
|
||||||
|
|
||||||
|
## Eigene Artikel (Serie: ESP32 Heizung)
|
||||||
|
| Teil | Status | Titel | WP Post-ID |
|
||||||
|
|------|--------|-------|------------|
|
||||||
|
| 1 | veröffentlicht | Vier Heizungen, ein Pufferspeicher: Warum ich meine Heizung smart mache | 1209 |
|
||||||
|
| 2 | geplant 06.03. | Hardware: ESP32, DS18B20-Sensoren, Verkabelung | — |
|
||||||
|
| 3 | geplant | Software: InfluxDB, Grafana-Dashboard | — |
|
||||||
|
| 4 | geplant | Display-Projekt: Layout, Wetterprognose, Kostenrechnung | — |
|
||||||
|
| 5 | geplant | Bonus: 2.8-Zoll-ESP32 als WLAN-Scanner | — |
|
||||||
|
|
||||||
|
### Quellen für Artikelinhalt
|
||||||
|
- Entwurf Teil 1: `/root/homelab-brain/arakava-news/artikel/esp32-serie-teil1-entwurf.md`
|
||||||
|
- Technische Doku (Display-Layouts, Regeln, MQTT, Hardware): `pct exec 999 -- cat /root/ESP32-Heizung-Projekt.md`
|
||||||
|
- Hardware + Pin-Belegung: `/root/homelab-brain/esp32/PLAN.md`
|
||||||
|
- Smart Home Kontext (ioBroker, Grafana, InfluxDB): `/root/homelab-brain/smart-home/STATE.md`
|
||||||
|
- Grafiken (Header + Schema): WP Media ID 1207, 1208
|
||||||
|
- Display-Mockups: Aus ASCII-Art in Doku generieren (noch offen)
|
||||||
|
- Heizung: 4 Quellen (Solar, Holzvergaser, Luft-Luft-WP, Ölkessel), 1800L Puffer, Luft-Luft-WP NICHT am Puffer!
|
||||||
|
|
||||||
|
### Seafile-Assets (lokal kopiert)
|
||||||
|
Quelle: `Seafile → Nextcloud-Migration/Meine Dateien/Server/ESP32 Projekt`
|
||||||
|
Lokal: `/root/homelab-brain/arakava-news/artikel/seafile-assets/`
|
||||||
|
- `Fließschaltbild v4 HTML.md` — Animiertes interaktives SVG-Schaltbild (Solar/Holz/Öl, Energieflüsse animiert, Buttons für Betriebsmodi) → in WP als Custom HTML Block einbettbar
|
||||||
|
- `Fließschaltbild von Clode 4.6 als HTML.md` — Vereinfachte Version
|
||||||
|
- `Sensoren und Bilder.md` — Detaillierte Sensor-Planung (17 Sensoren: 6 Erzeuger, 2 Heizung, 4 HK-Rückläufe, 5 Puffer-Zonen)
|
||||||
|
- `Kombipufferspeicher.webp` — Grafik Pufferspeicher
|
||||||
|
|
||||||
|
### Seafile: Echte Fotos der Heizungsanlage
|
||||||
|
Pfad: `Seafile → Nextcloud-Migration/Meine Dateien/Heizung und Lüftung/Bilder`
|
||||||
|
- Mein HV.JPG — Holzvergaser
|
||||||
|
- PT100 im Abgasrohr des Holzvergasers.JPG — Abgassensor
|
||||||
|
- Pufferspeicher mit Isolierung.JPG — Puffertank
|
||||||
|
- Puffertank ohne Isolierung.jpg — Tank nackt
|
||||||
|
- Solarwärmetauscher.jpg — Solarthermie
|
||||||
|
- Heizkreisverteiler vor dem Umbau.jpg — Verteiler
|
||||||
|
- Siemens Logo und Solarregler mit KWH Anzeige.JPG — Steuerung
|
||||||
|
|
||||||
## Code (CT 109: /opt/rss-manager/)
|
## Code (CT 109: /opt/rss-manager/)
|
||||||
poster.py, scheduler.py, app.py, db.py
|
poster.py, scheduler.py, app.py, db.py
|
||||||
|
|
||||||
## Änderungshistorie
|
## Änderungshistorie
|
||||||
|
- 27.02.2026: Google Search Console eingerichtet + Sitemap eingereicht
|
||||||
|
- 27.02.2026: Rank Math SEO installiert (Open Graph, Sitemap, Meta Tags)
|
||||||
|
- 27.02.2026: wp-login.php öffentlich gesperrt (Anti-Phishing)
|
||||||
|
- 27.02.2026: ESP32-Heizung Artikel Teil 1 veröffentlicht (Post 1209)
|
||||||
- 24.02.2026: Scheduler Lock gegen Doppelstarts
|
- 24.02.2026: Scheduler Lock gegen Doppelstarts
|
||||||
- 24.02.2026: Telegram auf HTML-Modus (Sonderzeichen-Fix)
|
- 24.02.2026: Telegram auf HTML-Modus (Sonderzeichen-Fix)
|
||||||
- 24.02.2026: Werbeartikel-Blacklist (Anzeige:, Sponsored, etc.)
|
- 24.02.2026: Werbeartikel-Blacklist (Anzeige:, Sponsored, etc.)
|
||||||
|
|
|
||||||
138
arakava-news/artikel/esp32-serie-teil1-entwurf.md
Normal file
138
arakava-news/artikel/esp32-serie-teil1-entwurf.md
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
# Vier Heizungen, ein Pufferspeicher: Warum ich meine Heizung smart mache
|
||||||
|
|
||||||
|
**Serie: Mein Smart-Home-Heizungsprojekt mit ESP32 — Teil 1 von 5**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Das Problem: Vier Wärmequellen, null Überblick
|
||||||
|
|
||||||
|
In meinem Haus stecken vier verschiedene Heizungen. Klingt nach Luxus — ist im Alltag eher Chaos:
|
||||||
|
|
||||||
|
- **Solarthermie** auf dem Dach — gratis, aber nur wenn die Sonne scheint
|
||||||
|
- **Holzvergaser** — billigste Wärme, aber ich muss selbst nachlegen
|
||||||
|
- **Ölkessel** — zuverlässiges Backup, aber teuer
|
||||||
|
- **Luft-Luft-Wärmepumpe** — heizt Räume direkt mit warmer Luft, ohne Umweg über Wasser
|
||||||
|
|
||||||
|
Die ersten drei speisen in denselben **Pufferspeicher** — einen 1800-Liter-Tank, der die Wärme zwischenspeichert und über Heizkörper ans Haus verteilt. Die Luft-Luft-Wärmepumpe läuft separat: Sie bläst warme Luft direkt in den Raum und hat mit dem Puffer nichts zu tun.
|
||||||
|
|
||||||
|
Im Prinzip eine clevere Kombi. In der Praxis bedeutet es: Ich renne ständig in den Keller, schaue auf Thermometer, drehe Ventile und frage mich, ob der Holzvergaser noch läuft, die Sonne gleich rauskommt — oder ob ich einfach die Luft-Luft-WP anwerfe und den Puffer in Ruhe lasse.
|
||||||
|
|
||||||
|
**Die zentrale Frage jeden Morgen:** Ölheizung anmachen, Luft-Luft-WP laufen lassen — oder darauf wetten, dass die Sonne reicht?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was mich das kostet (wenn ich falsch rate)
|
||||||
|
|
||||||
|
Ein Beispiel aus dem letzten Winter:
|
||||||
|
|
||||||
|
Dienstagmorgen, minus 3 Grad. Puffer steht bei 45°C. Die Wetter-App sagt „teils sonnig". Ich lasse den Ölkessel aus — soll die Sonne machen. Um 14 Uhr ist es immer noch bewölkt, der Puffer ist auf 35°C gefallen, das Haus wird kalt. Jetzt muss der Ölkessel ran — im ungünstigsten Moment, weil der Puffer so weit runtergekühlt ist.
|
||||||
|
|
||||||
|
**Ergebnis:** 8 Liter Heizöl verbrannt — rund 12 Euro an einem einzigen Tag. Die Solarthermie hat null beigetragen. Hätte ich morgens den Holzvergaser angeworfen, wäre das Holz für die gleiche Wärme bei vielleicht 3 Euro gelegen. Vierfacher Preisunterschied, nur weil ich falsch geraten habe.
|
||||||
|
|
||||||
|
Das ist kein Einzelfall. Die Kombination aus vier Wärmequellen ist nur dann sparsam, wenn man **zur richtigen Zeit die richtige Quelle** einschaltet. Manuell ist das Glücksspiel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die Idee: Ein ESP32 mit Display als Heizungs-Gehirn
|
||||||
|
|
||||||
|
Die Lösung ist überraschend günstig: Ein **ESP32-Mikrocontroller** mit einem **5-Zoll-Touchscreen** — zusammen für rund 30 Euro. Dazu acht Temperatursensoren (10 Euro) und etwas Kabel. Gesamtkosten: **unter 70 Euro**.
|
||||||
|
|
||||||
|
Was das kleine Gerät können soll:
|
||||||
|
|
||||||
|
1. **Alle Temperaturen live anzeigen** — Puffer oben/mitte/unten, Außentemperatur, Kollektor, Abgastemperatur
|
||||||
|
2. **Wetterprognose einbeziehen** — Wird es sonnig? Dann Öl sperren und auf Solar warten
|
||||||
|
3. **Automatisch entscheiden** — Nach fünf einfachen Regeln
|
||||||
|
4. **Ersparnisse berechnen** — Was hätte der Ölkessel gekostet? Was habe ich gespart?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die fünf Regeln — so einfach wie möglich
|
||||||
|
|
||||||
|
Nach monatelangem Beobachten meiner Heizung habe ich fünf Regeln destilliert, die fast jede Situation abdecken:
|
||||||
|
|
||||||
|
### Regel 1: Holzvergaser läuft? → Alles andere aus!
|
||||||
|
|
||||||
|
Der Holzvergaser ist die billigste Wärmequelle. Wenn er brennt, braucht niemand sonst zu laufen. Punkt.
|
||||||
|
|
||||||
|
### Regel 2: Sonne erwartet? → Morgens den Ölkessel sperren
|
||||||
|
|
||||||
|
Das ist die wichtigste Regel für die Geldbörse. Wenn die Wetterprognose mehr als 3 Sonnenstunden verspricht, bleibt der Ölkessel morgens gesperrt. Die Solarthermie übernimmt.
|
||||||
|
|
||||||
|
### Regel 3: Außentemperatur über +5°C? → Luft-Luft-WP erlaubt
|
||||||
|
|
||||||
|
Die Luft-Luft-Wärmepumpe heizt Räume direkt mit warmer Luft — ganz ohne Pufferspeicher. Ab +5°C aufwärts liefert sie einen guten COP (Leistungszahl) — sprich: aus 1 kWh Strom werden 3–4 kWh Wärme. Ideal als Ergänzung, wenn der Puffer gerade nicht nachgeladen werden muss.
|
||||||
|
|
||||||
|
### Regel 4: Außentemperatur unter +5°C? → Öl statt Luft-Luft-WP
|
||||||
|
|
||||||
|
Unter 5 Grad quält sich die Luft-Luft-WP — der COP sinkt auf 2 oder weniger, der Stromverbrauch steigt überproportional. Dann lieber den Ölkessel den Puffer laden lassen und die Räume über Heizkörper versorgen.
|
||||||
|
|
||||||
|
### Regel 5: Puffer unter 35°C? → Notstart!
|
||||||
|
|
||||||
|
Egal was die anderen Regeln sagen: Wenn der Puffer unter 35°C fällt, wird sofort die schnellste verfügbare Quelle gestartet. Kaltes Haus ist nicht verhandelbar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Die Prioritäten-Kette
|
||||||
|
|
||||||
|
Wenn keine Sonderregel greift, gilt diese Reihenfolge:
|
||||||
|
|
||||||
|
| Priorität | Quelle | Kosten ca. | Bedingung |
|
||||||
|
|-----------|--------|-----------|-----------|
|
||||||
|
| 1 | ☀️ Solarthermie | 0 €/kWh | Sonne scheint |
|
||||||
|
| 2 | 🪵 Holzvergaser | ~0,04 €/kWh | Manuell befeuern |
|
||||||
|
| 3 | 🌡️ Luft-Luft-WP | ~0,08 €/kWh | Außen > +5°C |
|
||||||
|
| 4 | 🛢️ Ölkessel | ~0,12 €/kWh | Immer verfügbar |
|
||||||
|
|
||||||
|
Die Logik: **Gratis vor billig vor effizient vor teuer.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was auf dem Display zu sehen sein wird
|
||||||
|
|
||||||
|
Das 5-Zoll-Display zeigt auf einen Blick:
|
||||||
|
|
||||||
|
- **Oben:** Außentemperatur und Datum
|
||||||
|
- **Mitte:** Wetterprognose mit Empfehlung (z.B. „5 Stunden Sonne erwartet → Öl bleibt aus")
|
||||||
|
- **Puffer-Balken:** Wie voll ist der Speicher? Temperatur oben, mitte, unten
|
||||||
|
- **Status aller vier Quellen:** Welche läuft, welche wartet, welche ist gesperrt?
|
||||||
|
- **Unten:** Tagesersparnis in Euro — was hätte der Ölkessel alleine gekostet?
|
||||||
|
|
||||||
|
*[Grafik: Display-Layout mit Pufferbalken und Statusanzeige — folgt in Teil 2]*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was kommt in den nächsten Teilen?
|
||||||
|
|
||||||
|
| Teil | Thema |
|
||||||
|
|------|-------|
|
||||||
|
| **Teil 2** | Hardware: ESP32-8048S050, DS18B20-Sensoren, Verkabelung — die komplette Einkaufsliste unter 70€ |
|
||||||
|
| **Teil 3** | Software: Sensordaten sammeln, InfluxDB, Grafana-Dashboard — der digitale Blick in den Keller |
|
||||||
|
| **Teil 4** | Das Display-Projekt: Layout, Wetterprognose, Kostenrechnung live auf 5 Zoll |
|
||||||
|
| **Teil 5** | Bonus: Ein 2.8-Zoll-ESP32 als WLAN-Scanner und Diagnose-Tool |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fazit: 70 Euro gegen Bauchgefühl
|
||||||
|
|
||||||
|
Bisher entscheide ich nach Bauchgefühl, welche Heizung laufen soll. Manchmal liege ich richtig, oft nicht. Ein ESP32 mit acht Temperatursensoren und einer Wetterprognose kann das besser — für weniger als eine Tankfüllung Heizöl.
|
||||||
|
|
||||||
|
Im nächsten Teil geht es ans Eingemachte: Welche Hardware genau, wo bestellen, und wie sieht die Verkabelung aus. **Teil 2 erscheint am 6. März.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Dieser Artikel ist Teil 1 der Serie „Mein Smart-Home-Heizungsprojekt mit ESP32". Die Serie dokumentiert den kompletten Aufbau einer intelligenten Heizungssteuerung mit vier Wärmequellen — von der Idee bis zum fertigen Display.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Tags:** ESP32, Smart Home, Heizung, Pufferspeicher, Solarthermie, Holzvergaser, Wärmepumpe, DIY, InfluxDB, Grafana
|
||||||
|
**Kategorie:** Technik / Smart Home
|
||||||
|
**Lesezeit:** ca. 6 Minuten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Grafik-Ideen für diesen Artikel
|
||||||
|
|
||||||
|
1. **Titelbild:** Heizungskeller mit Pufferspeicher, vier farbige Rohre (Solar=orange, Holz=braun, WP=blau, Öl=rot), ESP32-Display im Vordergrund
|
||||||
|
2. **Schaubild:** Die vier Wärmequellen mit Pfeilen zum Pufferspeicher — Prioritäten-Kette als Infografik
|
||||||
|
3. **Display-Mockup:** Screenshot des geplanten 5-Zoll-Layouts (Pufferbalken, Statusanzeige, Wetterprognose)
|
||||||
|
4. **Kostenvergleich:** Balkendiagramm — Heizkosten mit vs. ohne smartes System (geschätzt)
|
||||||
488
arakava-news/artikel/seafile-assets/Fließschaltbild v4 HTML.md
Normal file
488
arakava-news/artikel/seafile-assets/Fließschaltbild v4 HTML.md
Normal file
|
|
@ -0,0 +1,488 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Heizungsanlage - Hydraulikplan</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{
|
||||||
|
background:#111820;color:#ddd;
|
||||||
|
font-family:'Segoe UI',Arial,sans-serif;
|
||||||
|
display:flex;flex-direction:column;align-items:center;
|
||||||
|
padding:15px;
|
||||||
|
}
|
||||||
|
h1{font-size:1.3em;margin:8px 0 4px}
|
||||||
|
.sub{color:#778;font-size:.85em;margin-bottom:10px}
|
||||||
|
.btns{display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap;justify-content:center}
|
||||||
|
.btns button{
|
||||||
|
padding:10px 20px;border-radius:6px;border:2px solid #333;
|
||||||
|
background:#181e26;color:#bbb;cursor:pointer;font-size:.95em;
|
||||||
|
transition:.3s;
|
||||||
|
}
|
||||||
|
.btns button:hover{background:#222a35}
|
||||||
|
.btns button.on-sol{border-color:#e6a800;color:#ffd33d;background:#1f1a00}
|
||||||
|
.btns button.on-holz{border-color:#e07030;color:#f0883e;background:#1f1000}
|
||||||
|
.btns button.on-oel{border-color:#4090e0;color:#58a6ff;background:#001828}
|
||||||
|
.btns button.on-all{border-color:#a070f0;color:#c8a0ff;background:#150028}
|
||||||
|
.wrap{
|
||||||
|
width:100%;max-width:1100px;
|
||||||
|
background:#0d1117;border:1px solid #222;border-radius:10px;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
svg{width:100%;height:auto;display:block}
|
||||||
|
|
||||||
|
@keyframes fliess{from{stroke-dashoffset:30}to{stroke-dashoffset:0}}
|
||||||
|
@keyframes fliessR{from{stroke-dashoffset:0}to{stroke-dashoffset:30}}
|
||||||
|
.flow{
|
||||||
|
fill:none;stroke-linecap:round;stroke-linejoin:round;
|
||||||
|
stroke-dasharray:10,20;animation:fliess .8s linear infinite;
|
||||||
|
opacity:0;transition:opacity .6s;
|
||||||
|
}
|
||||||
|
.flow.rev{animation:fliessR .8s linear infinite}
|
||||||
|
.flow.on{opacity:1}
|
||||||
|
.comp{transition:opacity .4s}
|
||||||
|
.comp.dim{opacity:.2}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Heizungsanlage - Hydraulikplan</h1>
|
||||||
|
<p class="sub">Klicke einen Betriebsmodus um die Energiefluesse zu sehen</p>
|
||||||
|
|
||||||
|
<div class="btns">
|
||||||
|
<button id="b-sol" onclick="setMode('sol')">Solar</button>
|
||||||
|
<button id="b-holz" onclick="setMode('holz')">Holzvergaser</button>
|
||||||
|
<button id="b-oel" onclick="setMode('oel')">Oelkessel</button>
|
||||||
|
<button id="b-all" onclick="setMode('all')">Alle Quellen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<svg viewBox="0 0 1000 880" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="tempG" x1="0" y1="1" x2="0" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#112244" stop-opacity=".35"/>
|
||||||
|
<stop offset="100%" stop-color="#441111" stop-opacity=".35"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- ======== HINTERGRUND-ROHRE ======== -->
|
||||||
|
<g stroke="#1e242c" stroke-width="5" fill="none"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Öl-Kessel VL: Erzeuger → oberer Speicheranschluss -->
|
||||||
|
<path d="M130,155 L130,210 L320,210 L320,265"/>
|
||||||
|
<!-- Öl-Kessel RL: unterer Speicheranschluss → Erzeuger -->
|
||||||
|
<path d="M200,155 L200,225 L370,225 L370,580"/>
|
||||||
|
|
||||||
|
<!-- Holzvergaser VL: Erzeuger → oberer Speicheranschluss -->
|
||||||
|
<path d="M430,155 L430,210 L490,210 L490,265"/>
|
||||||
|
<!-- Holzvergaser RL: unterer Speicheranschluss → Erzeuger -->
|
||||||
|
<path d="M500,155 L500,225 L540,225 L540,580"/>
|
||||||
|
|
||||||
|
<!-- Solar VL: Kollektor → unterer interner Solar-WT -->
|
||||||
|
<path d="M800,155 L800,210 L680,210 L680,520"/>
|
||||||
|
<!-- Solar RL: aus WT → zurück zum Kollektor -->
|
||||||
|
<path d="M630,520 L630,220 L860,220 L860,155"/>
|
||||||
|
|
||||||
|
<!-- WW-Entnahme oben aus Speicher -->
|
||||||
|
<path d="M700,330 L770,330 L770,385 L890,385"/>
|
||||||
|
<!-- KW-Eingang -->
|
||||||
|
<path d="M890,500 L770,500 L770,475 L700,475"/>
|
||||||
|
|
||||||
|
<!-- Heizungs-VL: oben/mittig aus Speicher → Mischer → Verteiler -->
|
||||||
|
<path d="M540,380 L540,660 L540,710"/>
|
||||||
|
<path d="M540,745 L540,785"/>
|
||||||
|
<!-- Heizungs-RL: Sammler → unten in Speicher -->
|
||||||
|
<path d="M410,580 L410,710 L410,840"/>
|
||||||
|
|
||||||
|
<!-- Verteiler/Sammler Leitungen -->
|
||||||
|
<path d="M210,785 L810,785"/>
|
||||||
|
<path d="M210,840 L810,840"/>
|
||||||
|
<line x1="280" y1="785" x2="280" y2="800"/>
|
||||||
|
<line x1="440" y1="785" x2="440" y2="800"/>
|
||||||
|
<line x1="590" y1="785" x2="590" y2="800"/>
|
||||||
|
<line x1="740" y1="785" x2="740" y2="800"/>
|
||||||
|
<line x1="280" y1="825" x2="280" y2="840"/>
|
||||||
|
<line x1="440" y1="825" x2="440" y2="840"/>
|
||||||
|
<line x1="590" y1="825" x2="590" y2="840"/>
|
||||||
|
<line x1="740" y1="825" x2="740" y2="840"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- ======== ANIMIERTE FLUSS-ROHRE ======== -->
|
||||||
|
|
||||||
|
<!-- Öl-Kessel: VL Erzeuger → oberer Speicheranschluss (M→L = Flussrichtung) -->
|
||||||
|
<path class="flow" id="f-oel-vl" stroke="#58a6ff" stroke-width="4"
|
||||||
|
d="M130,155 L130,210 L320,210 L320,265"/>
|
||||||
|
<!-- Öl-Kessel: RL unterer Speicheranschluss → Erzeuger (M→L, .rev für Rückfluss) -->
|
||||||
|
<path class="flow rev" id="f-oel-rl" stroke="#2255aa" stroke-width="4"
|
||||||
|
d="M200,155 L200,225 L370,225 L370,580"/>
|
||||||
|
|
||||||
|
<!-- Holzvergaser: VL Erzeuger → oberer Speicheranschluss -->
|
||||||
|
<path class="flow" id="f-holz-vl" stroke="#f0883e" stroke-width="4"
|
||||||
|
d="M430,155 L430,210 L490,210 L490,265"/>
|
||||||
|
<!-- Holzvergaser: RL unterer Speicheranschluss → Erzeuger -->
|
||||||
|
<path class="flow rev" id="f-holz-rl" stroke="#885522" stroke-width="4"
|
||||||
|
d="M500,155 L500,225 L540,225 L540,580"/>
|
||||||
|
|
||||||
|
<!-- Solar: VL Kollektor → unterer interner Solar-WT -->
|
||||||
|
<path class="flow" id="f-sol-vl" stroke="#ffd33d" stroke-width="4"
|
||||||
|
d="M800,155 L800,210 L680,210 L680,520"/>
|
||||||
|
<!-- Solar: RL aus WT → zurück zum Kollektor -->
|
||||||
|
<path class="flow rev" id="f-sol-rl" stroke="#997700" stroke-width="4"
|
||||||
|
d="M630,520 L630,220 L860,220 L860,155"/>
|
||||||
|
|
||||||
|
<!-- Warmwasser: Entnahme oben aus Speicher (keine Animation im Speicher) -->
|
||||||
|
<path class="flow" id="f-ww" stroke="#dd5500" stroke-width="3"
|
||||||
|
d="M700,330 L770,330 L770,385 L890,385"/>
|
||||||
|
<!-- Kaltwasser Eingang -->
|
||||||
|
<path class="flow rev" id="f-kw" stroke="#33aadd" stroke-width="3"
|
||||||
|
d="M890,500 L770,500 L770,475 L700,475"/>
|
||||||
|
|
||||||
|
<!-- Heizungs-VL: oben/mittig aus Speicher → Mischer → Verteiler -->
|
||||||
|
<path class="flow" id="f-vl-h" stroke="#cc3333" stroke-width="4"
|
||||||
|
d="M540,380 L540,660 L540,710"/>
|
||||||
|
<path class="flow" id="f-vl-v" stroke="#cc3333" stroke-width="3"
|
||||||
|
d="M540,745 L540,785"/>
|
||||||
|
<path class="flow" id="f-vl-vert" stroke="#cc3333" stroke-width="3"
|
||||||
|
d="M540,785 L810,785 M540,785 L210,785"/>
|
||||||
|
<path class="flow" id="f-hk-vl1" stroke="#cc3333" stroke-width="3" d="M280,785 L280,800"/>
|
||||||
|
<path class="flow" id="f-hk-vl2" stroke="#cc3333" stroke-width="3" d="M440,785 L440,800"/>
|
||||||
|
<path class="flow" id="f-hk-vl3" stroke="#cc3333" stroke-width="3" d="M590,785 L590,800"/>
|
||||||
|
<path class="flow" id="f-hk-vl4" stroke="#cc3333" stroke-width="3" d="M740,785 L740,800"/>
|
||||||
|
|
||||||
|
<!-- Heizungs-RL: Sammler → unten in Speicher -->
|
||||||
|
<path class="flow rev" id="f-rl-p" stroke="#2255aa" stroke-width="4"
|
||||||
|
d="M410,580 L410,710 L410,840"/>
|
||||||
|
<path class="flow rev" id="f-rl-vert" stroke="#2255aa" stroke-width="3"
|
||||||
|
d="M210,840 L810,840"/>
|
||||||
|
<path class="flow rev" id="f-hk-rl1" stroke="#2255aa" stroke-width="3" d="M280,825 L280,840"/>
|
||||||
|
<path class="flow rev" id="f-hk-rl2" stroke="#2255aa" stroke-width="3" d="M440,825 L440,840"/>
|
||||||
|
<path class="flow rev" id="f-hk-rl3" stroke="#2255aa" stroke-width="3" d="M590,825 L590,840"/>
|
||||||
|
<path class="flow rev" id="f-hk-rl4" stroke="#2255aa" stroke-width="3" d="M740,825 L740,840"/>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ======== PUFFERSPEICHER ======== -->
|
||||||
|
<rect x="270" y="255" width="450" height="355" rx="10"
|
||||||
|
fill="#0a1220" stroke="#336699" stroke-width="2.5"/>
|
||||||
|
<rect x="280" y="265" width="430" height="335" rx="6" fill="url(#tempG)"/>
|
||||||
|
<text x="495" y="290" text-anchor="middle" fill="#336699"
|
||||||
|
font-size="16" font-weight="bold" opacity=".5">PUFFERSPEICHER</text>
|
||||||
|
|
||||||
|
<!-- Anschluesse Rechts -->
|
||||||
|
<text x="695" y="340" fill="#dd5500" font-size="11" opacity=".6">WW</text>
|
||||||
|
<text x="695" y="480" fill="#33aadd" font-size="11" opacity=".6">KW</text>
|
||||||
|
|
||||||
|
<!-- Anschluesse Links oben (Öl/Holz VL-Einspeisung) -->
|
||||||
|
<text x="325" y="280" fill="#58a6ff" font-size="9" opacity=".6">Öl-VL</text>
|
||||||
|
<text x="495" y="280" fill="#f0883e" font-size="9" opacity=".6">Holz-VL</text>
|
||||||
|
|
||||||
|
<!-- Anschluesse Links unten (Öl/Holz RL-Abgang) -->
|
||||||
|
<text x="375" y="595" fill="#2255aa" font-size="9" opacity=".6">Öl-RL</text>
|
||||||
|
<text x="545" y="595" fill="#885522" font-size="9" opacity=".6">Holz-RL</text>
|
||||||
|
|
||||||
|
<!-- Heizungs-VL/RL Anschlüsse -->
|
||||||
|
<text x="555" y="395" fill="#cc3333" font-size="9" opacity=".6">Hz-VL</text>
|
||||||
|
<text x="395" y="595" fill="#2255aa" font-size="9" opacity=".6">Hz-RL</text>
|
||||||
|
|
||||||
|
<text x="290" y="310" fill="#cc4444" font-size="10" opacity=".4">heiss</text>
|
||||||
|
<text x="290" y="585" fill="#4488ff" font-size="10" opacity=".4">kalt</text>
|
||||||
|
|
||||||
|
<!-- Solar-Wärmetauscher im unteren Bereich des Puffers -->
|
||||||
|
<rect x="580" y="490" width="120" height="60" rx="5"
|
||||||
|
fill="#181400" stroke="#d4a000" stroke-width="1.5"/>
|
||||||
|
<text x="640" y="515" text-anchor="middle" fill="#e6a800"
|
||||||
|
font-size="10">SOLAR</text>
|
||||||
|
<text x="640" y="530" text-anchor="middle" fill="#e6a800"
|
||||||
|
font-size="10">Waermetauscher</text>
|
||||||
|
<!-- Solar-WT Anschlüsse -->
|
||||||
|
<text x="685" y="525" fill="#ffd33d" font-size="9" opacity=".6">VL</text>
|
||||||
|
<text x="625" y="555" fill="#997700" font-size="9" opacity=".6">RL</text>
|
||||||
|
|
||||||
|
<!-- WW-Tauscher im oberen Bereich des Puffers -->
|
||||||
|
<rect x="430" y="310" width="270" height="55" rx="5"
|
||||||
|
fill="#120800" stroke="#c05000" stroke-width="1.5"/>
|
||||||
|
<text x="565" y="333" text-anchor="middle" fill="#dd6600"
|
||||||
|
font-size="10">WARMWASSER</text>
|
||||||
|
<text x="565" y="352" text-anchor="middle" fill="#dd6600"
|
||||||
|
font-size="10">Waermetauscher</text>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ======== OEL-KESSEL ======== -->
|
||||||
|
<g class="comp" id="c-oel">
|
||||||
|
<rect x="65" y="50" width="200" height="105" rx="10"
|
||||||
|
fill="#0d1520" stroke="#4090e0" stroke-width="2.5"/>
|
||||||
|
<g transform="translate(165,82)">
|
||||||
|
<path d="M0,-22 C-5,-15 -12,-8 -12,2 C-12,10 -6,16 0,16 C6,16 12,10 12,2 C12,-8 5,-15 0,-22Z"
|
||||||
|
fill="none" stroke="#58a6ff" stroke-width="2"/>
|
||||||
|
<path d="M0,-12 C-3,-8 -6,-3 -6,2 C-6,5 -3,8 0,8 C3,8 6,5 6,2 C6,-3 3,-8 0,-12Z"
|
||||||
|
fill="#58a6ff" opacity=".4"/>
|
||||||
|
</g>
|
||||||
|
<text x="165" y="118" text-anchor="middle" fill="#58a6ff"
|
||||||
|
font-size="14" font-weight="bold">Oel-Kessel</text>
|
||||||
|
<text x="130" y="145" text-anchor="middle" fill="#58a6ff" font-size="9">VL</text>
|
||||||
|
<text x="200" y="145" text-anchor="middle" fill="#2255aa" font-size="9">RL</text>
|
||||||
|
<polygon points="130,155 125,165 135,165" fill="#58a6ff"/>
|
||||||
|
<polygon points="200,165 195,155 205,155" fill="#2255aa"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- ======== HOLZVERGASER ======== -->
|
||||||
|
<g class="comp" id="c-holz">
|
||||||
|
<rect x="355" y="50" width="200" height="105" rx="10"
|
||||||
|
fill="#150e00" stroke="#e07830" stroke-width="2.5"/>
|
||||||
|
<g transform="translate(455,78)">
|
||||||
|
<rect x="-15" y="2" width="30" height="8" rx="3" fill="#885522" opacity=".6"/>
|
||||||
|
<rect x="-12" y="-4" width="24" height="8" rx="3" fill="#aa6633" opacity=".6"/>
|
||||||
|
<path d="M0,-22 C-4,-14 -9,-7 -9,1 C-9,7 -5,12 0,12 C5,12 9,7 9,1 C9,-7 4,-14 0,-22Z"
|
||||||
|
fill="none" stroke="#f0883e" stroke-width="2"/>
|
||||||
|
<path d="M0,-12 C-2,-7 -4,-2 -4,2 C-4,4 -2,6 0,6 C2,6 4,4 4,2 C4,-2 2,-7 0,-12Z"
|
||||||
|
fill="#f0883e" opacity=".5"/>
|
||||||
|
</g>
|
||||||
|
<text x="455" y="118" text-anchor="middle" fill="#f0883e"
|
||||||
|
font-size="13" font-weight="bold">Holzvergaser</text>
|
||||||
|
<text x="430" y="145" text-anchor="middle" fill="#f0883e" font-size="9">VL</text>
|
||||||
|
<text x="500" y="145" text-anchor="middle" fill="#885522" font-size="9">RL</text>
|
||||||
|
<polygon points="430,155 425,165 435,165" fill="#f0883e"/>
|
||||||
|
<polygon points="500,165 495,155 505,155" fill="#885522"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- ======== THERMO-SOLAR ======== -->
|
||||||
|
<g class="comp" id="c-sol">
|
||||||
|
<rect x="745" y="50" width="200" height="105" rx="10"
|
||||||
|
fill="#181400" stroke="#d4a000" stroke-width="2.5"/>
|
||||||
|
<g transform="translate(845,82)" stroke="#ffd33d" stroke-width="2" fill="none">
|
||||||
|
<circle r="8" fill="#ffd33d" opacity=".3"/>
|
||||||
|
<line x1="0" y1="-14" x2="0" y2="-20"/>
|
||||||
|
<line x1="0" y1="14" x2="0" y2="20"/>
|
||||||
|
<line x1="-14" y1="0" x2="-20" y2="0"/>
|
||||||
|
<line x1="14" y1="0" x2="20" y2="0"/>
|
||||||
|
<line x1="-10" y1="-10" x2="-14" y2="-14"/>
|
||||||
|
<line x1="10" y1="-10" x2="14" y2="-14"/>
|
||||||
|
<line x1="-10" y1="10" x2="-14" y2="14"/>
|
||||||
|
<line x1="10" y1="10" x2="14" y2="14"/>
|
||||||
|
</g>
|
||||||
|
<text x="845" y="118" text-anchor="middle" fill="#e6a800"
|
||||||
|
font-size="13" font-weight="bold">Thermo-Solar</text>
|
||||||
|
<text x="800" y="145" text-anchor="middle" fill="#ffd33d" font-size="9">VL</text>
|
||||||
|
<text x="860" y="145" text-anchor="middle" fill="#997700" font-size="9">RL</text>
|
||||||
|
<polygon points="800,155 795,165 805,165" fill="#ffd33d"/>
|
||||||
|
<polygon points="860,165 855,155 865,155" fill="#997700"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Solar-Pumpe -->
|
||||||
|
<circle cx="800" cy="185" r="12" fill="#181400" stroke="#d4a000" stroke-width="1.5"/>
|
||||||
|
<polygon points="794,180 794,190 804,185" fill="#d4a000"/>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ======== WW / KW BOXEN ======== -->
|
||||||
|
<rect x="870" y="365" width="120" height="40" rx="6"
|
||||||
|
fill="#180800" stroke="#dd5500" stroke-width="1.5"/>
|
||||||
|
<text x="930" y="390" text-anchor="middle" fill="#dd5500"
|
||||||
|
font-size="11" font-weight="bold">WW-Ausgang</text>
|
||||||
|
|
||||||
|
<rect x="870" y="480" width="120" height="40" rx="6"
|
||||||
|
fill="#001520" stroke="#33aadd" stroke-width="1.5"/>
|
||||||
|
<text x="930" y="505" text-anchor="middle" fill="#33aadd"
|
||||||
|
font-size="11" font-weight="bold">KW-Eingang</text>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ======== MISCHER ======== -->
|
||||||
|
<circle cx="540" cy="728" r="18" fill="#111820" stroke="#888" stroke-width="2"/>
|
||||||
|
<polygon points="528,720 528,736 540,728" fill="#cc2222"/>
|
||||||
|
<polygon points="552,720 552,736 540,728" fill="#2255bb"/>
|
||||||
|
<text x="568" y="732" fill="#889" font-size="10">Mischer</text>
|
||||||
|
|
||||||
|
<!-- Heizkreis-Pumpe -->
|
||||||
|
<circle cx="410" cy="728" r="14" fill="#111820" stroke="#888" stroke-width="2"/>
|
||||||
|
<polygon points="404,722 404,734 416,728" fill="#888"/>
|
||||||
|
<text x="410" y="755" text-anchor="middle" fill="#778" font-size="9">Pumpe</text>
|
||||||
|
|
||||||
|
<!-- VL / RL Labels -->
|
||||||
|
<text x="552" y="640" fill="#cc3333" font-size="11" font-weight="bold">VL</text>
|
||||||
|
<text x="388" y="640" fill="#2255aa" font-size="11" font-weight="bold">RL</text>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ======== HEIZKREISE HK 1-4 ======== -->
|
||||||
|
|
||||||
|
<!-- HK-1 -->
|
||||||
|
<rect x="235" y="800" width="90" height="25" rx="3"
|
||||||
|
fill="#1a1200" stroke="#b08020" stroke-width="1.5"/>
|
||||||
|
<g stroke="#996622" stroke-width="1" opacity=".5">
|
||||||
|
<line x1="248" y1="803" x2="248" y2="822"/>
|
||||||
|
<line x1="256" y1="803" x2="256" y2="822"/>
|
||||||
|
<line x1="264" y1="803" x2="264" y2="822"/>
|
||||||
|
<line x1="272" y1="803" x2="272" y2="822"/>
|
||||||
|
<line x1="280" y1="803" x2="280" y2="822"/>
|
||||||
|
<line x1="288" y1="803" x2="288" y2="822"/>
|
||||||
|
<line x1="296" y1="803" x2="296" y2="822"/>
|
||||||
|
<line x1="304" y1="803" x2="304" y2="822"/>
|
||||||
|
<line x1="312" y1="803" x2="312" y2="822"/>
|
||||||
|
</g>
|
||||||
|
<text x="280" y="816" text-anchor="middle" fill="#d29922"
|
||||||
|
font-size="10" font-weight="bold">HK-1</text>
|
||||||
|
|
||||||
|
<!-- HK-2 -->
|
||||||
|
<rect x="395" y="800" width="90" height="25" rx="3"
|
||||||
|
fill="#1a1200" stroke="#b08020" stroke-width="1.5"/>
|
||||||
|
<g stroke="#996622" stroke-width="1" opacity=".5">
|
||||||
|
<line x1="408" y1="803" x2="408" y2="822"/>
|
||||||
|
<line x1="416" y1="803" x2="416" y2="822"/>
|
||||||
|
<line x1="424" y1="803" x2="424" y2="822"/>
|
||||||
|
<line x1="432" y1="803" x2="432" y2="822"/>
|
||||||
|
<line x1="440" y1="803" x2="440" y2="822"/>
|
||||||
|
<line x1="448" y1="803" x2="448" y2="822"/>
|
||||||
|
<line x1="456" y1="803" x2="456" y2="822"/>
|
||||||
|
<line x1="464" y1="803" x2="464" y2="822"/>
|
||||||
|
<line x1="472" y1="803" x2="472" y2="822"/>
|
||||||
|
</g>
|
||||||
|
<text x="440" y="816" text-anchor="middle" fill="#d29922"
|
||||||
|
font-size="10" font-weight="bold">HK-2</text>
|
||||||
|
|
||||||
|
<!-- HK-3 -->
|
||||||
|
<rect x="545" y="800" width="90" height="25" rx="3"
|
||||||
|
fill="#1a1200" stroke="#b08020" stroke-width="1.5"/>
|
||||||
|
<g stroke="#996622" stroke-width="1" opacity=".5">
|
||||||
|
<line x1="558" y1="803" x2="558" y2="822"/>
|
||||||
|
<line x1="566" y1="803" x2="566" y2="822"/>
|
||||||
|
<line x1="574" y1="803" x2="574" y2="822"/>
|
||||||
|
<line x1="582" y1="803" x2="582" y2="822"/>
|
||||||
|
<line x1="590" y1="803" x2="590" y2="822"/>
|
||||||
|
<line x1="598" y1="803" x2="598" y2="822"/>
|
||||||
|
<line x1="606" y1="803" x2="606" y2="822"/>
|
||||||
|
<line x1="614" y1="803" x2="614" y2="822"/>
|
||||||
|
<line x1="622" y1="803" x2="622" y2="822"/>
|
||||||
|
</g>
|
||||||
|
<text x="590" y="816" text-anchor="middle" fill="#d29922"
|
||||||
|
font-size="10" font-weight="bold">HK-3</text>
|
||||||
|
|
||||||
|
<!-- HK-4 -->
|
||||||
|
<rect x="695" y="800" width="90" height="25" rx="3"
|
||||||
|
fill="#1a1200" stroke="#b08020" stroke-width="1.5"/>
|
||||||
|
<g stroke="#996622" stroke-width="1" opacity=".5">
|
||||||
|
<line x1="708" y1="803" x2="708" y2="822"/>
|
||||||
|
<line x1="716" y1="803" x2="716" y2="822"/>
|
||||||
|
<line x1="724" y1="803" x2="724" y2="822"/>
|
||||||
|
<line x1="732" y1="803" x2="732" y2="822"/>
|
||||||
|
<line x1="740" y1="803" x2="740" y2="822"/>
|
||||||
|
<line x1="748" y1="803" x2="748" y2="822"/>
|
||||||
|
<line x1="756" y1="803" x2="756" y2="822"/>
|
||||||
|
<line x1="764" y1="803" x2="764" y2="822"/>
|
||||||
|
<line x1="772" y1="803" x2="772" y2="822"/>
|
||||||
|
</g>
|
||||||
|
<text x="740" y="816" text-anchor="middle" fill="#d29922"
|
||||||
|
font-size="10" font-weight="bold">HK-4</text>
|
||||||
|
|
||||||
|
<!-- Verteiler/Sammler Labels -->
|
||||||
|
<text x="175" y="789" fill="#cc3333" font-size="9">VL-Verteiler</text>
|
||||||
|
<text x="175" y="844" fill="#2255aa" font-size="9">RL-Sammler</text>
|
||||||
|
|
||||||
|
<!-- ======== LEGENDE ======== -->
|
||||||
|
<g transform="translate(30,860)">
|
||||||
|
<line x1="0" y1="0" x2="25" y2="0" stroke="#cc3333" stroke-width="3"/>
|
||||||
|
<text x="30" y="4" fill="#999" font-size="9">Vorlauf (heiss)</text>
|
||||||
|
<line x1="140" y1="0" x2="165" y2="0" stroke="#2255aa" stroke-width="3"/>
|
||||||
|
<text x="170" y="4" fill="#999" font-size="9">Ruecklauf (kalt)</text>
|
||||||
|
<line x1="300" y1="0" x2="325" y2="0" stroke="#ffd33d" stroke-width="3"/>
|
||||||
|
<text x="330" y="4" fill="#999" font-size="9">Solar</text>
|
||||||
|
<line x1="390" y1="0" x2="415" y2="0" stroke="#dd5500" stroke-width="3"/>
|
||||||
|
<text x="420" y="4" fill="#999" font-size="9">Warmwasser</text>
|
||||||
|
<line x1="520" y1="0" x2="545" y2="0" stroke="#33aadd" stroke-width="3"/>
|
||||||
|
<text x="550" y="4" fill="#999" font-size="9">Kaltwasser</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="info" style="margin-top:12px;padding:10px 20px;
|
||||||
|
background:#161b22;border:1px solid #222;border-radius:8px;
|
||||||
|
max-width:700px;font-size:.9em;text-align:center;color:#889">
|
||||||
|
Waehle oben einen Betriebsmodus aus.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var modes = {
|
||||||
|
sol: {
|
||||||
|
flows: ['f-sol-vl','f-sol-rl','f-ww','f-kw'],
|
||||||
|
active: ['c-sol'],
|
||||||
|
dim: ['c-oel','c-holz'],
|
||||||
|
btn: 'on-sol',
|
||||||
|
text: 'SOLAR-BETRIEB: Solaranlage laedt Puffer ueber unteren WT, Warmwasser wird bereitet'
|
||||||
|
},
|
||||||
|
holz: {
|
||||||
|
flows: [
|
||||||
|
'f-holz-vl','f-holz-rl','f-ww','f-kw',
|
||||||
|
'f-vl-h','f-vl-v','f-vl-vert',
|
||||||
|
'f-hk-vl1','f-hk-vl2','f-hk-vl3','f-hk-vl4',
|
||||||
|
'f-rl-p','f-rl-vert',
|
||||||
|
'f-hk-rl1','f-hk-rl2','f-hk-rl3','f-hk-rl4'
|
||||||
|
],
|
||||||
|
active: ['c-holz'],
|
||||||
|
dim: ['c-oel','c-sol'],
|
||||||
|
btn: 'on-holz',
|
||||||
|
text: 'HOLZVERGASER-BETRIEB: Holz laedt Puffer oben, Heizung + Warmwasser aktiv'
|
||||||
|
},
|
||||||
|
oel: {
|
||||||
|
flows: [
|
||||||
|
'f-oel-vl','f-oel-rl','f-ww','f-kw',
|
||||||
|
'f-vl-h','f-vl-v','f-vl-vert',
|
||||||
|
'f-hk-vl1','f-hk-vl2','f-hk-vl3','f-hk-vl4',
|
||||||
|
'f-rl-p','f-rl-vert',
|
||||||
|
'f-hk-rl1','f-hk-rl2','f-hk-rl3','f-hk-rl4'
|
||||||
|
],
|
||||||
|
active: ['c-oel'],
|
||||||
|
dim: ['c-holz','c-sol'],
|
||||||
|
btn: 'on-oel',
|
||||||
|
text: 'OELKESSEL-BETRIEB: Oel-Kessel laedt Puffer oben, Heizung + Warmwasser aktiv'
|
||||||
|
},
|
||||||
|
all: {
|
||||||
|
flows: [
|
||||||
|
'f-oel-vl','f-oel-rl','f-holz-vl','f-holz-rl',
|
||||||
|
'f-sol-vl','f-sol-rl','f-ww','f-kw',
|
||||||
|
'f-vl-h','f-vl-v','f-vl-vert',
|
||||||
|
'f-hk-vl1','f-hk-vl2','f-hk-vl3','f-hk-vl4',
|
||||||
|
'f-rl-p','f-rl-vert',
|
||||||
|
'f-hk-rl1','f-hk-rl2','f-hk-rl3','f-hk-rl4'
|
||||||
|
],
|
||||||
|
active: ['c-oel','c-holz','c-sol'],
|
||||||
|
dim: [],
|
||||||
|
btn: 'on-all',
|
||||||
|
text: 'ALLE QUELLEN AKTIV: Maximale Waermeleistung'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var cur = null;
|
||||||
|
|
||||||
|
function setMode(m) {
|
||||||
|
var i, el, allFlows, allComps, allBtns;
|
||||||
|
|
||||||
|
allFlows = document.querySelectorAll('.flow');
|
||||||
|
for (i = 0; i < allFlows.length; i++) allFlows[i].classList.remove('on');
|
||||||
|
|
||||||
|
allComps = document.querySelectorAll('.comp');
|
||||||
|
for (i = 0; i < allComps.length; i++) allComps[i].classList.remove('dim');
|
||||||
|
|
||||||
|
allBtns = document.querySelectorAll('.btns button');
|
||||||
|
for (i = 0; i < allBtns.length; i++) allBtns[i].className = '';
|
||||||
|
|
||||||
|
if (cur === m) {
|
||||||
|
cur = null;
|
||||||
|
document.getElementById('info').innerHTML = 'Waehle oben einen Betriebsmodus aus.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cur = m;
|
||||||
|
var cfg = modes[m];
|
||||||
|
|
||||||
|
for (i = 0; i < cfg.flows.length; i++) {
|
||||||
|
el = document.getElementById(cfg.flows[i]);
|
||||||
|
if (el) el.classList.add('on');
|
||||||
|
}
|
||||||
|
for (i = 0; i < cfg.dim.length; i++) {
|
||||||
|
el = document.getElementById(cfg.dim[i]);
|
||||||
|
if (el) el.classList.add('dim');
|
||||||
|
}
|
||||||
|
document.getElementById('b-' + m).className = cfg.btn;
|
||||||
|
document.getElementById('info').innerHTML = cfg.text;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Heizungsanlage – Hydraulisches Schaltbild</title>
|
||||||
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{background:#0d1117;color:#e6edf3;font-family:'Segoe UI',system-ui,sans-serif;
|
||||||
|
display:flex;flex-direction:column;align-items:center;padding:20px;min-height:100vh}
|
||||||
|
h1{font-size:1.45em;margin-bottom:6px}
|
||||||
|
.sub{color:#8b949e;margin-bottom:14px;font-size:.92em}
|
||||||
|
.ctrls{display:flex;gap:8px;margin-bottom:15px;flex-wrap:wrap;justify-content:center}
|
||||||
|
.ctrls button{padding:10px 22px;border:2px solid #30363d;border-radius:8px;
|
||||||
|
background:#161b22;color:#c9d1d9;font-size:.95em;cursor:pointer;transition:all .3s}
|
||||||
|
.ctrls button:hover{background:#21262d}
|
||||||
|
.ctrls button.a-sol{border-color:#ffd33d;background:#2a2000;color:#ffd33d}
|
||||||
|
.ctrls button.a-holz{border-color:#f0883e;background:#2a1500;color:#f0883e}
|
||||||
|
.ctrls button.a-oel{border-color:#58a6ff;background:#001a33;color:#58a6ff}
|
||||||
|
.ctrls button.a-all{border-color:#a371f7;background:#1a0033;color:#a371f7}
|
||||||
|
.w{width:100%;max-width:1250px;border:1px solid #21262d;border-radius:12px;
|
||||||
|
overflow:hidden;background:#0d1117}
|
||||||
|
svg{width:100%;height:auto;display:block}
|
||||||
|
.info{margin-top:12px;padding:12px 24px;background:#161b22;border:1px solid #21262d;
|
||||||
|
border-radius:8px;text-align:center;font-size:.93em;max-width:780px;line-height:1.6}
|
||||||
|
|
||||||
|
@keyframes dash{from{stroke-dashoffset:24}to{stroke-dashoffset:0}}
|
||||||
|
.pf{stroke-dasharray:8 16;animation:dash .7s linear infinite;
|
||||||
|
fill:none;stroke-linecap:round;stroke-linejoin:round}
|
||||||
|
.fg{opacity:0;transition:opacity .5s}.fg.on{opacity:1}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🏠 Heizungsanlage – Hydraulisches Schaltbild</h1>
|
||||||
|
<p class="sub">Interaktive Energiefluss-Visualisierung · Klicke einen Betriebsmodus</p>
|
||||||
|
<div class="ctrls">
|
||||||
|
<button id="b-sol" onclick="go('sol')">☀️ Solar</button>
|
||||||
|
<button id="b-holz" onclick="go('holz')">🪵 Holzvergaser</button>
|
||||||
|
<button id="b-oel" onclick="go('oel')">🛢️ Ölkessel</button>
|
||||||
|
<button id="b-all" onclick="go('all')">⚡ Alle Quellen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w">
|
||||||
|
<svg viewBox="0 0 1200 960" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gP" x1="0" y1="1" x2="0" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#0a1628"/>
|
||||||
|
<stop offset="100%" stop-color="#1a0808"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="gT" x1="0" y1="1" x2="0" y2="0">
|
||||||
|
<stop offset="0%" stop-color="#223388" stop-opacity=".18"/>
|
||||||
|
<stop offset="100%" stop-color="#882222" stop-opacity=".18"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="gl">
|
||||||
|
<feGaussianBlur stdDeviation="4" result="b"/>
|
||||||
|
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
|
||||||
|
</filter>
|
||||||
|
<filter id="sh">
|
||||||
|
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity=".35"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- ═══════════ BACKGROUND PIPES ═══════════ -->
|
||||||
|
<g stroke="#1c2128" stroke-width="6" fill="none"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Öl -->
|
||||||
|
<path d="M150,140 V195 H350 V250"/>
|
||||||
|
<path d="M390,250 V215 H220 V140"/>
|
||||||
|
<!-- Holz -->
|
||||||
|
<path d="M400,140 V195 H490 V250"/>
|
||||||
|
<path d="M530,250 V215 H470 V140"/>
|
||||||
|
<!-- Solar -->
|
||||||
|
<path d="M930,140 V195 H710 V250"/>
|
||||||
|
<path d="M670,250 V215 H1000 V140"/>
|
||||||
|
<!-- WW / KW -->
|
||||||
|
<path d="M700,490 H860 V375 H920"/>
|
||||||
|
<path d="M920,545 H840 V535 H700"/>
|
||||||
|
<!-- VL Heizung -->
|
||||||
|
<path d="M560,600 V780"/>
|
||||||
|
<!-- RL Heizung -->
|
||||||
|
<path d="M440,600 V920"/>
|
||||||
|
<!-- Verteiler VL -->
|
||||||
|
<line x1="240" y1="780" x2="850" y2="780"/>
|
||||||
|
<!-- Sammler RL -->
|
||||||
|
<line x1="240" y1="920" x2="850" y2="920"/>
|
||||||
|
<!-- HK VL-Drops -->
|
||||||
|
<line x1="285" y1="780" x2="285" y2="820"/>
|
||||||
|
<line x1="455" y1="780" x2="455" y2="820"/>
|
||||||
|
<line x1="625" y1="780" x2="625" y2="820"/>
|
||||||
|
<line x1="795" y1="780" x2="795" y2="820"/>
|
||||||
|
<!-- HK RL-Returns -->
|
||||||
|
<line x1="285" y1="875" x2="285" y2="920"/>
|
||||||
|
<line x1="455" y1="875" x2="455" y2="920"/>
|
||||||
|
<line x1="625" y1="875" x2="625" y2="920"/>
|
||||||
|
<line x1="795" y1="875" x2="795" y2="920"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- ═══════════ PUFFERSPEICHER ═══════════ -->
|
||||||
|
<rect x="290" y="240" width="530" height="360" rx="12"
|
||||||
|
fill="url(#gP)" stroke="#336699" stroke-width="3" filter="url(#sh)"/>
|
||||||
|
<rect x="300" y="250" width="510" height="340" rx="8" fill="url(#gT)"/>
|
||||||
|
<text x="555" y="282" text-anchor="middle" fill="#336699"
|
||||||
|
font-size="17" font-weight="bold" opacity=".55">PUFFERSPEICHER</text>
|
||||||
|
<text x="310" y="305" fill="#ff6666" font-size="10" opacity=".4">▲ heiß</text>
|
||||||
|
<text x="310" y="585" fill="#6688ff" font-size="10" opacity=".4">▼ kalt</text>
|
||||||
|
|
||||||
|
<!-- Anschlüsse ⑦–⑪ -->
|
||||||
|
<g fill="#336699" font-size="12" opacity=".4">
|
||||||
|
<text x="808" y="310">⑦</text>
|
||||||
|
<text x="808" y="360">⑧</text>
|
||||||
|
<text x="808" y="410">⑨</text>
|
||||||
|
<text x="808" y="460">⑩</text>
|
||||||
|
<text x="808" y="510">⑪</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- WW-Wärmetauscher (im Puffer) -->
|
||||||
|
<rect x="410" y="475" width="290" height="65" rx="6"
|
||||||
|
fill="#1a0800" stroke="#dd5500" stroke-width="2"/>
|
||||||
|
<text x="555" y="514" text-anchor="middle" fill="#dd5500"
|
||||||
|
font-size="12" font-weight="bold">WW-Wärmetauscher</text>
|
||||||
|
|
||||||
|
<!-- ═══════════ WÄRMEQUELLEN ═══════════ -->
|
||||||
|
|
||||||
|
<!-- Öl-Kessel -->
|
||||||
|
<rect x="70" y="30" width="190" height="110" rx="10"
|
||||||
|
fill="#161b22" stroke="#58a6ff" stroke-width="2.5" filter="url(#sh)"/>
|
||||||
|
<text x="165" y="72" text-anchor="middle" fill="#58a6ff" font-size="24">🛢️</text>
|
||||||
|
<text x="165" y="100" text-anchor="middle" fill="#58a6ff"
|
||||||
|
font-size="14" font-weight="bold">Öl-Kessel</text>
|
||||||
|
|
||||||
|
<!-- Holzvergaser -->
|
||||||
|
<rect x="340" y="30" width="190" height="110" rx="10"
|
||||||
|
fill="#1a1208" stroke="#f0883e" stroke-width="2.5" filter="url(#sh)"/>
|
||||||
|
<text x="435" y="72" text-anchor="middle" fill="#f0883e" font-size="24">🪵</text>
|
||||||
|
<text x="435" y="100" text-anchor="middle" fill="#f0883e"
|
||||||
|
font-size="13" font-weight="bold">Holzvergaser</text>
|
||||||
|
|
||||||
|
<!-- Thermo-Solar -->
|
||||||
|
<rect x="860" y="30" width="200" height="110" rx="10"
|
||||||
|
fill="#1a1800" stroke="#e6a800" stroke-width="2.5" filter="url(#sh)"/>
|
||||||
|
<text x="960" y="72" text-anchor="middle" fill="#e6a800" font-size="24">☀️</text>
|
||||||
|
<text x="960" y="100" text-anchor="middle" fill="#e6a800"
|
||||||
|
font-size="14" font-weight="bold">Thermo-Solar</text>
|
||||||
|
|
||||||
|
<!-- Highlight-Ringe (wenn aktiv) -->
|
||||||
|
<rect id="hl-oel" x="66" y="26" width="198" height="118" rx="12"
|
||||||
|
fill="none" stroke="#58a6ff" stroke-width="3" filter="url(#gl)" opacity="0"/>
|
||||||
|
<rect id="hl-holz" x="336" y="26" width="198" height="118" rx="12"
|
||||||
|
fill="none" stroke="#f0883e" stroke-width="3" filter="url(#gl)" opacity="0"/>
|
||||||
|
<rect id="hl-sol" x="856" y="26" width="208" height="118" rx="12"
|
||||||
|
fill="none" stroke="#e6a800" stroke-width="3" filter="url(#gl)" opacity="0"/>
|
||||||
|
|
||||||
|
<!-- ═══════════ WW / KW BOXEN ═══════════ -->
|
||||||
|
<rect x="920" y="355" width="165" height="45" rx="8"
|
||||||
|
fill="#1a0800" stroke="#dd5500" stroke-width="2"/>
|
||||||
|
<text x="1002" y="383" text-anchor="middle" fill="#dd5500"
|
||||||
|
font-size="12" font-weight="bold">WW-Ausgang ⑬</text>
|
||||||
|
|
||||||
|
<rect x="920" y="525" width="165" height="45" rx="8"
|
||||||
|
fill="#001a2a" stroke="#33aadd" stroke-width="2"/>
|
||||||
|
<text x="1002" y="553" text-anchor="middle" fill="#33aadd"
|
||||||
|
font-size="12" font-weight="bold">KW-Eingang</text>
|
||||||
|
|
||||||
|
<text x="872" y="465" fill="#dd5500" font-size="10" font-weight="bold">⑫ Mischung</text>
|
||||||
|
|
||||||
|
<!-- ═══════════ MISCHER & PUMPEN ═══════════ -->
|
||||||
|
|
||||||
|
<!-- Heizkreis-Mischer (VL) -->
|
||||||
|
<circle cx="560" cy="660" r="20" fill="#161b22" stroke="#888" stroke-width="2"/>
|
||||||
|
<polygon points="545,652 545,668 560,660" fill="#cc2222"/>
|
||||||
|
<polygon points="575,652 575,668 560,660" fill="#2255bb"/>
|
||||||
|
<text x="590" y="665" fill="#888" font-size="10">Mischer</text>
|
||||||
|
|
||||||
|
<!-- Heizkreis-Pumpe (RL) -->
|
||||||
|
<circle cx="440" cy="660" r="16" fill="#161b22" stroke="#888" stroke-width="2"/>
|
||||||
|
<polygon points="432,653 432,667 448,660" fill="#888"/>
|
||||||
|
<text x="440" y="693" text-anchor="middle" fill="#888" font-size="10">Pumpe</text>
|
||||||
|
|
||||||
|
<!-- Solar-Pumpe -->
|
||||||
|
<circle cx="930" cy="175" r="14" fill="#1a1800" stroke="#e6a800" stroke-width="2"/>
|
||||||
|
<polygon points="923,169 923,181 937,175" fill="#e6a800"/>
|
||||||
|
<text x="930" y="202" text-anchor="middle" fill="#a08000" font-size="9">Pumpe</text>
|
||||||
|
|
||||||
|
<!-- ═══════════ HEIZKREISE HK 1–4 ═══════════ -->
|
||||||
|
|
||||||
|
<!-- HK-1 -->
|
||||||
|
<rect x="230" y="820" width="110" height="55" rx="4"
|
||||||
|
fill="#1a1200" stroke="#d29
|
||||||
BIN
arakava-news/artikel/seafile-assets/Kombipufferspeicher.webp
Normal file
BIN
arakava-news/artikel/seafile-assets/Kombipufferspeicher.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
274
arakava-news/artikel/seafile-assets/Sensoren und Bilder.md
Normal file
274
arakava-news/artikel/seafile-assets/Sensoren und Bilder.md
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
## 1) Erzeuger (jeweils Vorlauf + Rücklauf)
|
||||||
|
|
||||||
|
- **Ölkessel VL**
|
||||||
|
|
||||||
|
- **Ölkessel RL**
|
||||||
|
|
||||||
|
- **Thermosolar VL**
|
||||||
|
|
||||||
|
- **Thermosolar RL**
|
||||||
|
|
||||||
|
- **Holzvergaser VL**
|
||||||
|
|
||||||
|
- **Holzvergaser RL**
|
||||||
|
|
||||||
|
= **6 Sensoren**
|
||||||
|
|
||||||
|
## 2) Heizungsseite (nach dem Mischer)
|
||||||
|
|
||||||
|
Du willst wissen, was wirklich ins Haus geht und was zurückkommt.
|
||||||
|
|
||||||
|
- **Heizungs-VL nach Mischer (Gesamt)**
|
||||||
|
|
||||||
|
- **Heizungs-RL gesamt vor dem Eintritt in den Speicher / Sammelpunkt**
|
||||||
|
|
||||||
|
= **2 Sensoren** (Summe: 8)
|
||||||
|
|
||||||
|
## 3) Rückläufe der Heizkreise einzeln
|
||||||
|
|
||||||
|
Das ist genau die Stelle, wo du ohne hydraulischen Abgleich richtig Erkenntnis rausziehst (wer klaut Durchfluss, wer säuft Temperatur).
|
||||||
|
|
||||||
|
- **HK1 Rücklauf**
|
||||||
|
|
||||||
|
- **HK2 Rücklauf**
|
||||||
|
|
||||||
|
- **HK3 Rücklauf**
|
||||||
|
|
||||||
|
- **HK4 Rücklauf** (falls wirklich 4 aktiv)
|
||||||
|
|
||||||
|
= **3–4 Sensoren** (Summe: 11–12)
|
||||||
|
|
||||||
|
> Hinweis: Den **Vorlauf je Heizkreis** brauchst du erstmal nicht zwingend, *wenn* alle hinter dem gleichen Mischer hängen. Die Spreizung kommt über die einzelnen Rückläufe + Gesamt-Vorlauf bereits gut raus. Später kann man je Kreis-VL nachrüsten, wenn du wirklich fein regeln willst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Pufferspeicher: 3 Sensoren sind ok – 5 sind besser (dein Fall: besser)
|
||||||
|
|
||||||
|
**Nur oben/mitte/unten** ist die Minimal-Variante. Funktioniert – aber du wirst bei deiner Rücklauf-Rückkopplung und Takt-Thematik zu wenig “Auflösung” haben.
|
||||||
|
|
||||||
|
Ich würde **5 Zonen** machen, weil:
|
||||||
|
|
||||||
|
- du damit **Schichtung** siehst (die ist im echten Leben der Boss),
|
||||||
|
|
||||||
|
- du erkennst **wer den Puffer wo lädt/zieht**,
|
||||||
|
|
||||||
|
- und du kannst **Start/Stop-Logik** für Öl viel sauberer bauen (z.B. “oben unter X°C” ist zu grob, wenn du nicht weißt, ob Mitte kollabiert).
|
||||||
|
|
||||||
|
Empfehlung:
|
||||||
|
|
||||||
|
- **Puffer oben (Top)**
|
||||||
|
|
||||||
|
- **Puffer oben-unten / oberes Drittel** (bei Tank-in-Tank besonders spannend)
|
||||||
|
|
||||||
|
- **Puffer Mitte**
|
||||||
|
|
||||||
|
- **Puffer unteres Drittel**
|
||||||
|
|
||||||
|
- **Puffer ganz unten**
|
||||||
|
|
||||||
|
= **5 Sensoren**
|
||||||
|
|
||||||
|
Wenn du unbedingt sparsam sein willst: nimm **4** (oben, oberes Drittel, Mitte, unten). Aber **5** ist die sweet spot-Variante.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ergebnis als harte Stückliste
|
||||||
|
|
||||||
|
- Erzeuger: **6**
|
||||||
|
|
||||||
|
- Heizung nach Mischer gesamt: **2**
|
||||||
|
|
||||||
|
- Heizkreis-Rückläufe: **3–4**
|
||||||
|
|
||||||
|
- Puffer: **5**
|
||||||
|
|
||||||
|
➡️ **Total: 16–17 Temperatursensoren**
|
||||||
|
|
||||||
|
Das ist nicht “Overkill”, das ist genau die Dichte, die aus Kaffeesatz ein System macht.
|
||||||
|
|
||||||
|
Kleiner Bonus (wenn du wirklich sauber debuggen willst, optional):
|
||||||
|
|
||||||
|
- **Kaltwasser-Zulauf Tank-in-Tank** und **Warmwasser-Ausgang** (2 Sensoren) – zeigt dir Zapfprofile und ob der Puffer oben „wegbricht“.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🏠 HEIZUNG -3°C │ So 26.01. 14:32 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ╔════════════════════════════════════════════════════════════════════════╗ │
|
||||||
|
│ ║ ☀️ PROGNOSE: 5 Stunden Sonne erwartet ║ │
|
||||||
|
│ ║ → Öl bleibt aus, warte auf Solar ║ │
|
||||||
|
│ ╚════════════════════════════════════════════════════════════════════════╝ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 📊 PUFFER │████████████████████░░░░░░░░░░│ 52°C 67% OK │ │
|
||||||
|
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ Oben: 58°C Mitte: 45°C Unten: 32°C │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
||||||
|
│ │ ☀️ SOLAR │ │ 🪵 HOLZ │ │ ❄️ WP │ │ 🛢️ ÖL │
|
||||||
|
│ │ │ │ │ │ │ │ │
|
||||||
|
│ │ ⏸️ wartet │ │ ⏸️ aus │ │ ▶️ LÄUFT │ │ 🚫 gesperrt │
|
||||||
|
│ │ │ │ │ │ Regel aktiv │ │ (Solar!) │
|
||||||
|
│ └────────────────┘ └────────────────┘ └────────────────┘ └────────────────┘
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 💰 GESPART: Heute ~4.20€ Monat: ~89€ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [ 🏠 Home ] [ 📊 Statistik ] [ 📈 Verlauf ] [ ⚙️ System ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ❄️ WÄRMEPUMPE -3°C │ So 26.01. 14:32 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────┐ ┌─────────────────────────────────┐
|
||||||
|
│ │ STATUS │ │ HEUTE │
|
||||||
|
│ │ ╔═══════════════════════════════╗ │ │ │
|
||||||
|
│ │ ║ ▶️ LÄUFT ║ │ │ Laufzeit: 4h 23m │
|
||||||
|
│ │ ║ Regel: WP > Öl ║ │ │ Starts: 3 │
|
||||||
|
│ │ ╚═══════════════════════════════╝ │ │ Verbrauch: ~6.2 kWh │
|
||||||
|
│ │ │ │ Kosten: ~1.86€ │
|
||||||
|
│ │ Außentemperatur: -3.1°C │ │ Wärme geliefert: ~18.6 kWh │
|
||||||
|
│ │ WP-Grenze: -5.0°C │ │ Wirtschaftlich: ❌ │
|
||||||
|
│ └─────────────────────────────────────┘ └─────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ BETRIEBSBEREICH │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ -15° -10° -5° 0° +5° +10° +15° │ │
|
||||||
|
│ │ │ │ │ │ │ │ │ │ │
|
||||||
|
│ │ ├──────┼──────┼─────┼──────┼──────┼──────┤ │ │
|
||||||
|
│ │ │░░░░░░│░░░░░░│█████│██████│██████│██████│ │ │
|
||||||
|
│ │ │ ÖL │ ÖL │ WP │ WP │ WP │ WP │ │ │
|
||||||
|
│ │ └──────┴──────┴──▲──┴──────┴──────┴──────┘ │ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ JETZT (-3°C) │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PROGNOSE MORGEN │ │
|
||||||
|
│ │ Min: -7°C (05:00) → WP aus, Öl übernimmt │ │
|
||||||
|
│ │ Max: +2°C (14:00) → WP möglich │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [ 🏠 Home ] [ 📊 Statistik ] [ 📈 Verlauf ] [ ⚙️ System ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ☀️ SOLAR-THERMIE +2°C │ So 26.01. 12:45 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────┐ ┌─────────────────────────────────┐
|
||||||
|
│ │ KOLLEKTOR │ │ LEISTUNG │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ ┌─────┐ │ │ ╔═══════════════════╗ │
|
||||||
|
│ │ / \ 68°C │ │ ║ 4.2 kW ║ │
|
||||||
|
│ │ │ ☀️☀️☀️ │ │ │ ╚═══════════════════╝ │
|
||||||
|
│ │ \_______/ │ │ Peak heute: 5.1 kW │
|
||||||
|
│ │ │ │ │ Durchschnitt: 3.8 kW │
|
||||||
|
│ │ ▼ │ │ │
|
||||||
|
│ │ ┌───────────┐ │ │ │
|
||||||
|
│ │ │ PUFFER │ 52°C │ │ │
|
||||||
|
│ │ └───────────┘ │ │ │
|
||||||
|
│ └─────────────────────────────────────┘ └─────────────────────────────────┘
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ERTRAG HEUTE │ │
|
||||||
|
│ │ Heute: 18.4 kWh = 2.58€ gespart (vs. Öl) │ │
|
||||||
|
│ │ Monat: 185 kWh = 25.90€ │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [ 🏠 Home ] [ 📊 Statistik ] [ 📈 Verlauf ] [ ⚙️ System ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🪵🔥 HOLZVERGASER -2°C │ So 26.01. 16:20 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ╔═══════════════════════════════╗ │
|
||||||
|
│ ║ 🔥 AKTIV – AUSBRAND ║ │
|
||||||
|
│ ║ Läuft seit: 3h 45m ║ │
|
||||||
|
│ ╚═══════════════════════════════╝ │
|
||||||
|
│ │
|
||||||
|
│ Abgas: 165°C | Ziel: 150–200°C │
|
||||||
|
│ VL: 78°C RL: 62°C ΔT: 16°C │
|
||||||
|
│ │
|
||||||
|
│ PUFFER: Oben 68°C Mitte 52°C Unten 32°C │
|
||||||
|
│ Geliefert: ~42 kWh Rest: ~15 kWh Voll in: ~45 min │
|
||||||
|
│ │
|
||||||
|
│ 💰 HEUTE: 42 kWh Holz = ~5.88€ gespart │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [ 🏠 Home ] [ 📊 Statistik ] [ 📈 Verlauf ] [ ⚙️ System ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 🌤️ WETTER-PROGNOSE -3°C │ So 26.01. 14:32 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ HEUTE: ⛅ -1° / -6° ☀️ 5h │
|
||||||
|
│ MO: ☁️ +2° / -4° ☀️ 1h │
|
||||||
|
│ DI: 🌧️ +4° / +1° ☀️ 0h │
|
||||||
|
│ MI: ☀️ +3° / -2° ☀️ 7h │
|
||||||
|
│ DO: ⛅ +1° / -5° ☀️ 4h │
|
||||||
|
│ │
|
||||||
|
│ HEIZUNGS-EMPFEHLUNG │
|
||||||
|
│ HEUTE: Solar → WP bevorzugt │
|
||||||
|
│ MO: Wenig Solar → WP Grenzbereich │
|
||||||
|
│ DI: Kein Solar → Öl │
|
||||||
|
│ MI: Viel Solar → WP │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [ 🏠 Home ] [ 📊 Statistik ] [ 📈 Verlauf ] [ ⚙️ System ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 📊 JAHRES-VERGLEICH Januar 2026 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ Ölverbrauch Januar │
|
||||||
|
│ 2025: 95 L ████████████████████████████████████████ │
|
||||||
|
│ 2026: 38 L ███████████████░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||||
|
│ │
|
||||||
|
│ TREND: -60 % Ölverbrauch │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [ 🏠 Home ] [ 📊 Statistik ] [ 📈 Verlauf ] [ ⚙️ System ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ⚠️ EREIGNISSE So 26.01. 14:32 │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ AKTUELL: keine Warnungen ✅ │
|
||||||
|
│ │
|
||||||
|
│ HEUTE │
|
||||||
|
│ 14:20 WP gestartet (Regel erfüllt) │
|
||||||
|
│ 11:45 Solar-Pumpe aus │
|
||||||
|
│ 09:30 Solar-Pumpe an │
|
||||||
|
│ 06:15 Öl-Sperre aktiviert │
|
||||||
|
│ │
|
||||||
|
│ GESTERN │
|
||||||
|
│ 22:30 WP gestoppt (Puffer voll) │
|
||||||
|
│ 05:30 🚨 NOTSTART Öl (Puffer 34°C) │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ [ 🏠 Home ] [ 📊 Statistik ] [ 📈 Verlauf ] [ ⚙️ System ] │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -126,6 +126,7 @@ Node antwortet:
|
||||||
|---------|--------|-----------|
|
|---------|--------|-----------|
|
||||||
| Kayak (Roundtrip) | ✅ Aktiv | Beste Datenquelle, GDPR-Consent automatisiert |
|
| Kayak (Roundtrip) | ✅ Aktiv | Beste Datenquelle, GDPR-Consent automatisiert |
|
||||||
| **Kayak Multi-City CX via HKG** | ✅ Aktiv | Primärer Scanner — FRA→HKG→KTI→FRA |
|
| **Kayak Multi-City CX via HKG** | ✅ Aktiv | Primärer Scanner — FRA→HKG→KTI→FRA |
|
||||||
|
| **Cathay Pacific direkt** | ✅ Aktiv | CX direkt buchen — FRA→KTI Roundtrip via cathaypacific.com |
|
||||||
| Trip.com | ✅ Aktiv | Gute Ergänzung, auch CX-Filter |
|
| Trip.com | ✅ Aktiv | Gute Ergänzung, auch CX-Filter |
|
||||||
| Momondo | ✅ Aktiv | Nur auf Muldenstein (Geo-Block aus Asien) |
|
| Momondo | ✅ Aktiv | Nur auf Muldenstein (Geo-Block aus Asien) |
|
||||||
| Google Flights | ⚠ Eingeschränkt | Wenige Ergebnisse, Consent-Probleme |
|
| Google Flights | ⚠ Eingeschränkt | Wenige Ergebnisse, Consent-Probleme |
|
||||||
|
|
|
||||||
417
fuenfvoacht/src/bot.py
Normal file
417
fuenfvoacht/src/bot.py
Normal file
|
|
@ -0,0 +1,417 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
FünfVorAcht Bot — Review, Scheduling, Briefing, Fehler-Alarm
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, Bot
|
||||||
|
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler,
|
||||||
|
MessageHandler, filters, ContextTypes)
|
||||||
|
from telegram.constants import ParseMode
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
|
||||||
|
import database as db
|
||||||
|
import logger as flog
|
||||||
|
|
||||||
|
BOT_TOKEN = os.environ['TELEGRAM_BOT_TOKEN']
|
||||||
|
CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '')
|
||||||
|
TZ = pytz.timezone(os.environ.get('TIMEZONE', 'Europe/Berlin'))
|
||||||
|
POST_TIME = os.environ.get('POST_TIME', '19:55')
|
||||||
|
|
||||||
|
BRAND_MARKER = "Pax et Lux Terranaut01 https://t.me/DieneDemLeben"
|
||||||
|
BRAND_SIGNATURE = (
|
||||||
|
"Wir schützen die Zukunft unserer Kinder und das Leben❤️\n\n"
|
||||||
|
"Pax et Lux Terranaut01 https://t.me/DieneDemLeben\n\n"
|
||||||
|
"Unterstützt die Menschen, die für Uns einstehen❗️"
|
||||||
|
)
|
||||||
|
|
||||||
|
edit_pending = {}
|
||||||
|
|
||||||
|
|
||||||
|
def today_str():
|
||||||
|
return datetime.now(TZ).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
|
||||||
|
def with_branding(content: str) -> str:
|
||||||
|
text = (content or "").rstrip()
|
||||||
|
if BRAND_MARKER in text:
|
||||||
|
return text
|
||||||
|
return f"{text}\n\n{BRAND_SIGNATURE}" if text else BRAND_SIGNATURE
|
||||||
|
|
||||||
|
|
||||||
|
def review_keyboard(date_str: str, post_time: str):
|
||||||
|
return InlineKeyboardMarkup([[
|
||||||
|
InlineKeyboardButton("✅ Freigeben", callback_data=f"approve:{date_str}:{post_time}"),
|
||||||
|
InlineKeyboardButton("✏️ Bearbeiten", callback_data=f"edit:{date_str}:{post_time}"),
|
||||||
|
]])
|
||||||
|
|
||||||
|
|
||||||
|
def is_reviewer(user_id: int) -> bool:
|
||||||
|
return user_id in db.get_reviewer_chat_ids()
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_reviewers(bot: Bot, text: str, parse_mode=ParseMode.HTML,
|
||||||
|
reply_markup=None):
|
||||||
|
for chat_id in db.get_reviewer_chat_ids():
|
||||||
|
try:
|
||||||
|
await bot.send_message(chat_id, text,
|
||||||
|
parse_mode=parse_mode,
|
||||||
|
reply_markup=reply_markup)
|
||||||
|
except Exception as e:
|
||||||
|
flog.error('notify_reviewer_failed', chat_id=chat_id, reason=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Commands ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not is_reviewer(update.effective_user.id):
|
||||||
|
return
|
||||||
|
await update.message.reply_text(
|
||||||
|
"🕗 <b>FünfVorAcht — Review Bot</b>\n\n"
|
||||||
|
"Artikel werden im Dashboard erstellt und eingeplant.\n"
|
||||||
|
"Hier kannst du sie freigeben oder letzte Änderungen vornehmen.\n\n"
|
||||||
|
"<b>Befehle:</b>\n"
|
||||||
|
"/heute — Alle Slots von heute\n"
|
||||||
|
"/queue — Nächste 3 Tage\n"
|
||||||
|
"/skip — Heutigen Hauptslot überspringen",
|
||||||
|
parse_mode=ParseMode.HTML
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_heute(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not is_reviewer(update.effective_user.id):
|
||||||
|
return
|
||||||
|
d = today_str()
|
||||||
|
articles = db.get_articles_by_date(d)
|
||||||
|
if not articles:
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"📭 Noch keine Artikel für heute ({d}).\n"
|
||||||
|
"👉 Dashboard: http://100.73.171.62:8080"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
status_map = {
|
||||||
|
'draft': '📝 Entwurf',
|
||||||
|
'scheduled': '🗓️ Eingeplant',
|
||||||
|
'sent_to_bot': '📱 Zum Review gesendet',
|
||||||
|
'approved': '✅ Freigegeben',
|
||||||
|
'posted': '📤 Gepostet ✓',
|
||||||
|
'skipped': '⏭️ Übersprungen',
|
||||||
|
'pending_review': '⏳ Wartet auf Freigabe',
|
||||||
|
}
|
||||||
|
lines = [f"📅 <b>{d}</b> — {len(articles)} Slot(s)\n"]
|
||||||
|
for art in articles:
|
||||||
|
lines.append(
|
||||||
|
f"<b>{art['post_time']} Uhr</b> · "
|
||||||
|
f"{status_map.get(art['status'], art['status'])} · "
|
||||||
|
f"v{art['version']}"
|
||||||
|
)
|
||||||
|
await update.message.reply_text('\n'.join(lines), parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_queue(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not is_reviewer(update.effective_user.id):
|
||||||
|
return
|
||||||
|
lines = ["📆 <b>Nächste 3 Tage:</b>\n"]
|
||||||
|
status_icons = {
|
||||||
|
'draft': '📝', 'scheduled': '🗓️', 'sent_to_bot': '📱',
|
||||||
|
'approved': '✅', 'posted': '📤', 'skipped': '⏭️',
|
||||||
|
}
|
||||||
|
for i in range(3):
|
||||||
|
d = (datetime.now(TZ) + timedelta(days=i)).strftime('%Y-%m-%d')
|
||||||
|
arts = db.get_articles_by_date(d)
|
||||||
|
if arts:
|
||||||
|
slots = ', '.join(
|
||||||
|
f"{a['post_time']} {status_icons.get(a['status'], '❓')}"
|
||||||
|
for a in arts
|
||||||
|
)
|
||||||
|
lines.append(f"<b>{d}</b>: {slots}")
|
||||||
|
else:
|
||||||
|
lines.append(f"❌ <b>{d}</b> — keine Artikel")
|
||||||
|
await update.message.reply_text('\n'.join(lines), parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
async def cmd_skip(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not is_reviewer(update.effective_user.id):
|
||||||
|
return
|
||||||
|
d = today_str()
|
||||||
|
channel = db.get_channel()
|
||||||
|
pt = channel.get('post_time', POST_TIME)
|
||||||
|
art = db.get_article_by_date(d, pt)
|
||||||
|
if not art:
|
||||||
|
db.create_article(d, "SKIP", "", None, "allgemein", pt)
|
||||||
|
db.update_article_status(d, 'skipped', post_time=pt)
|
||||||
|
flog.article_skipped(d, pt)
|
||||||
|
await update.message.reply_text(f"⏭️ {d} {pt} Uhr übersprungen.")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Callbacks ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def handle_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
parts = query.data.split(':', 2)
|
||||||
|
action = parts[0]
|
||||||
|
date_str = parts[1] if len(parts) > 1 else today_str()
|
||||||
|
post_time = parts[2] if len(parts) > 2 else POST_TIME
|
||||||
|
|
||||||
|
if action == "approve":
|
||||||
|
db.update_article_status(date_str, 'approved',
|
||||||
|
query.message.message_id,
|
||||||
|
query.message.chat_id,
|
||||||
|
post_time=post_time)
|
||||||
|
article = db.get_article_by_date(date_str, post_time)
|
||||||
|
flog.article_approved(date_str, post_time, query.message.chat_id)
|
||||||
|
await query.edit_message_text(
|
||||||
|
f"✅ <b>Freigegeben!</b>\n\n"
|
||||||
|
f"Wird automatisch um <b>{post_time} Uhr</b> gepostet.\n\n"
|
||||||
|
f"{article['content_final']}",
|
||||||
|
parse_mode=ParseMode.HTML
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action == "edit":
|
||||||
|
article = db.get_article_by_date(date_str, post_time)
|
||||||
|
edit_pending[f"{date_str}:{post_time}"] = True
|
||||||
|
await query.edit_message_text(
|
||||||
|
f"✏️ <b>Bearbeiten</b> — {date_str} {post_time} Uhr\n\n"
|
||||||
|
f"Schick mir den neuen Text als nächste Nachricht.\n\n"
|
||||||
|
f"<i>Aktueller Text:</i>\n{article['content_final']}",
|
||||||
|
parse_mode=ParseMode.HTML
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Textnachrichten ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
|
if not is_reviewer(update.effective_user.id):
|
||||||
|
return
|
||||||
|
# Suche ob ein Edit-Pending-Slot aktiv ist
|
||||||
|
pending_key = next((k for k in edit_pending), None)
|
||||||
|
if not pending_key:
|
||||||
|
await update.message.reply_text(
|
||||||
|
"ℹ️ Artikel werden im Dashboard erstellt.\n"
|
||||||
|
"http://100.73.171.62:8080"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
date_str, post_time = pending_key.split(':', 1)
|
||||||
|
new_text = update.message.text.strip()
|
||||||
|
db.update_article_content(date_str, new_text, post_time=post_time)
|
||||||
|
del edit_pending[pending_key]
|
||||||
|
await update.message.reply_text(
|
||||||
|
f"✏️ <b>Aktualisiert</b> — {date_str} {post_time} Uhr\n\n{new_text}",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
reply_markup=review_keyboard(date_str, post_time)
|
||||||
|
)
|
||||||
|
db.update_article_status(date_str, 'sent_to_bot',
|
||||||
|
update.message.message_id,
|
||||||
|
update.effective_chat.id,
|
||||||
|
post_time=post_time)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Scheduler Jobs ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def job_post_articles(bot: Bot):
|
||||||
|
"""Postet alle freigegebenen Artikel deren post_time jetzt fällig ist."""
|
||||||
|
now_berlin = datetime.now(TZ)
|
||||||
|
d = now_berlin.strftime('%Y-%m-%d')
|
||||||
|
current_slot = now_berlin.strftime('%H:%M')
|
||||||
|
|
||||||
|
articles = db.get_articles_by_date(d)
|
||||||
|
for article in articles:
|
||||||
|
if article['status'] != 'approved':
|
||||||
|
continue
|
||||||
|
if article['post_time'] != current_slot:
|
||||||
|
continue
|
||||||
|
if not CHANNEL_ID:
|
||||||
|
await notify_reviewers(bot, "⚠️ Kanal-ID nicht konfiguriert!")
|
||||||
|
flog.error('no_channel_id', date=d, post_time=current_slot)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
final_text = with_branding(article['content_final'])
|
||||||
|
msg = await bot.send_message(CHANNEL_ID, final_text, parse_mode=ParseMode.HTML)
|
||||||
|
db.update_article_status(d, 'posted', post_time=current_slot)
|
||||||
|
db.save_post_history(d, msg.message_id, post_time=current_slot)
|
||||||
|
flog.article_posted(d, current_slot, CHANNEL_ID, msg.message_id)
|
||||||
|
await notify_reviewers(
|
||||||
|
bot,
|
||||||
|
f"📤 <b>Fünf vor Acht gepostet!</b>\n{d} · {current_slot} Uhr"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
flog.posting_failed(d, current_slot, str(e))
|
||||||
|
await notify_reviewers(
|
||||||
|
bot,
|
||||||
|
f"❌ <b>Posting fehlgeschlagen!</b>\n\n"
|
||||||
|
f"📅 {d} · ⏰ {current_slot} Uhr\n"
|
||||||
|
f"Kanal: {CHANNEL_ID}\n\n"
|
||||||
|
f"<b>Ursache:</b> {str(e)[:250]}\n\n"
|
||||||
|
f"👉 Dashboard: http://100.73.171.62:8080"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def job_check_notify(bot: Bot):
|
||||||
|
"""Prüft alle 5 Min ob scheduled-Artikel zur Bot-Benachrichtigung fällig sind."""
|
||||||
|
due = db.get_due_notifications()
|
||||||
|
for article in due:
|
||||||
|
d = article['date']
|
||||||
|
pt = article['post_time']
|
||||||
|
text = (
|
||||||
|
f"📋 <b>Review: {d} · {pt} Uhr</b>\n"
|
||||||
|
f"Version {article['version']}\n"
|
||||||
|
f"──────────────────────\n\n"
|
||||||
|
f"{article['content_final']}\n\n"
|
||||||
|
f"──────────────────────\n"
|
||||||
|
f"Freigeben oder bearbeiten?"
|
||||||
|
)
|
||||||
|
await notify_reviewers(bot, text,
|
||||||
|
reply_markup=review_keyboard(d, pt))
|
||||||
|
db.update_article_status(d, 'sent_to_bot', post_time=pt)
|
||||||
|
flog.article_sent_to_bot(d, pt, db.get_reviewer_chat_ids())
|
||||||
|
|
||||||
|
|
||||||
|
async def job_morning_briefing(bot: Bot):
|
||||||
|
"""Morgen-Briefing: was ist heute geplant, was fehlt."""
|
||||||
|
d = today_str()
|
||||||
|
now_berlin = datetime.now(TZ)
|
||||||
|
channel = db.get_channel()
|
||||||
|
|
||||||
|
articles_today = db.get_articles_by_date(d)
|
||||||
|
approved = [a for a in articles_today if a['status'] in ('approved', 'scheduled', 'sent_to_bot')]
|
||||||
|
missing = [a for a in articles_today if a['status'] in ('draft', 'pending_review')]
|
||||||
|
|
||||||
|
# Nächste 3 Tage für Ausblick
|
||||||
|
plan_lines = []
|
||||||
|
for i in range(1, 4):
|
||||||
|
next_d = (now_berlin + timedelta(days=i)).strftime('%Y-%m-%d')
|
||||||
|
arts = db.get_articles_by_date(next_d)
|
||||||
|
if arts:
|
||||||
|
slots = ', '.join(f"{a['post_time']} ✅" if a['status'] in ('approved', 'scheduled') else f"{a['post_time']} 📝" for a in arts)
|
||||||
|
plan_lines.append(f" {next_d}: {slots}")
|
||||||
|
else:
|
||||||
|
plan_lines.append(f" {next_d}: ❌ leer")
|
||||||
|
|
||||||
|
lines = [f"☀️ <b>Guten Morgen — FünfVorAcht Briefing</b>\n",
|
||||||
|
f"📅 <b>Heute: {d}</b>"]
|
||||||
|
|
||||||
|
if approved:
|
||||||
|
lines.append(f"✅ Eingeplant: {len(approved)} Slot(s)")
|
||||||
|
for a in approved:
|
||||||
|
lines.append(f" • {a['post_time']} Uhr — {(a['content_final'] or '')[:50]}…")
|
||||||
|
else:
|
||||||
|
lines.append("⚠️ Noch kein Artikel für heute freigegeben!")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
lines.append(f"📝 Entwürfe (noch nicht freigegeben): {len(missing)}")
|
||||||
|
|
||||||
|
if not articles_today:
|
||||||
|
lines.append("❌ Kein Artikel erstellt — bitte jetzt anlegen.")
|
||||||
|
|
||||||
|
lines.append(f"\n📆 <b>Nächste 3 Tage:</b>")
|
||||||
|
lines.extend(plan_lines)
|
||||||
|
lines.append(f"\n👉 Dashboard: http://100.73.171.62:8080")
|
||||||
|
|
||||||
|
await notify_reviewers(bot, '\n'.join(lines))
|
||||||
|
flog.info('morning_briefing_sent', date=d)
|
||||||
|
|
||||||
|
|
||||||
|
async def job_cleanup_db():
|
||||||
|
"""Woechentliche DB-Bereinigung: alte Eintraege loeschen."""
|
||||||
|
import sqlite3, os
|
||||||
|
db_path = os.environ.get('DB_PATH', '/data/fuenfvoacht.db')
|
||||||
|
con = sqlite3.connect(db_path)
|
||||||
|
r1 = con.execute("DELETE FROM post_history WHERE posted_at < datetime('now', '-90 days')").rowcount
|
||||||
|
r2 = con.execute("DELETE FROM article_versions WHERE created_at < datetime('now', '-90 days')").rowcount
|
||||||
|
r3 = con.execute("DELETE FROM articles WHERE status='posted' AND date < date('now', '-180 days')").rowcount
|
||||||
|
con.execute("VACUUM")
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
flog.info('db_cleanup', ph_del=r1, av_del=r2, art_del=r3)
|
||||||
|
|
||||||
|
|
||||||
|
async def job_reminder_afternoon(bot: Bot):
|
||||||
|
"""Nachmittags-Reminder wenn Hauptslot noch nicht freigegeben."""
|
||||||
|
d = today_str()
|
||||||
|
channel = db.get_channel()
|
||||||
|
post_t = channel.get('post_time', POST_TIME)
|
||||||
|
art = db.get_article_by_date(d, post_t)
|
||||||
|
if art and art['status'] in ('sent_to_bot', 'pending_review', 'draft', 'scheduled'):
|
||||||
|
await notify_reviewers(
|
||||||
|
bot,
|
||||||
|
f"⚠️ <b>Noch nicht freigegeben!</b>\n\n"
|
||||||
|
f"Posting um {post_t} Uhr — bitte jetzt freigeben.",
|
||||||
|
reply_markup=review_keyboard(d, post_t) if art['status'] == 'sent_to_bot' else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
db.init_db()
|
||||||
|
|
||||||
|
# Standard-Reviewer aus ENV falls DB leer
|
||||||
|
reviewers_in_db = db.get_reviewers()
|
||||||
|
if not reviewers_in_db:
|
||||||
|
import os as _os
|
||||||
|
raw = _os.environ.get('REVIEW_CHAT_IDS', '')
|
||||||
|
admin = _os.environ.get('ADMIN_CHAT_ID', '')
|
||||||
|
ids_raw = [x.strip() for x in raw.split(',') if x.strip()] if raw else []
|
||||||
|
if admin and admin not in ids_raw:
|
||||||
|
ids_raw.append(admin)
|
||||||
|
for cid in ids_raw:
|
||||||
|
try:
|
||||||
|
db.add_reviewer(int(cid), f"Redakteur {cid}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
application = Application.builder().token(BOT_TOKEN).build()
|
||||||
|
bot = application.bot
|
||||||
|
|
||||||
|
channel = db.get_channel()
|
||||||
|
post_t = channel.get('post_time', POST_TIME) if channel else POST_TIME
|
||||||
|
|
||||||
|
location = db.get_current_location()
|
||||||
|
if location:
|
||||||
|
(rem_m_h, rem_m_m), (rem_a_h, rem_a_m) = db.get_reminder_times_in_berlin(location)
|
||||||
|
flog.info('scheduler_init',
|
||||||
|
location=location['name'],
|
||||||
|
morning=f"{rem_m_h:02d}:{rem_m_m:02d}",
|
||||||
|
afternoon=f"{rem_a_h:02d}:{rem_a_m:02d}")
|
||||||
|
else:
|
||||||
|
rem_m_h, rem_m_m = 10, 0
|
||||||
|
rem_a_h, rem_a_m = 17, 45
|
||||||
|
|
||||||
|
scheduler = AsyncIOScheduler(timezone=TZ)
|
||||||
|
# Jede Minute prüfen ob ein Artikel zu posten ist
|
||||||
|
scheduler.add_job(job_post_articles, 'cron', minute='*',
|
||||||
|
kwargs={'bot': bot})
|
||||||
|
# Alle 5 Min auf fällige Benachrichtigungen prüfen
|
||||||
|
scheduler.add_job(job_check_notify, 'cron', minute='*/5',
|
||||||
|
kwargs={'bot': bot})
|
||||||
|
# Morgen-Briefing 10:00 MEZ
|
||||||
|
scheduler.add_job(job_morning_briefing, 'cron',
|
||||||
|
hour=rem_m_h, minute=rem_m_m,
|
||||||
|
kwargs={'bot': bot})
|
||||||
|
# Nachmittags-Reminder
|
||||||
|
scheduler.add_job(job_reminder_afternoon, 'cron',
|
||||||
|
hour=rem_a_h, minute=rem_a_m,
|
||||||
|
kwargs={'bot': bot})
|
||||||
|
scheduler.add_job(job_cleanup_db, 'cron', day_of_week='sun', hour=3, minute=0)
|
||||||
|
scheduler.start()
|
||||||
|
|
||||||
|
flog.info('bot_started', post_time=post_t,
|
||||||
|
briefing=f"{rem_m_h:02d}:{rem_m_m:02d}",
|
||||||
|
afternoon=f"{rem_a_h:02d}:{rem_a_m:02d}")
|
||||||
|
|
||||||
|
application.add_handler(CommandHandler("start", cmd_start))
|
||||||
|
application.add_handler(CommandHandler("heute", cmd_heute))
|
||||||
|
application.add_handler(CommandHandler("queue", cmd_queue))
|
||||||
|
application.add_handler(CommandHandler("skip", cmd_skip))
|
||||||
|
application.add_handler(CallbackQueryHandler(handle_callback))
|
||||||
|
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
||||||
|
|
||||||
|
application.run_polling(drop_pending_updates=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
347
fuenfvoracht/KURZUEBERSICHT.html
Normal file
347
fuenfvoracht/KURZUEBERSICHT.html
Normal file
|
|
@ -0,0 +1,347 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>FünfVorAcht — Kurzübersicht</title>
|
||||||
|
<style>
|
||||||
|
@page { size: A4; margin: 18mm 18mm 18mm 18mm; }
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
color: #1e293b;
|
||||||
|
background: #fff;
|
||||||
|
width: 210mm;
|
||||||
|
min-height: 297mm;
|
||||||
|
padding: 18mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 3px solid #6366f1;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.header-left h1 {
|
||||||
|
font-size: 22pt;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #6366f1;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
.header-left .subtitle {
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #6366f1;
|
||||||
|
color: white;
|
||||||
|
font-size: 8pt;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intro */
|
||||||
|
.intro {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-left: 4px solid #6366f1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zwei-Spalten-Layout */
|
||||||
|
.two-col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boxen */
|
||||||
|
.box {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.box h3 {
|
||||||
|
font-size: 9.5pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
.box ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.box ul li {
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #374151;
|
||||||
|
padding: 2px 0;
|
||||||
|
padding-left: 14px;
|
||||||
|
position: relative;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.box ul li::before {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workflow */
|
||||||
|
.workflow {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.workflow h3 {
|
||||||
|
font-size: 9.5pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.workflow-steps {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
.step {
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.step-icon {
|
||||||
|
font-size: 14pt;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.step-text {
|
||||||
|
font-size: 7.5pt;
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.step-arrow {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 14pt;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zugangsdaten */
|
||||||
|
.access {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.access h3 {
|
||||||
|
font-size: 9.5pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
.access-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.access-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
}
|
||||||
|
.access-item .label {
|
||||||
|
font-size: 7.5pt;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.access-item .value {
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
padding-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.footer-left {
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
.footer-right {
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body { padding: 0; }
|
||||||
|
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="badge">🕗 Automatisierung</div>
|
||||||
|
<h1>FünfVorAcht</h1>
|
||||||
|
<div class="subtitle">KI-gestützter Telegram-Kanal-Poster mit Redaktions-Dashboard</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
Stand: Februar 2026<br>
|
||||||
|
CT 112 · pve-hetzner<br>
|
||||||
|
<strong>fuenfvoacht.orbitalo.net</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Intro -->
|
||||||
|
<div class="intro">
|
||||||
|
FünfVorAcht automatisiert die tägliche Bespielung eines Telegram-Kanals. Artikel werden per KI generiert, im Dashboard redigiert, auf den Punkt genau eingeplant und automatisch zum gewünschten Zeitpunkt gepostet — ohne manuelles Eingreifen.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Workflow -->
|
||||||
|
<div class="workflow">
|
||||||
|
<h3>⚡ Workflow</h3>
|
||||||
|
<div class="workflow-steps">
|
||||||
|
<div class="step"><span class="step-icon">🔗</span><span class="step-text">Quelle /<br>Thema eingeben</span></div>
|
||||||
|
<div class="step-arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">🤖</span><span class="step-text">KI generiert<br>Artikel</span></div>
|
||||||
|
<div class="step-arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">✏️</span><span class="step-text">Redigieren +<br>Vorschau</span></div>
|
||||||
|
<div class="step-arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">📅</span><span class="step-text">Datum +<br>Uhrzeit wählen</span></div>
|
||||||
|
<div class="step-arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">📩</span><span class="step-text">Review per<br>Telegram</span></div>
|
||||||
|
<div class="step-arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">✅</span><span class="step-text">Freigeben<br>oder bearbeiten</span></div>
|
||||||
|
<div class="step-arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">📤</span><span class="step-text">Automatisch<br>gepostet</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature-Boxen -->
|
||||||
|
<div class="two-col">
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h3>✍️ Artikel & KI</h3>
|
||||||
|
<ul>
|
||||||
|
<li>KI-Generierung via OpenRouter (GPT-4o, Claude, Mistral u.a.)</li>
|
||||||
|
<li>7 verschiedene Schreibstile wählbar (sarkastisch, sachlich, humorvoll…)</li>
|
||||||
|
<li>Echtzeit Telegram-Vorschau während dem Schreiben</li>
|
||||||
|
<li>Markenzeichen wird automatisch angehängt</li>
|
||||||
|
<li>Versionsverlauf bei Neu-Generierung</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h3>📅 Zeitplanung</h3>
|
||||||
|
<ul>
|
||||||
|
<li>15-Minuten-Zeitraster (06:00 – 23:45 Uhr)</li>
|
||||||
|
<li>Mehrere Artikel pro Tag planbar</li>
|
||||||
|
<li>7-Tage-Redaktionskalender im Dashboard</li>
|
||||||
|
<li>Umplanen & Löschen direkt im Board</li>
|
||||||
|
<li>Slot-Konfliktschutz (kein doppeltes Buchen)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h3>🤖 Telegram Bot</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Review-Anfrage an alle Redakteure</li>
|
||||||
|
<li>Inline-Buttons: ✅ Freigeben / ✏️ Bearbeiten</li>
|
||||||
|
<li>Morgen-Briefing täglich 10:00 Uhr</li>
|
||||||
|
<li>Nachmittags-Reminder um 18:00 Uhr</li>
|
||||||
|
<li>Sofort-Alarm bei Posting-Fehlern</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h3>⚙️ Dashboard & Verwaltung</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Webbasiertes Dashboard (Browser)</li>
|
||||||
|
<li>Mehrere Redakteure verwaltbar</li>
|
||||||
|
<li>Prompt-Bibliothek editierbar</li>
|
||||||
|
<li>Posting-History der letzten 30 Tage</li>
|
||||||
|
<li>OpenRouter-Kontostand abrufbar</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zugangsdaten -->
|
||||||
|
<div class="access">
|
||||||
|
<h3>🔑 Zugang & Technik</h3>
|
||||||
|
<div class="access-grid">
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Dashboard URL</div>
|
||||||
|
<div class="value">fuenfvoacht.orbitalo.net</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Telegram Bot</div>
|
||||||
|
<div class="value">@Diendemleben_bot</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Standard Posting-Zeit</div>
|
||||||
|
<div class="value">19:55 Uhr (konfigurierbar)</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Server</div>
|
||||||
|
<div class="value">CT 112 · pve-hetzner</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">GitHub</div>
|
||||||
|
<div class="value">github.com/Orbitalo/F-nf-vor-Acht</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Stack</div>
|
||||||
|
<div class="value">Python · Flask · Docker · SQLite</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="footer">
|
||||||
|
<div class="footer-left">
|
||||||
|
FünfVorAcht · Orbitalo Homelab · Stand Februar 2026
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
Zum Drucken: Browser → Drucken → DIN A4 · Ränder: Keine
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
231
fuenfvoracht/README.md
Normal file
231
fuenfvoracht/README.md
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
# FünfVorAcht
|
||||||
|
|
||||||
|
> KI-gestützter Telegram-Kanal-Poster mit Dashboard — selbst gehostet, Docker-basiert.
|
||||||
|
|
||||||
|
FünfVorAcht automatisiert die tägliche Bespielung eines Telegram-Kanals. Artikel werden per KI generiert, im Dashboard redigiert, zeitgenau eingeplant und automatisch gepostet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was kann FünfVorAcht?
|
||||||
|
|
||||||
|
| Feature | Beschreibung |
|
||||||
|
|---------|-------------|
|
||||||
|
| ✍️ KI-Artikel | Artikel per KI generieren (OpenRouter / GPT-4o, Claude, Mistral...) |
|
||||||
|
| 📅 Zeitplanung | 15-Minuten-Zeitslots, mehrere Artikel pro Tag möglich |
|
||||||
|
| 🗓️ Redaktionskalender | 7-Tage-Übersicht mit Umplanen & Löschen direkt im Board |
|
||||||
|
| 👥 Multi-Reviewer | Mehrere Redakteure erhalten Review-Anfragen parallel |
|
||||||
|
| ✅ Bot-Review | Artikel per Telegram freigeben oder zur Bearbeitung zurückgeben |
|
||||||
|
| ⏰ Morgen-Briefing | Täglich 10:00 Uhr: Tagesplan + Ausblick 3 Tage |
|
||||||
|
| 🔔 Nachmittags-Reminder | 18:00 Uhr: Warnung wenn Artikel noch nicht freigegeben |
|
||||||
|
| 🚨 Fehler-Alarm | Sofortbenachrichtigung bei Posting-Fehlern |
|
||||||
|
| 🏷️ Markenzeichen | Wird automatisch unter jeden Beitrag angehängt |
|
||||||
|
| 📚 Prompt-Bibliothek | Mehrere KI-Stile verwaltbar (Sarkastisch, Humorvoll, Sachlich...) |
|
||||||
|
| 📊 JSON-Logging | Strukturiertes Logging aller Ereignisse |
|
||||||
|
| 🐳 Docker | Kompletter Stack per Docker Compose |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schnellstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Repository klonen
|
||||||
|
git clone https://github.com/Orbitalo/F-nf-vor-Acht.git fuenfvoracht
|
||||||
|
cd fuenfvoracht
|
||||||
|
|
||||||
|
# 2. Konfiguration anlegen
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # Pflichtfelder ausfüllen
|
||||||
|
|
||||||
|
# 3. Stack starten
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
| Software | Version |
|
||||||
|
|----------|---------|
|
||||||
|
| Docker | 24+ |
|
||||||
|
| Docker Compose | 2.x |
|
||||||
|
| Telegram Bot | Via [@BotFather](https://t.me/BotFather) erstellen |
|
||||||
|
| OpenRouter Account | [openrouter.ai](https://openrouter.ai) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konfiguration (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pflichtfelder
|
||||||
|
|
||||||
|
| Variable | Beschreibung |
|
||||||
|
|----------|-------------|
|
||||||
|
| `TELEGRAM_BOT_TOKEN` | Bot-Token von [@BotFather](https://t.me/BotFather) |
|
||||||
|
| `TELEGRAM_CHANNEL_ID` | Kanal-ID (z.B. `-1001234567890`) |
|
||||||
|
| `REVIEW_CHAT_IDS` | Chat-IDs der Redakteure (kommagetrennt) |
|
||||||
|
| `OPENROUTER_API_KEY` | API-Key von [openrouter.ai](https://openrouter.ai) |
|
||||||
|
| `AUTH_USER` | Dashboard-Benutzername |
|
||||||
|
| `AUTH_PASS` | Dashboard-Passwort |
|
||||||
|
|
||||||
|
### Optionale Felder
|
||||||
|
|
||||||
|
| Variable | Standard | Beschreibung |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `POST_TIME` | `19:55` | Standard-Posting-Zeit (HH:MM) |
|
||||||
|
| `TIMEZONE` | `Europe/Berlin` | Zeitzone |
|
||||||
|
| `DB_PATH` | `/data/fuenfvoracht.db` | Datenbankpfad |
|
||||||
|
|
||||||
|
### Eigene Chat-ID herausfinden
|
||||||
|
|
||||||
|
Nachricht an [@userinfobot](https://t.me/userinfobot) senden — er antwortet mit der Chat-ID.
|
||||||
|
|
||||||
|
### Kanal-ID herausfinden
|
||||||
|
|
||||||
|
Den Bot zum Kanal als Admin hinzufügen, dann eine Nachricht senden und die Kanal-ID aus der Telegram API auslesen (beginnt mit `-100`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ Docker Stack │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────┐ ┌──────────────────────┐ │
|
||||||
|
│ │ fuenfvoracht-web │ │ fuenfvoracht-bot │ │
|
||||||
|
│ │ Flask :8080 │ │ python-telegram-bot │ │
|
||||||
|
│ │ Dashboard │ │ APScheduler │ │
|
||||||
|
│ └────────┬─────────┘ └──────────┬───────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────┬─────────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ SQLite Datenbank │
|
||||||
|
│ /data/fuenfvoracht.db │
|
||||||
|
└──────────────────────┬──────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────┴────────┐
|
||||||
|
▼ ▼
|
||||||
|
Telegram Bot Telegram Kanal
|
||||||
|
(Review) (Posting)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Quelle/Thema im Dashboard eingeben
|
||||||
|
↓
|
||||||
|
2. KI generiert Artikel (OpenRouter)
|
||||||
|
↓
|
||||||
|
3. Redigieren + Telegram-Vorschau
|
||||||
|
↓
|
||||||
|
4. Einplanen: Datum + 15-Min-Slot + Review-Zeit
|
||||||
|
↓
|
||||||
|
5. Bot schickt Review an alle Redakteure
|
||||||
|
↓
|
||||||
|
[✅ Freigeben] [✏️ Bearbeiten]
|
||||||
|
↓
|
||||||
|
6. Scheduler postet automatisch zum geplanten Zeitpunkt
|
||||||
|
↓
|
||||||
|
7. Markenzeichen wird automatisch angehängt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bot-Befehle
|
||||||
|
|
||||||
|
| Befehl | Funktion |
|
||||||
|
|--------|---------|
|
||||||
|
| `/start` | Übersicht & alle Befehle |
|
||||||
|
| `/heute` | Alle geplanten Slots des heutigen Tages |
|
||||||
|
| `/queue` | Nächste 3 Tage mit Slots |
|
||||||
|
| `/skip` | Hauptslot heute überspringen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard-Seiten
|
||||||
|
|
||||||
|
| Seite | URL | Funktion |
|
||||||
|
|-------|-----|---------|
|
||||||
|
| Studio | `/` | Artikel generieren, redigieren, einplanen |
|
||||||
|
| History | `/history` | Alle Posts der letzten 30 Tage |
|
||||||
|
| Prompts | `/prompts` | KI-Prompt Bibliothek verwalten |
|
||||||
|
| Einstellungen | `/settings` | Kanal, Reviewers, Posting-Zeit |
|
||||||
|
| Hilfe | `/hilfe` | Ausführliche Anleitung |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verzeichnisstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
fuenfvoracht/
|
||||||
|
├── docker-compose.yml ← Docker Stack Definition
|
||||||
|
├── .env.example ← Konfigurationsvorlage
|
||||||
|
├── README.md ← Diese Datei
|
||||||
|
├── .gitignore ← .env, data/, logs/ ausgeschlossen
|
||||||
|
└── src/
|
||||||
|
├── app.py ← Flask Dashboard
|
||||||
|
├── bot.py ← Telegram Bot + APScheduler
|
||||||
|
├── database.py ← SQLite Datenbankschicht
|
||||||
|
├── openrouter.py ← KI-API Client
|
||||||
|
├── logger.py ← JSON-Logging
|
||||||
|
├── requirements.txt ← Python-Abhängigkeiten
|
||||||
|
├── Dockerfile.web ← Dashboard Container
|
||||||
|
├── Dockerfile.bot ← Bot Container
|
||||||
|
└── templates/ ← HTML-Templates (Jinja2)
|
||||||
|
├── index.html ← Studio
|
||||||
|
├── history.html
|
||||||
|
├── prompts.html
|
||||||
|
├── settings.html
|
||||||
|
└── hilfe.html
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Öffentlicher Zugang via Cloudflare Tunnel
|
||||||
|
|
||||||
|
Für Zugriff ohne offene Firewall-Ports:
|
||||||
|
|
||||||
|
1. **[Cloudflare Zero Trust](https://one.dash.cloudflare.com)** → Networks → Tunnels → Create
|
||||||
|
2. Hostname: `fuenfvoracht.example.com`
|
||||||
|
3. Service: `http://localhost:8080`
|
||||||
|
4. Den `docker run cloudflare/cloudflared ...`-Befehl ausführen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bekannte Hinweise
|
||||||
|
|
||||||
|
**Tailscale + Docker DNS:** Falls der Host Tailscale verwendet, kann der Bot `api.telegram.org` nicht auflösen. Die `docker-compose.yml` enthält bereits den Fix:
|
||||||
|
```yaml
|
||||||
|
dns:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 8.8.4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datensicherung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Datenbank sichern
|
||||||
|
cp data/fuenfvoracht.db backups/fuenfvoracht_$(date +%Y%m%d).db
|
||||||
|
|
||||||
|
# Komplettes Verzeichnis sichern
|
||||||
|
tar -czf fuenfvoracht_backup_$(date +%Y%m%d).tar.gz data/ logs/ .env
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT — frei verwendbar, anpassbar und weitergabe erlaubt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Entwickelt von
|
||||||
|
|
||||||
|
[Orbitalo](https://github.com/Orbitalo) — Homelab & Automatisierungsprojekte
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# STATE: FünfVorAcht
|
# STATE: FünfVorAcht
|
||||||
**Stand: 26.02.2026**
|
**Stand: 27.02.2026**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -43,11 +43,10 @@ KI generiert Artikel (OpenRouter)
|
||||||
↓
|
↓
|
||||||
Redigieren im Dashboard + Telegram-Vorschau
|
Redigieren im Dashboard + Telegram-Vorschau
|
||||||
↓
|
↓
|
||||||
Einplanen: Datum + 15-Min-Zeitslot + Bot-Benachrichtigungszeit
|
Einplanen: Datum + 15-Min-Zeitslot
|
||||||
↓
|
↓
|
||||||
Scheduler schickt Review an alle Redakteure (notify_at)
|
Status → approved (direkt, kein Review nötig)
|
||||||
↓
|
Info-Nachricht an Redakteure: "Artikel eingeplant für XX:XX"
|
||||||
[✅ Freigeben] [✏️ Bearbeiten]
|
|
||||||
↓
|
↓
|
||||||
APScheduler postet automatisch zum Zeitslot
|
APScheduler postet automatisch zum Zeitslot
|
||||||
↓
|
↓
|
||||||
|
|
@ -89,11 +88,11 @@ Bestätigung + Markenzeichen automatisch angehängt
|
||||||
|
|
||||||
### Article-Status-Lifecycle
|
### Article-Status-Lifecycle
|
||||||
```
|
```
|
||||||
draft → scheduled → sent_to_bot → approved → posted
|
draft → approved → posted
|
||||||
↘ rejected
|
|
||||||
↘ skipped
|
↘ skipped
|
||||||
↘ pending_review
|
|
||||||
```
|
```
|
||||||
|
**Hinweis:** Review-Schritt (sent_to_bot → approve/reject) wurde am 27.02.2026 entfernt.
|
||||||
|
Einplanen setzt direkt auf `approved`, Info-Nachricht statt Review-Buttons.
|
||||||
|
|
||||||
### Zeitslot-System
|
### Zeitslot-System
|
||||||
- `UNIQUE(date, post_time)` — Konflikte technisch ausgeschlossen
|
- `UNIQUE(date, post_time)` — Konflikte technisch ausgeschlossen
|
||||||
|
|
@ -138,9 +137,9 @@ draft → scheduled → sent_to_bot → approved → posted
|
||||||
| `/heute` | Alle Slots des heutigen Tages |
|
| `/heute` | Alle Slots des heutigen Tages |
|
||||||
| `/queue` | Nächste 3 Tage mit Slots |
|
| `/queue` | Nächste 3 Tage mit Slots |
|
||||||
| `/skip` | Hauptslot heute überspringen |
|
| `/skip` | Hauptslot heute überspringen |
|
||||||
| Inline-Review | ✅ Freigeben / ✏️ Bearbeiten |
|
| Inline-Review | Legacy (noch im Code, nicht mehr aktiv genutzt) |
|
||||||
| Morgen-Briefing | 10:00 MEZ: Tagesplan + Ausblick 3 Tage |
|
| Morgen-Briefing | 10:00 MEZ: Tagesplan + Ausblick 3 Tage |
|
||||||
| Nachmittags-Reminder | 18:00 MEZ: Warnung wenn nicht freigegeben |
|
| Nachmittags-Reminder | 18:00 MEZ: Warnung wenn kein Artikel eingeplant |
|
||||||
| Fehler-Alarm | Sofort bei Posting-Fehler: Ursache + Dashboard-Link |
|
| Fehler-Alarm | Sofort bei Posting-Fehler: Ursache + Dashboard-Link |
|
||||||
| Willkommensnachricht | Automatisch bei neuem Redakteur |
|
| Willkommensnachricht | Automatisch bei neuem Redakteur |
|
||||||
|
|
||||||
|
|
@ -181,6 +180,15 @@ Events: `article_generated`, `article_saved`, `article_scheduled`, `article_sent
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 27.02.2026 — Review-Schritt entfernt
|
||||||
|
- **Einplanen setzt direkt auf `approved`** — kein `scheduled` → `sent_to_bot` → Review mehr
|
||||||
|
- **Info-Nachricht statt Review-Buttons:** Redakteure bekommen nur Hinweis "Artikel eingeplant für XX:XX"
|
||||||
|
- **Save-Endpoint Bug gefixt:** Artikel mit anderer `post_time` wurde nicht gefunden
|
||||||
|
- **Bot `job_check_notify`:** Setzt direkt auf `approved` statt `sent_to_bot`
|
||||||
|
- **Nachmittags-Reminder:** Nur noch wenn gar kein Artikel eingeplant
|
||||||
|
- **Dashboard:** Button "Einplanen" statt "Einplanen & zum Bot senden", Notify-Dropdown entfernt
|
||||||
|
- **Docker-Images neu gebaut** (Source ist ins Image gebaut, nicht gemountet)
|
||||||
|
|
||||||
### 26.02.2026 — Board-Interaktivität
|
### 26.02.2026 — Board-Interaktivität
|
||||||
- **Redaktionsplan komplett überarbeitet:** Mehrere Artikel pro Tag sichtbar (vorher: einer pro Tag)
|
- **Redaktionsplan komplett überarbeitet:** Mehrere Artikel pro Tag sichtbar (vorher: einer pro Tag)
|
||||||
- **🔄 Umplanen:** Inline-Panel direkt unter dem Artikel — Datum + Uhrzeit mit Live-Slot-Prüfung, 15-Minuten-Raster
|
- **🔄 Umplanen:** Inline-Panel direkt unter dem Artikel — Datum + Uhrzeit mit Live-Slot-Prüfung, 15-Minuten-Raster
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,23 @@ async def job_morning_briefing(bot: Bot):
|
||||||
flog.info('morning_briefing_sent', date=d)
|
flog.info('morning_briefing_sent', date=d)
|
||||||
|
|
||||||
|
|
||||||
|
async def job_cleanup_db():
|
||||||
|
"""Wöchentliche DB-Bereinigung: alte Einträge löschen."""
|
||||||
|
import sqlite3, os
|
||||||
|
db_path = os.environ.get('DB_PATH', '/data/fuenfvoracht.db')
|
||||||
|
con = sqlite3.connect(db_path)
|
||||||
|
# post_history älter als 90 Tage
|
||||||
|
r1 = con.execute("DELETE FROM post_history WHERE posted_at < datetime('now', '-90 days')").rowcount
|
||||||
|
# article_versions älter als 90 Tage
|
||||||
|
r2 = con.execute("DELETE FROM article_versions WHERE created_at < datetime('now', '-90 days')").rowcount
|
||||||
|
# Artikel die bereits gepostet sind und älter als 180 Tage
|
||||||
|
r3 = con.execute("DELETE FROM articles WHERE status='posted' AND date < date('now', '-180 days')").rowcount
|
||||||
|
con.execute("VACUUM")
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
flog.info('db_cleanup', post_history_deleted=r1, versions_deleted=r2, articles_deleted=r3)
|
||||||
|
|
||||||
|
|
||||||
async def job_reminder_afternoon(bot: Bot):
|
async def job_reminder_afternoon(bot: Bot):
|
||||||
"""Nachmittags-Reminder wenn Hauptslot noch nicht freigegeben."""
|
"""Nachmittags-Reminder wenn Hauptslot noch nicht freigegeben."""
|
||||||
d = today_str()
|
d = today_str()
|
||||||
|
|
@ -382,6 +399,8 @@ def main():
|
||||||
scheduler.add_job(job_reminder_afternoon, 'cron',
|
scheduler.add_job(job_reminder_afternoon, 'cron',
|
||||||
hour=rem_a_h, minute=rem_a_m,
|
hour=rem_a_h, minute=rem_a_m,
|
||||||
kwargs={'bot': bot})
|
kwargs={'bot': bot})
|
||||||
|
# Wöchentliche DB-Bereinigung (Sonntags 03:00 Uhr)
|
||||||
|
scheduler.add_job(job_cleanup_db, 'cron', day_of_week='sun', hour=3, minute=0)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
flog.info('bot_started', post_time=post_t,
|
flog.info('bot_started', post_time=post_t,
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@
|
||||||
## Routing
|
## Routing
|
||||||
- Cloudflare Tunnel CT 101: arakava-news-2.orbitalo.net → :80
|
- Cloudflare Tunnel CT 101: arakava-news-2.orbitalo.net → :80
|
||||||
- Cloudflare Tunnel CT 109: matomo.orbitalo.net → :80
|
- Cloudflare Tunnel CT 109: matomo.orbitalo.net → :80
|
||||||
|
- Cloudflare Tunnel CT 109: rss-manager.orbitalo.net → :8080
|
||||||
- Kein Traefik, kein PBS-Gateway mehr
|
- Kein Traefik, kein PBS-Gateway mehr
|
||||||
|
|
||||||
## Zugangsdaten
|
## Zugangsdaten
|
||||||
|
|
|
||||||
408
redax-wp/KURZUEBERSICHT.html
Normal file
408
redax-wp/KURZUEBERSICHT.html
Normal file
|
|
@ -0,0 +1,408 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Redax-WP — Kurzübersicht</title>
|
||||||
|
<style>
|
||||||
|
@page { size: A4 landscape; margin: 14mm 16mm 14mm 16mm; }
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #1e293b;
|
||||||
|
background: #fff;
|
||||||
|
width: 297mm;
|
||||||
|
min-height: 210mm;
|
||||||
|
padding: 14mm 16mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-bottom: 3px solid #2563eb;
|
||||||
|
padding-bottom: 9px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.header-left { display: flex; align-items: center; gap: 14px; }
|
||||||
|
.logo-icon {
|
||||||
|
font-size: 28pt;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.header-left h1 {
|
||||||
|
font-size: 20pt;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2563eb;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.header-left .subtitle {
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #2563eb;
|
||||||
|
color: white;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Intro-Banner */
|
||||||
|
.intro {
|
||||||
|
background: linear-gradient(135deg, #eff6ff, #f0fdf4);
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
border-left: 4px solid #2563eb;
|
||||||
|
padding: 7px 12px;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drei-Spalten-Layout */
|
||||||
|
.three-col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zwei-Spalten-Layout */
|
||||||
|
.two-col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.1fr 0.9fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boxen */
|
||||||
|
.box {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 11px;
|
||||||
|
}
|
||||||
|
.box.blue { border-top: 3px solid #2563eb; }
|
||||||
|
.box.green { border-top: 3px solid #16a34a; }
|
||||||
|
.box.purple{ border-top: 3px solid #7c3aed; }
|
||||||
|
.box.orange{ border-top: 3px solid #ea580c; }
|
||||||
|
.box.teal { border-top: 3px solid #0891b2; }
|
||||||
|
.box.rose { border-top: 3px solid #e11d48; }
|
||||||
|
|
||||||
|
.box h3 {
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.box ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.box ul li {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #374151;
|
||||||
|
padding: 1.5px 0 1.5px 12px;
|
||||||
|
position: relative;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.box ul li::before {
|
||||||
|
content: "›";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workflow horizontal */
|
||||||
|
.workflow {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.workflow h3 {
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
.workflow-steps {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.step { text-align: center; flex: 1; }
|
||||||
|
.step-icon { font-size: 13pt; display: block; margin-bottom: 2px; }
|
||||||
|
.step-label {
|
||||||
|
font-size: 7pt;
|
||||||
|
color: #475569;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.step-sub {
|
||||||
|
font-size: 6.5pt;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.arrow { color: #cbd5e1; font-size: 13pt; flex-shrink: 0; padding: 0 3px; }
|
||||||
|
|
||||||
|
/* Routing-Tabelle */
|
||||||
|
.routing {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 11px;
|
||||||
|
}
|
||||||
|
.routing h3 {
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
|
.route-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid #f8fafc;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
}
|
||||||
|
.route-row:last-child { border-bottom: none; }
|
||||||
|
.route-tag {
|
||||||
|
font-size: 7.5pt;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tag-ki { background: #dbeafe; color: #1d4ed8; }
|
||||||
|
.tag-rss { background: #dcfce7; color: #166534; }
|
||||||
|
.route-arrow { color: #94a3b8; font-weight: bold; }
|
||||||
|
.route-dest { color: #374151; }
|
||||||
|
|
||||||
|
/* Zugangsdaten */
|
||||||
|
.access-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.access-item {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
.access-item .label {
|
||||||
|
font-size: 7pt;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
.access-item .value {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
padding-top: 7px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.footer span { font-size: 7.5pt; color: #94a3b8; }
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body { padding: 0; }
|
||||||
|
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<div class="logo-icon">📝</div>
|
||||||
|
<div>
|
||||||
|
<div class="badge">WordPress · KI · RSS</div>
|
||||||
|
<h1>Redax-WP</h1>
|
||||||
|
<div class="subtitle">KI-gestütztes Redaktionssystem für WordPress — selbst gehostet</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
Stand: Februar 2026<br>
|
||||||
|
CT 113 · pve-hetzner<br>
|
||||||
|
<strong>redax.orbitalo.net</strong><br>
|
||||||
|
deutschlandblog.orbitalo.net
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Intro -->
|
||||||
|
<div class="intro">
|
||||||
|
Redax-WP verbindet ein KI-Dashboard direkt mit WordPress. Artikel werden generiert, SEO-optimiert und mit einem Klick veröffentlicht. Parallel importiert ein RSS-Manager Fremdinhalte automatisch — ohne dass der Redakteur eingreifen muss.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Workflow -->
|
||||||
|
<div class="workflow">
|
||||||
|
<h3>⚡ Workflow</h3>
|
||||||
|
<div class="workflow-steps">
|
||||||
|
<div class="step"><span class="step-icon">🔗</span><span class="step-label">Quelle / Thema</span><span class="step-sub">URL oder Freitext</span></div>
|
||||||
|
<div class="arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">🤖</span><span class="step-label">KI generiert</span><span class="step-sub">OpenRouter API</span></div>
|
||||||
|
<div class="arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">✏️</span><span class="step-label">Redigieren</span><span class="step-sub">Markdown + WP-Preview</span></div>
|
||||||
|
<div class="arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">🔍</span><span class="step-label">SEO befüllen</span><span class="step-sub">Titel, Meta, Keyword</span></div>
|
||||||
|
<div class="arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">🖼️</span><span class="step-label">Bild & Kategorie</span><span class="step-sub">Featured Image</span></div>
|
||||||
|
<div class="arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">📅</span><span class="step-label">Einplanen</span><span class="step-sub">Datum & Uhrzeit</span></div>
|
||||||
|
<div class="arrow">›</div>
|
||||||
|
<div class="step"><span class="step-icon">🚀</span><span class="step-label">Veröffentlichen</span><span class="step-sub">WordPress + Telegram</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature-Boxen (3 Spalten) -->
|
||||||
|
<div class="three-col">
|
||||||
|
|
||||||
|
<div class="box blue">
|
||||||
|
<h3>✍️ KI-Redaktion</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Artikel generieren via OpenRouter</li>
|
||||||
|
<li>GPT-4o, Claude, Mistral wählbar</li>
|
||||||
|
<li>Mehrere Schreibstile & Töne</li>
|
||||||
|
<li>Markdown-Editor mit WP-Vorschau</li>
|
||||||
|
<li>Prompt-Bibliothek verwaltbar</li>
|
||||||
|
<li>Versionierung bei Neu-Generierung</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box green">
|
||||||
|
<h3>📰 RSS Feed-Import</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Beliebig viele Feeds verwaltbar</li>
|
||||||
|
<li>Automatischer Import (stündlich)</li>
|
||||||
|
<li>Duplikat-Schutz via GUID</li>
|
||||||
|
<li>Blacklist-Filter für Themen</li>
|
||||||
|
<li>Featured Image via og:image</li>
|
||||||
|
<li>Optional: KI-Umschreiben vor Publish</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box purple">
|
||||||
|
<h3>🔍 WordPress & SEO</h3>
|
||||||
|
<ul>
|
||||||
|
<li>WordPress REST API Integration</li>
|
||||||
|
<li>Yoast SEO: Titel, Meta, Keyword</li>
|
||||||
|
<li>Kategorien & Tags direkt wählbar</li>
|
||||||
|
<li>Featured Image Upload automatisch</li>
|
||||||
|
<li>Permalink-Struktur /%postname%/</li>
|
||||||
|
<li>Entwurf, Geplant oder Sofort-Publish</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zweite Reihe: Routing + Planung + Zugang -->
|
||||||
|
<div class="two-col">
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
|
||||||
|
<div class="box orange">
|
||||||
|
<h3>📲 Telegram & Alerts</h3>
|
||||||
|
<ul>
|
||||||
|
<li>KI-Artikel als Teaser posten</li>
|
||||||
|
<li>Morgen-Briefing 10:00 Uhr</li>
|
||||||
|
<li>Fehler-Alarm bei Posting-Fehlern</li>
|
||||||
|
<li>Mehrere Reviewer unterstützt</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box teal">
|
||||||
|
<h3>🗓️ Planung & History</h3>
|
||||||
|
<ul>
|
||||||
|
<li>7-Tage-Redaktionskalender</li>
|
||||||
|
<li>Umplanen & Löschen im Board</li>
|
||||||
|
<li>Veröffentlichungs-History</li>
|
||||||
|
<li>Strukturiertes JSON-Logging</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="routing">
|
||||||
|
<h3>🔀 Inhalts-Routing</h3>
|
||||||
|
<div class="route-row">
|
||||||
|
<span class="route-tag tag-ki">KI-Artikel</span>
|
||||||
|
<span class="route-arrow">→</span>
|
||||||
|
<span class="route-dest">WordPress veröffentlichen</span>
|
||||||
|
</div>
|
||||||
|
<div class="route-row">
|
||||||
|
<span class="route-tag tag-ki">KI-Artikel</span>
|
||||||
|
<span class="route-arrow">→</span>
|
||||||
|
<span class="route-dest">Telegram Kanal (Teaser)</span>
|
||||||
|
</div>
|
||||||
|
<div class="route-row">
|
||||||
|
<span class="route-tag tag-rss">RSS-Artikel</span>
|
||||||
|
<span class="route-arrow">→</span>
|
||||||
|
<span class="route-dest">Nur WordPress</span>
|
||||||
|
</div>
|
||||||
|
<div class="route-row">
|
||||||
|
<span class="route-tag tag-rss">RSS-Artikel</span>
|
||||||
|
<span class="route-arrow">✗</span>
|
||||||
|
<span class="route-dest" style="color:#94a3b8;">Kein Telegram-Post</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<div class="label" style="font-size:7.5pt;color:#94a3b8;font-weight:700;text-transform:uppercase;letter-spacing:0.3px;margin-bottom:4px;">Zugang</div>
|
||||||
|
<div class="access-grid">
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Dashboard</div>
|
||||||
|
<div class="value">redax.orbitalo.net</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Blog</div>
|
||||||
|
<div class="value">deutschlandblog.orbitalo.net</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">WP-Admin Login</div>
|
||||||
|
<div class="value">admin / Redax2026!</div>
|
||||||
|
</div>
|
||||||
|
<div class="access-item">
|
||||||
|
<div class="label">Stack</div>
|
||||||
|
<div class="value">Flask · WordPress · MySQL · Docker</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="footer">
|
||||||
|
<span>Redax-WP · Orbitalo Homelab · Stand Februar 2026 · CT 113 pve-hetzner</span>
|
||||||
|
<span>github.com/Orbitalo/Redax-Wordpress</span>
|
||||||
|
<span>Drucken: Querformat · DIN A4 · Ränder: Minimal</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -108,3 +108,15 @@ docker-compose.yml
|
||||||
- [ ] .env mit echten Credentials befüllen (OpenRouter, Telegram)
|
- [ ] .env mit echten Credentials befüllen (OpenRouter, Telegram)
|
||||||
- [x] Cloudflare Tunnel → https://redax.orbitalo.net
|
- [x] Cloudflare Tunnel → https://redax.orbitalo.net
|
||||||
- [ ] Erste Feeds hinzufügen
|
- [ ] Erste Feeds hinzufügen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 27.02.2026 — DB-Cleanup-Job + Datenbank-Hygiene
|
||||||
|
- **Automatischer Cleanup:** Neuer Scheduler-Job läuft jeden Sonntag 03:00 Uhr
|
||||||
|
- `feed_items` (published/rejected) älter als 60 Tage → automatisch gelöscht
|
||||||
|
- `feed_items` (new/unbearbeitet) älter als 30 Tage → automatisch gelöscht
|
||||||
|
- `post_history` älter als 90 Tage → automatisch gelöscht
|
||||||
|
- VACUUM nach Cleanup → DB bleibt kompakt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,26 @@ def job_fetch_feeds():
|
||||||
rss_fetcher.run_all_feeds()
|
rss_fetcher.run_all_feeds()
|
||||||
|
|
||||||
|
|
||||||
|
def job_cleanup_db():
|
||||||
|
"""Woechentliche DB-Bereinigung: alte Feed-Items und Post-History loeschen."""
|
||||||
|
import sqlite3, os
|
||||||
|
db_path = os.environ.get('DB_PATH', '/data/redax.db')
|
||||||
|
con = sqlite3.connect(db_path)
|
||||||
|
r1 = con.execute(
|
||||||
|
"DELETE FROM feed_items WHERE status IN ('published', 'rejected') "
|
||||||
|
"AND fetched_at < datetime('now', '-60 days')"
|
||||||
|
).rowcount
|
||||||
|
r2 = con.execute(
|
||||||
|
"DELETE FROM feed_items WHERE status = 'new' "
|
||||||
|
"AND fetched_at < datetime('now', '-30 days')"
|
||||||
|
).rowcount
|
||||||
|
r3 = con.execute("DELETE FROM post_history WHERE posted_at < datetime('now', '-90 days')").rowcount
|
||||||
|
con.execute("VACUUM")
|
||||||
|
con.commit()
|
||||||
|
con.close()
|
||||||
|
flog.info('db_cleanup', fi_processed=r1, fi_stale=r2, ph_del=r3)
|
||||||
|
|
||||||
|
|
||||||
def job_morning_briefing():
|
def job_morning_briefing():
|
||||||
today = date.today().strftime('%Y-%m-%d')
|
today = date.today().strftime('%Y-%m-%d')
|
||||||
tomorrow = (date.today() + timedelta(days=1)).strftime('%Y-%m-%d')
|
tomorrow = (date.today() + timedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
|
@ -444,6 +464,8 @@ def start_scheduler():
|
||||||
scheduler.add_job(job_fetch_feeds, 'interval', minutes=30, id='fetch_feeds')
|
scheduler.add_job(job_fetch_feeds, 'interval', minutes=30, id='fetch_feeds')
|
||||||
# Morgen-Briefing: täglich 10:00
|
# Morgen-Briefing: täglich 10:00
|
||||||
scheduler.add_job(job_morning_briefing, 'cron', hour=10, minute=0, id='morning_briefing')
|
scheduler.add_job(job_morning_briefing, 'cron', hour=10, minute=0, id='morning_briefing')
|
||||||
|
# Woechentliche DB-Bereinigung (Sonntags 03:00 Uhr)
|
||||||
|
scheduler.add_job(job_cleanup_db, 'cron', day_of_week='sun', hour=3, minute=0, id='db_cleanup')
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
flog.info('scheduler_started')
|
flog.info('scheduler_started')
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue