From e607b1fba092b62d0a648208526aa6cafec70fdf Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 21:23:27 +0700 Subject: [PATCH] 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 --- arakava-news/STATE.md | 57 +- .../artikel/esp32-serie-teil1-entwurf.md | 138 +++++ .../seafile-assets/Fließschaltbild v4 HTML.md | 488 ++++++++++++++++++ .../Fließschaltbild von Clode 4.6 als HTML.md | 188 +++++++ .../seafile-assets/Kombipufferspeicher.webp | Bin 0 -> 21450 bytes .../seafile-assets/Sensoren und Bilder.md | 274 ++++++++++ flugpreisscanner/STATE.md | 1 + fuenfvoacht/src/bot.py | 417 +++++++++++++++ fuenfvoracht/KURZUEBERSICHT.html | 347 +++++++++++++ fuenfvoracht/README.md | 231 +++++++++ fuenfvoracht/STATE.md | 28 +- fuenfvoracht/src/bot.py | 19 + infrastructure/STATE.md | 1 + redax-wp/KURZUEBERSICHT.html | 408 +++++++++++++++ redax-wp/STATE.md | 12 + redax-wp/src/app.py | 22 + 16 files changed, 2619 insertions(+), 12 deletions(-) create mode 100644 arakava-news/artikel/esp32-serie-teil1-entwurf.md create mode 100644 arakava-news/artikel/seafile-assets/Fließschaltbild v4 HTML.md create mode 100644 arakava-news/artikel/seafile-assets/Fließschaltbild von Clode 4.6 als HTML.md create mode 100644 arakava-news/artikel/seafile-assets/Kombipufferspeicher.webp create mode 100644 arakava-news/artikel/seafile-assets/Sensoren und Bilder.md create mode 100644 fuenfvoacht/src/bot.py create mode 100644 fuenfvoracht/KURZUEBERSICHT.html create mode 100644 fuenfvoracht/README.md create mode 100644 redax-wp/KURZUEBERSICHT.html diff --git a/arakava-news/STATE.md b/arakava-news/STATE.md index cd826a83..71fd059a 100644 --- a/arakava-news/STATE.md +++ b/arakava-news/STATE.md @@ -24,9 +24,14 @@ $8.88 verbleibend ## URLs - Blog: https://arakava-news-2.orbitalo.net -- Admin: https://arakava-news-2.orbitalo.net/wp-admin (admin / eJIyhW0p5PFacjvvKGufKeXS) -- RSS Manager: http://100.113.244.101:8080 (admin / astral66) +- Admin: https://arakava-news-2.orbitalo.net/wp-admin (admin / eJIyhW0p5PFacjvvKGufKeXS) — ⚠️ nur via Tailscale erreichbar! +- 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) +- 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 | CT | Dienst | Tailscale | @@ -54,10 +59,58 @@ $8.88 verbleibend | 16 | Antispiegel | 08:30/14:30/20:30 | | 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/) poster.py, scheduler.py, app.py, db.py ## Ä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: Telegram auf HTML-Modus (Sonderzeichen-Fix) - 24.02.2026: Werbeartikel-Blacklist (Anzeige:, Sponsored, etc.) diff --git a/arakava-news/artikel/esp32-serie-teil1-entwurf.md b/arakava-news/artikel/esp32-serie-teil1-entwurf.md new file mode 100644 index 00000000..f26a0b9f --- /dev/null +++ b/arakava-news/artikel/esp32-serie-teil1-entwurf.md @@ -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) diff --git a/arakava-news/artikel/seafile-assets/Fließschaltbild v4 HTML.md b/arakava-news/artikel/seafile-assets/Fließschaltbild v4 HTML.md new file mode 100644 index 00000000..1c496026 --- /dev/null +++ b/arakava-news/artikel/seafile-assets/Fließschaltbild v4 HTML.md @@ -0,0 +1,488 @@ + + + + +Heizungsanlage - Hydraulikplan + + + + +

Heizungsanlage - Hydraulikplan

+

Klicke einen Betriebsmodus um die Energiefluesse zu sehen

+ +
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PUFFERSPEICHER + + +WW +KW + + +Öl-VL +Holz-VL + + +Öl-RL +Holz-RL + + +Hz-VL +Hz-RL + +heiss +kalt + + + +SOLAR +Waermetauscher + +VL +RL + + + +WARMWASSER +Waermetauscher + + + + + + + + + + Oel-Kessel + VL + RL + + + + + + + + + + + + + + Holzvergaser + VL + RL + + + + + + + + + + + + + + + + + + + Thermo-Solar + VL + RL + + + + + + + + + + + +WW-Ausgang + + +KW-Eingang + + + + + + +Mischer + + + + +Pumpe + + +VL +RL + + + + + + + + + + + + + + + + + +HK-1 + + + + + + + + + + + + + + +HK-2 + + + + + + + + + + + + + + +HK-3 + + + + + + + + + + + + + + +HK-4 + + +VL-Verteiler +RL-Sammler + + + + + Vorlauf (heiss) + + Ruecklauf (kalt) + + Solar + + Warmwasser + + Kaltwasser + + + +
+ +
+ Waehle oben einen Betriebsmodus aus. +
+ + + + + \ No newline at end of file diff --git a/arakava-news/artikel/seafile-assets/Fließschaltbild von Clode 4.6 als HTML.md b/arakava-news/artikel/seafile-assets/Fließschaltbild von Clode 4.6 als HTML.md new file mode 100644 index 00000000..1880bb37 --- /dev/null +++ b/arakava-news/artikel/seafile-assets/Fließschaltbild von Clode 4.6 als HTML.md @@ -0,0 +1,188 @@ + + + + + +Heizungsanlage – Hydraulisches Schaltbild + + + +

🏠 Heizungsanlage – Hydraulisches Schaltbild

+

Interaktive Energiefluss-Visualisierung · Klicke einen Betriebsmodus

+
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +PUFFERSPEICHER +▲ heiß +▼ kalt + + + + + + + + + + + + +WW-Wärmetauscher + + + + + +🛢️ +Öl-Kessel + + + +🪵 +Holzvergaser + + + +☀️ +Thermo-Solar + + + + + + + + +WW-Ausgang ⑬ + + +KW-Eingang + +⑫ Mischung + + + + + + + +Mischer + + + + +Pumpe + + + + +Pumpe + + + + + 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( + "🕗 FünfVorAcht — Review Bot\n\n" + "Artikel werden im Dashboard erstellt und eingeplant.\n" + "Hier kannst du sie freigeben oder letzte Änderungen vornehmen.\n\n" + "Befehle:\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"📅 {d} — {len(articles)} Slot(s)\n"] + for art in articles: + lines.append( + f"{art['post_time']} Uhr · " + 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 = ["📆 Nächste 3 Tage:\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"{d}: {slots}") + else: + lines.append(f"❌ {d} — 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"✅ Freigegeben!\n\n" + f"Wird automatisch um {post_time} Uhr 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"✏️ Bearbeiten — {date_str} {post_time} Uhr\n\n" + f"Schick mir den neuen Text als nächste Nachricht.\n\n" + f"Aktueller Text:\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"✏️ Aktualisiert — {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"📤 Fünf vor Acht gepostet!\n{d} · {current_slot} Uhr" + ) + except Exception as e: + flog.posting_failed(d, current_slot, str(e)) + await notify_reviewers( + bot, + f"❌ Posting fehlgeschlagen!\n\n" + f"📅 {d} · ⏰ {current_slot} Uhr\n" + f"Kanal: {CHANNEL_ID}\n\n" + f"Ursache: {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"📋 Review: {d} · {pt} Uhr\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"☀️ Guten Morgen — FünfVorAcht Briefing\n", + f"📅 Heute: {d}"] + + 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📆 Nächste 3 Tage:") + 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"⚠️ Noch nicht freigegeben!\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() diff --git a/fuenfvoracht/KURZUEBERSICHT.html b/fuenfvoracht/KURZUEBERSICHT.html new file mode 100644 index 00000000..d421d346 --- /dev/null +++ b/fuenfvoracht/KURZUEBERSICHT.html @@ -0,0 +1,347 @@ + + + + +FünfVorAcht — Kurzübersicht + + + + + +
+
+
🕗 Automatisierung
+

FünfVorAcht

+
KI-gestützter Telegram-Kanal-Poster mit Redaktions-Dashboard
+
+
+ Stand: Februar 2026
+ CT 112 · pve-hetzner
+ fuenfvoacht.orbitalo.net +
+
+ + +
+ 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. +
+ + +
+

⚡ Workflow

+
+
🔗Quelle /
Thema eingeben
+
+
🤖KI generiert
Artikel
+
+
✏️Redigieren +
Vorschau
+
+
📅Datum +
Uhrzeit wählen
+
+
📩Review per
Telegram
+
+
Freigeben
oder bearbeiten
+
+
📤Automatisch
gepostet
+
+
+ + +
+ +
+

✍️ Artikel & KI

+
    +
  • KI-Generierung via OpenRouter (GPT-4o, Claude, Mistral u.a.)
  • +
  • 7 verschiedene Schreibstile wählbar (sarkastisch, sachlich, humorvoll…)
  • +
  • Echtzeit Telegram-Vorschau während dem Schreiben
  • +
  • Markenzeichen wird automatisch angehängt
  • +
  • Versionsverlauf bei Neu-Generierung
  • +
+
+ +
+

📅 Zeitplanung

+
    +
  • 15-Minuten-Zeitraster (06:00 – 23:45 Uhr)
  • +
  • Mehrere Artikel pro Tag planbar
  • +
  • 7-Tage-Redaktionskalender im Dashboard
  • +
  • Umplanen & Löschen direkt im Board
  • +
  • Slot-Konfliktschutz (kein doppeltes Buchen)
  • +
+
+ +
+

🤖 Telegram Bot

+
    +
  • Review-Anfrage an alle Redakteure
  • +
  • Inline-Buttons: ✅ Freigeben / ✏️ Bearbeiten
  • +
  • Morgen-Briefing täglich 10:00 Uhr
  • +
  • Nachmittags-Reminder um 18:00 Uhr
  • +
  • Sofort-Alarm bei Posting-Fehlern
  • +
+
+ +
+

⚙️ Dashboard & Verwaltung

+
    +
  • Webbasiertes Dashboard (Browser)
  • +
  • Mehrere Redakteure verwaltbar
  • +
  • Prompt-Bibliothek editierbar
  • +
  • Posting-History der letzten 30 Tage
  • +
  • OpenRouter-Kontostand abrufbar
  • +
+
+ +
+ + +
+

🔑 Zugang & Technik

+
+
+
Dashboard URL
+
fuenfvoacht.orbitalo.net
+
+
+
Telegram Bot
+
@Diendemleben_bot
+
+
+
Standard Posting-Zeit
+
19:55 Uhr (konfigurierbar)
+
+
+
Server
+
CT 112 · pve-hetzner
+
+
+
GitHub
+
github.com/Orbitalo/F-nf-vor-Acht
+
+
+
Stack
+
Python · Flask · Docker · SQLite
+
+
+
+ + + + + + diff --git a/fuenfvoracht/README.md b/fuenfvoracht/README.md new file mode 100644 index 00000000..b8982068 --- /dev/null +++ b/fuenfvoracht/README.md @@ -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 diff --git a/fuenfvoracht/STATE.md b/fuenfvoracht/STATE.md index 1b082585..fdd2100e 100644 --- a/fuenfvoracht/STATE.md +++ b/fuenfvoracht/STATE.md @@ -1,5 +1,5 @@ # STATE: FünfVorAcht -**Stand: 26.02.2026** +**Stand: 27.02.2026** --- @@ -43,11 +43,10 @@ KI generiert Artikel (OpenRouter) ↓ 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) - ↓ -[✅ Freigeben] [✏️ Bearbeiten] +Status → approved (direkt, kein Review nötig) +Info-Nachricht an Redakteure: "Artikel eingeplant für XX:XX" ↓ APScheduler postet automatisch zum Zeitslot ↓ @@ -89,11 +88,11 @@ Bestätigung + Markenzeichen automatisch angehängt ### Article-Status-Lifecycle ``` -draft → scheduled → sent_to_bot → approved → posted - ↘ rejected +draft → approved → posted ↘ 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 - `UNIQUE(date, post_time)` — Konflikte technisch ausgeschlossen @@ -138,9 +137,9 @@ draft → scheduled → sent_to_bot → approved → posted | `/heute` | Alle Slots des heutigen Tages | | `/queue` | Nächste 3 Tage mit Slots | | `/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 | -| 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 | | Willkommensnachricht | Automatisch bei neuem Redakteur | @@ -181,6 +180,15 @@ Events: `article_generated`, `article_saved`, `article_scheduled`, `article_sent ## 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 - **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 diff --git a/fuenfvoracht/src/bot.py b/fuenfvoracht/src/bot.py index 969c4569..995bb03c 100644 --- a/fuenfvoracht/src/bot.py +++ b/fuenfvoracht/src/bot.py @@ -315,6 +315,23 @@ async def job_morning_briefing(bot: Bot): 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): """Nachmittags-Reminder wenn Hauptslot noch nicht freigegeben.""" d = today_str() @@ -382,6 +399,8 @@ def main(): scheduler.add_job(job_reminder_afternoon, 'cron', hour=rem_a_h, minute=rem_a_m, 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() flog.info('bot_started', post_time=post_t, diff --git a/infrastructure/STATE.md b/infrastructure/STATE.md index 6edcae08..4062e7ef 100644 --- a/infrastructure/STATE.md +++ b/infrastructure/STATE.md @@ -37,6 +37,7 @@ ## Routing - Cloudflare Tunnel CT 101: arakava-news-2.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 ## Zugangsdaten diff --git a/redax-wp/KURZUEBERSICHT.html b/redax-wp/KURZUEBERSICHT.html new file mode 100644 index 00000000..198d78c3 --- /dev/null +++ b/redax-wp/KURZUEBERSICHT.html @@ -0,0 +1,408 @@ + + + + +Redax-WP — Kurzübersicht + + + + + +
+
+
📝
+
+
WordPress · KI · RSS
+

Redax-WP

+
KI-gestütztes Redaktionssystem für WordPress — selbst gehostet
+
+
+
+ Stand: Februar 2026
+ CT 113 · pve-hetzner
+ redax.orbitalo.net
+ deutschlandblog.orbitalo.net +
+
+ + +
+ 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. +
+ + +
+

⚡ Workflow

+
+
🔗Quelle / ThemaURL oder Freitext
+
+
🤖KI generiertOpenRouter API
+
+
✏️RedigierenMarkdown + WP-Preview
+
+
🔍SEO befüllenTitel, Meta, Keyword
+
+
🖼️Bild & KategorieFeatured Image
+
+
📅EinplanenDatum & Uhrzeit
+
+
🚀VeröffentlichenWordPress + Telegram
+
+
+ + +
+ +
+

✍️ KI-Redaktion

+
    +
  • Artikel generieren via OpenRouter
  • +
  • GPT-4o, Claude, Mistral wählbar
  • +
  • Mehrere Schreibstile & Töne
  • +
  • Markdown-Editor mit WP-Vorschau
  • +
  • Prompt-Bibliothek verwaltbar
  • +
  • Versionierung bei Neu-Generierung
  • +
+
+ +
+

📰 RSS Feed-Import

+
    +
  • Beliebig viele Feeds verwaltbar
  • +
  • Automatischer Import (stündlich)
  • +
  • Duplikat-Schutz via GUID
  • +
  • Blacklist-Filter für Themen
  • +
  • Featured Image via og:image
  • +
  • Optional: KI-Umschreiben vor Publish
  • +
+
+ +
+

🔍 WordPress & SEO

+
    +
  • WordPress REST API Integration
  • +
  • Yoast SEO: Titel, Meta, Keyword
  • +
  • Kategorien & Tags direkt wählbar
  • +
  • Featured Image Upload automatisch
  • +
  • Permalink-Struktur /%postname%/
  • +
  • Entwurf, Geplant oder Sofort-Publish
  • +
+
+ +
+ + +
+ +
+ +
+

📲 Telegram & Alerts

+
    +
  • KI-Artikel als Teaser posten
  • +
  • Morgen-Briefing 10:00 Uhr
  • +
  • Fehler-Alarm bei Posting-Fehlern
  • +
  • Mehrere Reviewer unterstützt
  • +
+
+ +
+

🗓️ Planung & History

+
    +
  • 7-Tage-Redaktionskalender
  • +
  • Umplanen & Löschen im Board
  • +
  • Veröffentlichungs-History
  • +
  • Strukturiertes JSON-Logging
  • +
+
+ +
+ +
+

🔀 Inhalts-Routing

+
+ KI-Artikel + + WordPress veröffentlichen +
+
+ KI-Artikel + + Telegram Kanal (Teaser) +
+
+ RSS-Artikel + + Nur WordPress +
+
+ RSS-Artikel + + Kein Telegram-Post +
+
+
Zugang
+
+
+
Dashboard
+
redax.orbitalo.net
+
+
+
Blog
+
deutschlandblog.orbitalo.net
+
+
+
WP-Admin Login
+
admin / Redax2026!
+
+
+
Stack
+
Flask · WordPress · MySQL · Docker
+
+
+
+
+ +
+ + + + + + diff --git a/redax-wp/STATE.md b/redax-wp/STATE.md index fb726d52..feb58c75 100644 --- a/redax-wp/STATE.md +++ b/redax-wp/STATE.md @@ -108,3 +108,15 @@ docker-compose.yml - [ ] .env mit echten Credentials befüllen (OpenRouter, Telegram) - [x] Cloudflare Tunnel → https://redax.orbitalo.net - [ ] 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 + diff --git a/redax-wp/src/app.py b/redax-wp/src/app.py index 6332e6d5..795c9b8a 100644 --- a/redax-wp/src/app.py +++ b/redax-wp/src/app.py @@ -69,6 +69,26 @@ def job_fetch_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(): today = date.today().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') # Morgen-Briefing: täglich 10:00 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() flog.info('scheduler_started')