diff --git a/fuenfvoracht/src/app.py b/fuenfvoracht/src/app.py
index eea568dfa..8de67eeea 100644
--- a/fuenfvoracht/src/app.py
+++ b/fuenfvoracht/src/app.py
@@ -2,6 +2,7 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for, R
from functools import wraps
from datetime import datetime, date, timedelta
import os
+import html
import asyncio
import logging
import requests as req_lib
@@ -76,8 +77,13 @@ def planning_days(count=7):
return [(t + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(count)]
+
+def sanitize_telegram_html(text: str) -> str:
+ """Escape <>& so Telegram HTML parse_mode does not fail on stray tags from KI/Quellen."""
+ return html.escape(text or "", quote=False)
+
def with_branding(content: str) -> str:
- text = (content or "").rstrip()
+ text = sanitize_telegram_html((content or "").rstrip())
if BRAND_MARKER in text:
return text
return f"{text}\n\n{BRAND_SIGNATURE}" if text else BRAND_SIGNATURE
@@ -105,6 +111,50 @@ def notify_all_reviewers(text, reply_markup=None):
return results
+def _notify_auto5vor8_review(date_str, post_time, title="", review_reason="", has_draft=True):
+ """Telegram an Reviewer: Auto-5vor8 braucht manuelle PrΓΌfung."""
+ title_line = sanitize_telegram_html(title[:120]) if title else "Dr.-Bine-Artikel"
+ reason_line = sanitize_telegram_html(review_reason) if review_reason else ""
+ if has_draft:
+ msg = (
+ f"β οΈ Auto-5vor8: Bitte manuell prΓΌfen\n\n"
+ f"π
Slot: {date_str} {post_time}\n"
+ f"π° {title_line}\n"
+ )
+ if reason_line:
+ msg += f"\nβ Faktencheck: {reason_line}\n"
+ msg += (
+ f"\nDer KI-Text hat die automatische QualitΓ€tsprΓΌfung nicht bestanden. "
+ f"Ein Entwurf liegt im Dashboard β bitte lesen, ggf. anpassen und freigeben.\n\n"
+ f"π https://fuenfvoracht.orbitalo.net"
+ )
+ else:
+ msg = (
+ f"π¨ Auto-5vor8: Generierung fehlgeschlagen\n\n"
+ f"π° {title_line}\n"
+ )
+ if reason_line:
+ msg += f"\nβ {reason_line}\n"
+ msg += (
+ f"\nEs wurde kein Telegram-Text erzeugt. Bitte im Dashboard manuell anlegen.\n\n"
+ f"π https://fuenfvoracht.orbitalo.net"
+ )
+ notify_all_reviewers(msg)
+
+
+def _notify_auto5vor8_scheduled(date_str, post_time, title=""):
+ """Telegram: Auto-5vor8 hat erfolgreich eingeplant."""
+ title_line = sanitize_telegram_html(title[:120]) if title else "Dr.-Bine-Artikel"
+ msg = (
+ f"β
Auto-5vor8: Artikel eingeplant\n\n"
+ f"π
{date_str} Β· {post_time} Uhr\n"
+ f"π° {title_line}\n\n"
+ f"Wird automatisch gepostet β im Dashboard noch editierbar.\n"
+ f"π https://fuenfvoracht.orbitalo.net"
+ )
+ notify_all_reviewers(msg)
+
+
# ββ Main Dashboard ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.route('/')
@@ -168,7 +218,7 @@ def index():
@app.route('/history')
def history():
- articles = db.get_recent_posted_articles(30)
+ articles = db.get_recent_articles(30)
return render_template('history.html', articles=articles)
@@ -354,7 +404,19 @@ def api_save(date_str):
if existing:
db.update_article_content(date_str, content, post_time=post_time)
flog.article_saved(date_str, post_time)
- return jsonify({'success': True})
+ return jsonify({'success': True})
+ all_today = db.get_articles_by_date(date_str)
+ candidate = next((a for a in all_today if a['status'] in ('draft', 'scheduled', 'sent_to_bot', 'approved', 'pending_review')), None)
+ if candidate:
+ old_time = candidate['post_time']
+ conn = db.get_conn()
+ conn.execute("UPDATE articles SET post_time=? WHERE date=? AND post_time=?", (post_time, date_str, old_time))
+ conn.commit()
+ conn.close()
+ db.update_article_content(date_str, content, post_time=post_time)
+ flog.article_saved(date_str, post_time)
+ return jsonify({'success': True})
+ return jsonify({'success': False, 'error': 'Kein Artikel fΓΌr diesen Tag vorhanden'})
@app.route('/api/article//schedule', methods=['POST'])
@@ -365,33 +427,38 @@ def api_schedule(date_str):
notify_mode = data.get('notify_mode', 'auto') # sofort | auto | custom
notify_at_custom = data.get('notify_at')
- # Slot-Konflikt prΓΌfen
existing = db.get_article_by_date(date_str, post_time)
if not existing:
- return jsonify({'success': False, 'error': 'Kein Artikel fΓΌr diesen Slot'})
+ all_today = db.get_articles_by_date(date_str)
+ candidate = next((a for a in all_today if a['status'] in ('draft', 'scheduled', 'sent_to_bot', 'pending_review')), None)
+ if not candidate:
+ return jsonify({'success': False, 'error': 'Kein Artikel fΓΌr diesen Tag gefunden'})
+ old_time = candidate['post_time']
+ conn = db.get_conn()
+ conn.execute(
+ "UPDATE articles SET post_time=? WHERE date=? AND post_time=?",
+ (post_time, date_str, old_time)
+ )
+ conn.commit()
+ conn.close()
- # notify_at berechnen
- import pytz
- from datetime import datetime as dt
- berlin = pytz.timezone('Europe/Berlin')
- now_berlin = dt.now(berlin)
- article_dt = berlin.localize(dt.strptime(f"{date_str} {post_time}", '%Y-%m-%d %H:%M'))
+ conn = db.get_conn()
+ ts = datetime.utcnow().isoformat()
+ conn.execute(
+ "UPDATE articles SET status='approved', scheduled_at=?, notify_at=? WHERE date=? AND post_time=?",
+ (ts, ts, date_str, post_time)
+ )
+ conn.commit()
+ conn.close()
- if notify_mode == 'sofort':
- notify_at = dt.utcnow().isoformat()
- elif notify_mode == 'custom' and notify_at_custom:
- notify_at = notify_at_custom
- else:
- # Auto: wenn heute β sofort, sonst Vortag 17:00
- if article_dt.date() == now_berlin.date():
- notify_at = dt.utcnow().isoformat()
- else:
- day_before = (article_dt - timedelta(days=1)).replace(hour=17, minute=0, second=0)
- notify_at = day_before.astimezone(pytz.utc).strftime('%Y-%m-%dT%H:%M:%S')
+ notify_all_reviewers(
+ f"π
Artikel eingeplant\n\n"
+ f"π {date_str} um {post_time} Uhr\n"
+ f"Wird automatisch gepostet."
+ )
- db.schedule_article(date_str, post_time, notify_at)
- flog.article_scheduled(date_str, post_time, notify_at)
- return jsonify({'success': True, 'notify_at': notify_at})
+ flog.article_scheduled(date_str, post_time, ts)
+ return jsonify({'success': True})
@app.route('/api/article//reschedule', methods=['POST'])
@@ -533,13 +600,35 @@ def api_skip(date_str):
return jsonify({'success': True})
-# ββ Auto-5vor8 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+# == Auto-5vor8 (API + Dashboard) ===============================================
+
+STYLE_ROTATION = [
+ ("Standard", "π Das Wesentliche"),
+ ("Leicht sarkastisch", "π Worum es wirklich geht"),
+ ("Humorvoll-serioes", "β Kurz erklaert"),
+ ("Knapp & direkt", "β‘ In Kuerze"),
+ ("Storytelling", "π Die Story"),
+ ("Wissenschaftlich nuechtern", "π¬ Befund"),
+]
+
+
+def _style_for_date(day):
+ from datetime import date as _date
+ if isinstance(day, str):
+ day = _date.fromisoformat(day)
+ return STYLE_ROTATION[day.toordinal() % len(STYLE_ROTATION)]
+
def _get_auto5vor8_settings():
conn = db.get_conn()
rows = conn.execute("SELECT key, value FROM auto_5vor8_settings").fetchall()
conn.close()
- return {r["key"]: r["value"] for r in rows}
+ settings = {r["key"]: r["value"] for r in rows}
+ if "style_rotation" not in settings:
+ settings["style_rotation"] = "1"
+ if "ai_model" not in settings or not settings["ai_model"]:
+ settings["ai_model"] = "deepseek/deepseek-v4-flash"
+ return settings
def _set_auto5vor8_setting(key, value):
@@ -553,30 +642,128 @@ def _set_auto5vor8_setting(key, value):
conn.close()
-@app.route('/auto5vor8')
+def _auto5vor8_style_info():
+ from datetime import date, timedelta
+ today = date.today()
+ tomorrow = today + timedelta(days=1)
+ t_name, t_intro = _style_for_date(today)
+ n_name, n_intro = _style_for_date(tomorrow)
+ return {
+ "styles": [{"name": n, "intro": i} for n, i in STYLE_ROTATION],
+ "today_style": t_name,
+ "today_intro": t_intro,
+ "tomorrow_style": n_name,
+ "tomorrow_intro": n_intro,
+ }
+
+
+@app.route("/api/article/create", methods=["POST"])
+def api_article_create():
+ """Erstellt einen Artikel und plant ihn direkt ein (fuer Auto-5vor8)."""
+ data = request.get_json()
+ if not data:
+ return jsonify({"success": False, "error": "Kein JSON-Body"}), 400
+ content = data.get("content", "").strip()
+ date_str = data.get("date", "").strip()
+ post_time = data.get("post_time", POST_TIME)
+ tag = data.get("tag", "allgemein")
+ source = data.get("source", "")
+ image_url = data.get("image_url", "")
+ if not content or not date_str:
+ return jsonify({"success": False, "error": "content und date erforderlich"}), 400
+ if db.slot_is_taken(date_str, post_time):
+ return jsonify({"success": False, "error": f"Slot {date_str} {post_time} belegt"}), 409
+ article_status = (data.get("status") or "approved").strip().lower()
+ if article_status not in ("approved", "draft"):
+ article_status = "approved"
+ db.create_article(date_str, source, content, None, tag, post_time)
+ from datetime import datetime as _dt
+ ts = _dt.utcnow().isoformat()
+ conn = db.get_conn()
+ if article_status == "approved":
+ conn.execute(
+ "UPDATE articles SET status='approved', scheduled_at=?, image_url=? WHERE date=? AND post_time=?",
+ (ts, image_url, date_str, post_time)
+ )
+ else:
+ conn.execute(
+ "UPDATE articles SET status='draft', image_url=? WHERE date=? AND post_time=?",
+ (image_url, date_str, post_time)
+ )
+ conn.commit()
+ conn.close()
+ if article_status == "draft":
+ _notify_auto5vor8_review(
+ date_str,
+ post_time,
+ title=(data.get("title") or "").strip(),
+ review_reason=(data.get("review_reason") or "").strip(),
+ has_draft=True,
+ )
+ elif (data.get("title") or "").strip():
+ _notify_auto5vor8_scheduled(
+ date_str,
+ post_time,
+ title=(data.get("title") or "").strip(),
+ )
+ return jsonify({"success": True, "date": date_str, "post_time": post_time, "status": article_status})
+
+
+@app.route("/api/auto5vor8/notify-failed", methods=["POST"])
+def api_auto5vor8_notify_failed():
+ """Benachrichtigung wenn Auto-5vor8 gar keinen Text erzeugen konnte."""
+ data = request.get_json() or {}
+ _notify_auto5vor8_review(
+ date_str=(data.get("date") or today_str()).strip(),
+ post_time=(data.get("post_time") or POST_TIME).strip(),
+ title=(data.get("title") or "").strip(),
+ review_reason=(data.get("review_reason") or "KI-Text konnte nicht generiert werden").strip(),
+ has_draft=False,
+ )
+ return jsonify({"success": True})
+
+
+@app.route("/api/auto5vor8/settings", methods=["GET"])
+def api_auto5vor8_settings_get():
+ settings = _get_auto5vor8_settings()
+ settings.update(_auto5vor8_style_info())
+ return jsonify(settings)
+
+
+@app.route("/api/auto5vor8/settings", methods=["POST"])
+def api_auto5vor8_settings_post():
+ data = request.get_json() or {}
+ for key, val in data.items():
+ if val is not None:
+ _set_auto5vor8_setting(key, str(val).strip())
+ return jsonify({"success": True})
+
+
+@app.route("/auto5vor8")
def auto5vor8():
settings = _get_auto5vor8_settings()
- return render_template('auto5vor8.html', settings=settings)
+ style_info = _auto5vor8_style_info()
+ return render_template("auto5vor8.html", settings=settings, style_info=style_info)
-@app.route('/auto5vor8/toggle', methods=['POST'])
+@app.route("/auto5vor8/toggle", methods=["POST"])
def auto5vor8_toggle():
current = _get_auto5vor8_settings()
- new_val = '0' if current.get('enabled') == '1' else '1'
- _set_auto5vor8_setting('enabled', new_val)
- state = 'aktiviert' if new_val == '1' else 'deaktiviert'
- logger.info('Auto-5vor8 %s', state)
- return redirect(url_for('auto5vor8'))
+ new_val = "0" if current.get("enabled") == "1" else "1"
+ _set_auto5vor8_setting("enabled", new_val)
+ state = "aktiviert" if new_val == "1" else "deaktiviert"
+ logger.info("Auto-5vor8 %s", state)
+ return redirect(url_for("auto5vor8"))
-@app.route('/auto5vor8/settings', methods=['POST'])
+@app.route("/auto5vor8/settings", methods=["POST"])
def auto5vor8_save_settings():
- for field in ('prompt', 'post_time', 'ai_model', 'feed_id', 'tag'):
+ for field in ("prompt", "post_time", "ai_model", "feed_id", "tag", "style_rotation"):
val = request.form.get(field)
if val is not None:
_set_auto5vor8_setting(field, val.strip())
- logger.info('Auto-5vor8 Einstellungen gespeichert')
- return redirect(url_for('auto5vor8'))
+ logger.info("Auto-5vor8 Einstellungen gespeichert")
+ return redirect(url_for("auto5vor8"))
if __name__ == '__main__':
diff --git a/fuenfvoracht/src/bot.py b/fuenfvoracht/src/bot.py
index 3a3b50aaa..9c2222201 100644
--- a/fuenfvoracht/src/bot.py
+++ b/fuenfvoracht/src/bot.py
@@ -4,6 +4,10 @@ FΓΌnfVorAcht Bot β Review, Scheduling, Briefing, Fehler-Alarm
"""
import asyncio
import os
+import html
+import re
+import urllib.error
+import urllib.request
from datetime import datetime, timedelta
import pytz
@@ -16,6 +20,50 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
import database as db
import logger as flog
+
+# Pre-Send-Check: jede arakavanews-URL muss vor dem Posten 200 liefern.
+# Faengt drei Klassen von Bugs ab:
+# 1. KI hat Slug halluziniert (auto_5vor8 sollte das schon repariert haben β Defense in Depth)
+# 2. WP-Post wurde nachtraeglich geloescht/depubliziert
+# 3. Cloudflare-Tunnel oder WP down zum Sendezeitpunkt
+_URL_RE = re.compile(r"https?://arakavanews\.com/\S+", re.IGNORECASE)
+
+
+def _check_arakava_links(text: str) -> tuple[bool, str]:
+ """Prueft alle arakavanews-URLs im Text per HEAD via stdlib. (ok, fehlertext).
+
+ "ok" ist True wenn entweder keine URL drin ist oder ALLE URLs HTTP 200 liefern.
+ Bei mehreren URLs reicht ein einziger Fehlschlag fuer ok=False.
+ Nutzt urllib (stdlib), damit der Bot-Container keine zusaetzliche Dependency
+ (requests) braucht.
+ """
+ if not text:
+ return True, ""
+ urls = []
+ for raw in _URL_RE.findall(text):
+ clean = raw.rstrip(').,;:!?"\'<>')
+ if clean and clean not in urls:
+ urls.append(clean)
+ if not urls:
+ return True, ""
+ for url in urls:
+ try:
+ req = urllib.request.Request(
+ url,
+ method="HEAD",
+ headers={"User-Agent": "fuenfvoracht-presend-check/1.0"},
+ )
+ with urllib.request.urlopen(req, timeout=5) as resp:
+ if resp.status != 200:
+ return False, f"{url} -> HTTP {resp.status}"
+ except urllib.error.HTTPError as e:
+ return False, f"{url} -> HTTP {e.code}"
+ except urllib.error.URLError as e:
+ return False, f"{url} -> URLError: {str(e.reason)[:80]}"
+ except Exception as e:
+ return False, f"{url} -> {type(e).__name__}: {str(e)[:80]}"
+ return True, ""
+
BOT_TOKEN = os.environ['TELEGRAM_BOT_TOKEN']
CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '')
TZ = pytz.timezone(os.environ.get('TIMEZONE', 'Europe/Berlin'))
@@ -35,8 +83,13 @@ def today_str():
return datetime.now(TZ).strftime('%Y-%m-%d')
+
+def sanitize_telegram_html(text: str) -> str:
+ """Escape <>& so Telegram HTML parse_mode does not fail on stray tags from KI/Quellen."""
+ return html.escape(text or "", quote=False)
+
def with_branding(content: str) -> str:
- text = (content or "").rstrip()
+ text = sanitize_telegram_html((content or "").rstrip())
if BRAND_MARKER in text:
return text
return f"{text}\n\n{BRAND_SIGNATURE}" if text else BRAND_SIGNATURE
@@ -232,10 +285,48 @@ async def job_post_articles(bot: Bot):
return
try:
final_text = with_branding(article['content_final'])
- msg = await bot.send_message(CHANNEL_ID, final_text, parse_mode=ParseMode.HTML)
+
+ ok, link_err = await asyncio.to_thread(_check_arakava_links, final_text)
+ if not ok:
+ flog.posting_failed(d, current_slot, f"link_check_failed: {link_err}")
+ await notify_reviewers(
+ bot,
+ f"β Posting abgebrochen β kaputter Link!\n\n"
+ f"π
{d} Β· β° {current_slot} Uhr\n\n"
+ f"Fehler:\n{link_err}\n\n"
+ f"Artikel bleibt auf approved (kein Auto-Retry).\n"
+ f"π Dashboard: http://100.73.171.62:8080"
+ )
+ continue
+
+ image_url = article.get('image_url', '') or ''
+ if image_url:
+ try:
+ msg = await bot.send_photo(CHANNEL_ID, photo=image_url, caption=final_text, parse_mode=ParseMode.HTML)
+ except Exception:
+ msg = await bot.send_message(CHANNEL_ID, final_text, parse_mode=ParseMode.HTML)
+ else:
+ 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)
+ # memory_service_event
+ try:
+ import requests as _rq
+ _rq.post("http://100.121.192.94:8400/events", json={
+ "source": "fuenfvoracht",
+ "event_type": "article_posted",
+ "object_key": f"{d}_{current_slot}",
+ "payload_json": __import__("json").dumps({
+ "date": d,
+ "slot": current_slot,
+ "channel": str(CHANNEL_ID),
+ "message_id": msg.message_id,
+ "title": article.get("title", ""),
+ }, ensure_ascii=False),
+ }, headers={"Authorization": "Bearer Ai8eeQibV6Z1RWc7oNPim4PXB4vILU1nRW2-XgRcX2M"}, timeout=3)
+ except Exception:
+ pass
await notify_reviewers(
bot,
f"π€ FΓΌnf vor Acht gepostet!\n{d} Β· {current_slot} Uhr"
@@ -253,23 +344,19 @@ async def job_post_articles(bot: Bot):
async def job_check_notify(bot: Bot):
- """PrΓΌft alle 5 Min ob scheduled-Artikel zur Bot-Benachrichtigung fΓ€llig sind."""
+ """PrΓΌft alle 5 Min ob scheduled-Artikel fΓ€llig sind β direkt approved setzen."""
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?"
+ db.update_article_status(d, 'approved', post_time=pt)
+ await notify_reviewers(
+ bot,
+ f"π
Artikel eingeplant\n\n"
+ f"π {d} um {pt} Uhr\n"
+ f"Wird automatisch gepostet."
)
- 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())
+ flog.info('article_auto_approved', date=d, post_time=pt)
async def job_morning_briefing(bot: Bot):
@@ -300,53 +387,91 @@ async def job_morning_briefing(bot: Bot):
lines.append(f"β
Eingeplant: {len(approved)} Slot(s)")
for a in approved:
lines.append(f" β’ {a['post_time']} Uhr β {(a['content_final'] or '')[:50]}β¦")
+ elif missing:
+ lines.append("β οΈ Entwurf wartet auf Freigabe (Auto-5vor8 Faktencheck)")
+ for a in missing:
+ preview = (a.get('content_final') or a.get('content_raw') or '')[:50]
+ lines.append(f" β’ {a['post_time']} Uhr β {preview}β¦")
+ lines.append(" β Bitte im Dashboard prΓΌfen und freigeben.")
else:
- lines.append("β οΈ Noch kein Artikel fΓΌr heute freigegeben!")
+ lines.append(
+ "β οΈ Heute noch kein Artikel im Plan.\n"
+ " Der Dr.-Bine-RSS-Lauf ist um 14 Uhr β wenn dort nichts Neues kam, "
+ "bitte manuell anlegen."
+ )
- if missing:
- lines.append(f"π EntwΓΌrfe (noch nicht freigegeben): {len(missing)}")
-
- if not articles_today:
- lines.append("β Kein Artikel erstellt β bitte jetzt anlegen.")
+ if missing and approved:
+ lines.append(f"\nπ ZusΓ€tzlich EntwΓΌrfe offen: {len(missing)}")
+ for a in missing:
+ preview = (a.get('content_final') or a.get('content_raw') or '')[:50]
+ lines.append(f" β’ {a['post_time']} Uhr β {preview}β¦")
lines.append(f"\nπ NΓ€chste 3 Tage:")
lines.extend(plan_lines)
- lines.append(f"\nπ Dashboard: http://100.73.171.62:8080")
+ lines.append(f"\nπ https://fuenfvoracht.orbitalo.net")
await notify_reviewers(bot, '\n'.join(lines))
flog.info('morning_briefing_sent', date=d)
async def job_cleanup_db():
- """WΓΆchentliche DB-Bereinigung: alte EintrΓ€ge lΓΆschen."""
+ """Woechentliche DB-Bereinigung: alte Eintraege loeschen + VACUUM."""
import sqlite3, os
- db_path = os.environ.get('DB_PATH', '/data/fuenfvoracht.db')
+ db_path = os.environ.get('DB_PATH', '/data/fuenfvoacht.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)
+ vac = sqlite3.connect(db_path, isolation_level=None)
+ vac.execute("VACUUM")
+ vac.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."""
+ """Reminder: Entwurf zur PrΓΌfung, oder Hinweis wenn gar nichts da ist."""
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
+ articles = db.get_articles_by_date(d)
+ active_statuses = {'approved', 'scheduled', 'sent_to_bot', 'posted'}
+ active_articles = [a for a in articles if a.get('status') in active_statuses]
+ draft_articles = [a for a in articles if a.get('status') in ('draft', 'pending_review')]
+
+ if active_articles:
+ flog.info(
+ 'afternoon_reminder_skipped_article_exists',
+ date=d,
+ slots=[a.get('post_time') for a in active_articles],
)
+ return
+
+ if draft_articles:
+ lines = [
+ f"π Entwurf wartet auf Freigabe\n",
+ f"π
Heute ({d}) β Auto-5vor8 hat einen Entwurf angelegt, der noch nicht freigegeben ist:\n",
+ ]
+ for a in draft_articles:
+ preview = (a.get('content_final') or a.get('content_raw') or '')[:55]
+ lines.append(f" β’ {a['post_time']} Uhr β {preview}β¦")
+ lines.append(
+ "\nβ οΈ Der Faktencheck war nicht zufriedenstellend β bitte im Dashboard prΓΌfen, "
+ "anpassen und freigeben, sonst geht heute nichts raus."
+ )
+ lines.append("\nπ https://fuenfvoracht.orbitalo.net")
+ await notify_reviewers(bot, '\n'.join(lines))
+ flog.info('afternoon_reminder_draft_pending', date=d, count=len(draft_articles))
+ return
+
+ channel = db.get_channel()
+ post_t = channel.get('post_time', POST_TIME) if channel else POST_TIME
+ await notify_reviewers(
+ bot,
+ f"β οΈ Noch kein Artikel fΓΌr heute!\n\n"
+ f"Weder freigegeben noch als Entwurf vorhanden.\n"
+ f"Geplanter Slot: {post_t} Uhr.\n"
+ f"π https://fuenfvoracht.orbitalo.net"
+ )
# ββ Main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -383,7 +508,7 @@ def main():
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_m_h, rem_m_m = 15, 0
rem_a_h, rem_a_m = 17, 45
scheduler = AsyncIOScheduler(timezone=TZ)
@@ -393,7 +518,7 @@ def main():
# 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
+ # Tages-Briefing 15:00 β nach Dr.-Bine-RSS-Lauf (14:00) + Auto-5vor8
scheduler.add_job(job_morning_briefing, 'cron',
hour=rem_m_h, minute=rem_m_m,
kwargs={'bot': bot})
@@ -401,7 +526,6 @@ 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()