- Draft-Status, review_reason, notify-failed Endpoint - Telegram bei Entwurf, erfolgreichem Auto-Approve und Generierungsfehler - Nachmittags-Briefing 15:00 statt 10:00, bessere Entwurf-vs-leer Logik
771 lines
28 KiB
Python
771 lines
28 KiB
Python
from flask import Flask, render_template, request, jsonify, redirect, url_for, Response
|
||
from functools import wraps
|
||
from datetime import datetime, date, timedelta
|
||
import os
|
||
import html
|
||
import asyncio
|
||
import logging
|
||
import requests as req_lib
|
||
|
||
import database as db
|
||
import openrouter
|
||
import logger as flog
|
||
|
||
app = Flask(__name__)
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
TZ_NAME = os.environ.get('TIMEZONE', 'Europe/Berlin')
|
||
BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
||
POST_TIME = os.environ.get('POST_TIME', '19:55')
|
||
|
||
AUTH_USER = os.environ.get('AUTH_USER', 'Holgerhh')
|
||
AUTH_PASS = os.environ.get('AUTH_PASS', 'ddlhh')
|
||
|
||
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❗️"
|
||
)
|
||
|
||
|
||
def check_auth(username, password):
|
||
return username == AUTH_USER and password == AUTH_PASS
|
||
|
||
|
||
def authenticate():
|
||
return Response(
|
||
'Zugang verweigert.', 401,
|
||
{'WWW-Authenticate': 'Basic realm="FünfVorAcht"'})
|
||
|
||
|
||
@app.before_request
|
||
def before_request_auth():
|
||
auth = request.authorization
|
||
if not auth or not check_auth(auth.username, auth.password):
|
||
return authenticate()
|
||
|
||
|
||
@app.after_request
|
||
def add_no_cache(response):
|
||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||
response.headers['Pragma'] = 'no-cache'
|
||
return response
|
||
|
||
|
||
def today_str():
|
||
import pytz
|
||
return datetime.now(pytz.timezone(TZ_NAME)).strftime('%Y-%m-%d')
|
||
|
||
|
||
def today_display():
|
||
import pytz
|
||
return datetime.now(pytz.timezone(TZ_NAME)).strftime('%d. %B %Y')
|
||
|
||
|
||
def week_range():
|
||
today = date.today()
|
||
start = today - timedelta(days=today.weekday())
|
||
return [(start + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(7)]
|
||
|
||
|
||
def planning_days(count=7):
|
||
import pytz
|
||
tz = pytz.timezone(TZ_NAME)
|
||
t = datetime.now(tz).date()
|
||
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 = 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
|
||
|
||
|
||
def send_telegram_message(chat_id, text, reply_markup=None):
|
||
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||
payload = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"}
|
||
if reply_markup:
|
||
import json
|
||
payload["reply_markup"] = json.dumps(reply_markup)
|
||
try:
|
||
r = req_lib.post(url, json=payload, timeout=10)
|
||
return r.json()
|
||
except Exception as e:
|
||
logger.error("Telegram send fehlgeschlagen: %s", e)
|
||
return None
|
||
|
||
|
||
def notify_all_reviewers(text, reply_markup=None):
|
||
results = []
|
||
for chat_id in db.get_reviewer_chat_ids():
|
||
result = send_telegram_message(chat_id, text, reply_markup)
|
||
results.append({'chat_id': chat_id, 'ok': bool(result and result.get('ok'))})
|
||
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"⚠️ <b>Auto-5vor8: Bitte manuell prüfen</b>\n\n"
|
||
f"📅 Slot: <b>{date_str} {post_time}</b>\n"
|
||
f"📰 {title_line}\n"
|
||
)
|
||
if reason_line:
|
||
msg += f"\n❗ <b>Faktencheck:</b> {reason_line}\n"
|
||
msg += (
|
||
f"\nDer KI-Text hat die automatische Qualitätsprüfung nicht bestanden. "
|
||
f"Ein <b>Entwurf</b> liegt im Dashboard — bitte lesen, ggf. anpassen und freigeben.\n\n"
|
||
f"👉 https://fuenfvoracht.orbitalo.net"
|
||
)
|
||
else:
|
||
msg = (
|
||
f"🚨 <b>Auto-5vor8: Generierung fehlgeschlagen</b>\n\n"
|
||
f"📰 {title_line}\n"
|
||
)
|
||
if reason_line:
|
||
msg += f"\n❗ {reason_line}\n"
|
||
msg += (
|
||
f"\nEs wurde <b>kein</b> 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"✅ <b>Auto-5vor8: Artikel eingeplant</b>\n\n"
|
||
f"📅 <b>{date_str} · {post_time} Uhr</b>\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('/')
|
||
def index():
|
||
today = today_str()
|
||
articles_today = db.get_articles_by_date(today)
|
||
article_today = articles_today[0] if articles_today else None
|
||
week_days = week_range()
|
||
week_articles_raw = db.get_week_articles(week_days[0], week_days[-1])
|
||
# Mehrere Artikel pro Tag: dict date → list
|
||
week_articles = {}
|
||
for a in week_articles_raw:
|
||
week_articles.setdefault(a['date'], []).append(a)
|
||
|
||
recent = db.get_recent_articles(10)
|
||
stats = db.get_monthly_stats()
|
||
channel = db.get_channel()
|
||
prompts = db.get_prompts()
|
||
tags = db.get_tags()
|
||
favorites = db.get_favorites()
|
||
locations = db.get_locations()
|
||
current_location = db.get_current_location()
|
||
reviewers = db.get_reviewers()
|
||
last_posted = db.get_last_posted()
|
||
|
||
plan_days = planning_days(7)
|
||
plan_raw = db.get_week_articles(plan_days[0], plan_days[-1])
|
||
plan_articles = {}
|
||
for a in plan_raw:
|
||
plan_articles.setdefault(a['date'], []).append(a)
|
||
|
||
month_start = date.today().replace(day=1).strftime('%Y-%m-%d')
|
||
month_end = (date.today().replace(day=28) + timedelta(days=4)).replace(day=1) - timedelta(days=1)
|
||
month_articles = {}
|
||
for a in db.get_week_articles(month_start, month_end.strftime('%Y-%m-%d')):
|
||
month_articles.setdefault(a['date'], []).append(a['status'])
|
||
|
||
return render_template('index.html',
|
||
today=today,
|
||
article_today=article_today,
|
||
articles_today=articles_today,
|
||
week_days=week_days,
|
||
week_articles=week_articles,
|
||
plan_days=plan_days,
|
||
plan_articles=plan_articles,
|
||
month_articles=month_articles,
|
||
recent=recent,
|
||
stats=stats,
|
||
channel=channel,
|
||
post_time=POST_TIME,
|
||
prompts=prompts,
|
||
tags=tags,
|
||
favorites=favorites,
|
||
locations=locations,
|
||
current_location=current_location,
|
||
reviewers=reviewers,
|
||
last_posted=last_posted)
|
||
|
||
|
||
# ── History ───────────────────────────────────────────────────────────────────
|
||
|
||
@app.route('/history')
|
||
def history():
|
||
articles = db.get_recent_articles(30)
|
||
return render_template('history.html', articles=articles)
|
||
|
||
|
||
# ── Prompts ───────────────────────────────────────────────────────────────────
|
||
|
||
@app.route('/prompts')
|
||
def prompts():
|
||
all_prompts = db.get_prompts()
|
||
return render_template('prompts.html', prompts=all_prompts)
|
||
|
||
|
||
@app.route('/prompts/save', methods=['POST'])
|
||
def save_prompt():
|
||
pid = request.form.get('id')
|
||
name = request.form.get('name', '').strip()
|
||
system_prompt = request.form.get('system_prompt', '').strip()
|
||
if pid:
|
||
db.save_prompt(int(pid), name, system_prompt)
|
||
else:
|
||
db.create_prompt(name, system_prompt)
|
||
return redirect(url_for('prompts'))
|
||
|
||
|
||
@app.route('/prompts/default/<int:pid>', methods=['POST'])
|
||
def set_default_prompt(pid):
|
||
db.set_default_prompt(pid)
|
||
return redirect(url_for('prompts'))
|
||
|
||
|
||
@app.route('/prompts/delete/<int:pid>', methods=['POST'])
|
||
def delete_prompt(pid):
|
||
db.delete_prompt(pid)
|
||
return redirect(url_for('prompts'))
|
||
|
||
|
||
@app.route('/prompts/test', methods=['POST'])
|
||
def test_prompt():
|
||
data = request.get_json()
|
||
system_prompt = data.get('system_prompt', '')
|
||
source = data.get('source', 'https://tagesschau.de')
|
||
tag = data.get('tag', 'Politik')
|
||
prompt_id = data.get('prompt_id')
|
||
import pytz
|
||
date_display = datetime.now(pytz.timezone(TZ_NAME)).strftime('%d. %B %Y')
|
||
try:
|
||
result = asyncio.run(openrouter.generate_article(source, system_prompt, date_display, tag))
|
||
if prompt_id:
|
||
db.save_prompt_test_result(int(prompt_id), result)
|
||
return jsonify({'success': True, 'result': result})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)})
|
||
|
||
|
||
# ── Hilfe ─────────────────────────────────────────────────────────────────────
|
||
|
||
@app.route('/hilfe')
|
||
def hilfe():
|
||
return render_template('hilfe.html')
|
||
|
||
|
||
# ── Settings ──────────────────────────────────────────────────────────────────
|
||
|
||
@app.route('/settings')
|
||
def settings():
|
||
channel = db.get_channel()
|
||
favorites = db.get_favorites()
|
||
tags = db.get_tags()
|
||
reviewers = db.get_reviewers(active_only=False)
|
||
return render_template('settings.html', channel=channel,
|
||
favorites=favorites, tags=tags,
|
||
reviewers=reviewers)
|
||
|
||
|
||
@app.route('/settings/channel', methods=['POST'])
|
||
def save_channel():
|
||
telegram_id = request.form.get('telegram_id', '').strip()
|
||
post_time = request.form.get('post_time', '19:55').strip()
|
||
db.update_channel(telegram_id, post_time)
|
||
return redirect(url_for('settings'))
|
||
|
||
|
||
@app.route('/settings/favorite/add', methods=['POST'])
|
||
def add_favorite():
|
||
label = request.form.get('label', '').strip()
|
||
url = request.form.get('url', '').strip()
|
||
if label and url:
|
||
db.add_favorite(label, url)
|
||
return redirect(url_for('settings'))
|
||
|
||
|
||
# ── Reviewer API ──────────────────────────────────────────────────────────────
|
||
|
||
@app.route('/api/reviewers', methods=['GET'])
|
||
def api_reviewers():
|
||
return jsonify(db.get_reviewers(active_only=False))
|
||
|
||
|
||
@app.route('/api/reviewers/add', methods=['POST'])
|
||
def api_add_reviewer():
|
||
data = request.get_json()
|
||
chat_id = data.get('chat_id')
|
||
name = data.get('name', '').strip()
|
||
if not chat_id or not name:
|
||
return jsonify({'success': False, 'error': 'chat_id und name erforderlich'})
|
||
try:
|
||
chat_id = int(chat_id)
|
||
except ValueError:
|
||
return jsonify({'success': False, 'error': 'Ungültige Chat-ID'})
|
||
added = db.add_reviewer(chat_id, name)
|
||
if not added:
|
||
return jsonify({'success': False, 'error': 'Chat-ID bereits vorhanden'})
|
||
# Willkommensnachricht
|
||
welcome = (
|
||
f"👋 <b>Willkommen bei FünfVorAcht!</b>\n\n"
|
||
f"Du wurdest als Redakteur hinzugefügt.\n"
|
||
f"Ab jetzt erhältst du Reviews, Reminder und Status-Meldungen.\n\n"
|
||
f"/start für eine Übersicht aller Befehle."
|
||
)
|
||
send_telegram_message(chat_id, welcome)
|
||
return jsonify({'success': True})
|
||
|
||
|
||
@app.route('/api/reviewers/remove', methods=['POST'])
|
||
def api_remove_reviewer():
|
||
data = request.get_json()
|
||
chat_id = data.get('chat_id')
|
||
if not chat_id:
|
||
return jsonify({'success': False, 'error': 'chat_id erforderlich'})
|
||
db.remove_reviewer(int(chat_id))
|
||
return jsonify({'success': True})
|
||
|
||
|
||
# ── API Endpoints ─────────────────────────────────────────────────────────────
|
||
|
||
@app.route('/api/generate', methods=['POST'])
|
||
def api_generate():
|
||
data = request.get_json()
|
||
source = data.get('source', '').strip()
|
||
tag = data.get('tag', 'allgemein')
|
||
prompt_id = data.get('prompt_id')
|
||
date_str = data.get('date', today_str())
|
||
post_time = data.get('post_time', POST_TIME)
|
||
if not source:
|
||
return jsonify({'success': False, 'error': 'Keine Quelle angegeben'})
|
||
prompt = None
|
||
if prompt_id:
|
||
all_prompts = db.get_prompts()
|
||
prompt = next((p for p in all_prompts if str(p['id']) == str(prompt_id)), None)
|
||
if not prompt:
|
||
prompt = db.get_default_prompt()
|
||
if not prompt:
|
||
return jsonify({'success': False, 'error': 'Kein Prompt konfiguriert'})
|
||
try:
|
||
content = asyncio.run(
|
||
openrouter.generate_article(source, prompt['system_prompt'], today_display(), tag)
|
||
)
|
||
existing = db.get_article_by_date(date_str, post_time)
|
||
if existing:
|
||
db.update_article_content(date_str, content, new_version=True, post_time=post_time)
|
||
conn = db.get_conn()
|
||
conn.execute(
|
||
"UPDATE articles SET source_input=?, tag=?, status='draft' WHERE date=? AND post_time=?",
|
||
(source, tag, date_str, post_time)
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
else:
|
||
db.create_article(date_str, source, content, prompt['id'], tag, post_time)
|
||
flog.article_generated(date_str, source, 1, tag)
|
||
return jsonify({'success': True, 'content': content})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)})
|
||
|
||
|
||
@app.route('/api/article/<date_str>/save', methods=['POST'])
|
||
def api_save(date_str):
|
||
data = request.get_json()
|
||
content = data.get('content', '').strip()
|
||
post_time = data.get('post_time', POST_TIME)
|
||
if not content:
|
||
return jsonify({'success': False, 'error': 'Kein Inhalt'})
|
||
existing = db.get_article_by_date(date_str, post_time)
|
||
if existing:
|
||
db.update_article_content(date_str, content, post_time=post_time)
|
||
flog.article_saved(date_str, post_time)
|
||
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/<date_str>/schedule', methods=['POST'])
|
||
def api_schedule(date_str):
|
||
"""Artikel einplanen: post_time + notify_at setzen."""
|
||
data = request.get_json()
|
||
post_time = data.get('post_time', POST_TIME)
|
||
notify_mode = data.get('notify_mode', 'auto') # sofort | auto | custom
|
||
notify_at_custom = data.get('notify_at')
|
||
|
||
existing = db.get_article_by_date(date_str, post_time)
|
||
if not existing:
|
||
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()
|
||
|
||
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()
|
||
|
||
notify_all_reviewers(
|
||
f"📅 <b>Artikel eingeplant</b>\n\n"
|
||
f"📆 {date_str} um <b>{post_time} Uhr</b>\n"
|
||
f"Wird automatisch gepostet."
|
||
)
|
||
|
||
flog.article_scheduled(date_str, post_time, ts)
|
||
return jsonify({'success': True})
|
||
|
||
|
||
@app.route('/api/article/<int:article_id>/reschedule', methods=['POST'])
|
||
def api_reschedule(article_id):
|
||
data = request.get_json()
|
||
new_date = data.get('date')
|
||
new_time = data.get('post_time')
|
||
if not new_date or not new_time:
|
||
return jsonify({'success': False, 'error': 'date und post_time erforderlich'})
|
||
# Slot-Konflikt prüfen
|
||
if db.slot_is_taken(new_date, new_time, exclude_id=article_id):
|
||
taken = db.get_taken_slots(new_date)
|
||
flog.slot_conflict(new_date, new_time)
|
||
return jsonify({
|
||
'success': False,
|
||
'error': f'Slot {new_date} {new_time} ist bereits belegt.',
|
||
'taken_slots': taken
|
||
})
|
||
ok = db.reschedule_article(article_id, new_date, new_time)
|
||
if ok:
|
||
return jsonify({'success': True})
|
||
return jsonify({'success': False, 'error': 'Fehler beim Umplanen'})
|
||
|
||
|
||
@app.route('/api/article/<int:article_id>/delete', methods=['POST'])
|
||
def api_delete(article_id):
|
||
db.delete_article(article_id)
|
||
return jsonify({'success': True})
|
||
|
||
|
||
@app.route('/api/slots/<date_str>')
|
||
def api_slots(date_str):
|
||
"""Gibt belegte Slots für einen Tag zurück."""
|
||
taken = db.get_taken_slots(date_str)
|
||
return jsonify({'date': date_str, 'taken': taken})
|
||
|
||
|
||
@app.route('/api/article/<date_str>/send-to-bot', methods=['POST'])
|
||
def api_send_to_bot(date_str):
|
||
data = request.get_json() or {}
|
||
post_time = data.get('post_time', POST_TIME)
|
||
article = db.get_article_by_date(date_str, post_time)
|
||
if not article or not article.get('content_final'):
|
||
return jsonify({'success': False, 'error': 'Kein Artikel vorhanden'})
|
||
channel = db.get_channel()
|
||
pt = channel.get('post_time', post_time)
|
||
branded = with_branding(article['content_final'])
|
||
text = (
|
||
f"📋 <b>Review: {date_str} · {post_time} Uhr</b>\n"
|
||
f"Version {article['version']}\n"
|
||
f"──────────────────────\n\n"
|
||
f"{branded}\n\n"
|
||
f"──────────────────────\n"
|
||
f"Freigeben oder bearbeiten?"
|
||
)
|
||
keyboard = {
|
||
"inline_keyboard": [[
|
||
{"text": "✅ Freigeben", "callback_data": f"approve:{date_str}:{post_time}"},
|
||
{"text": "✏️ Bearbeiten", "callback_data": f"edit:{date_str}:{post_time}"}
|
||
]]
|
||
}
|
||
results = notify_all_reviewers(text, keyboard)
|
||
ok_count = sum(1 for r in results if r['ok'])
|
||
if ok_count > 0:
|
||
db.update_article_status(date_str, 'sent_to_bot', post_time=post_time)
|
||
flog.article_sent_to_bot(date_str, post_time, [r['chat_id'] for r in results if r['ok']])
|
||
return jsonify({'success': True})
|
||
return jsonify({'success': False, 'error': 'Kein Reviewer erreichbar'})
|
||
|
||
|
||
@app.route('/api/settings/post-time', methods=['POST'])
|
||
def api_save_post_time():
|
||
data = request.get_json()
|
||
post_time = data.get('post_time', '19:55')
|
||
channel = db.get_channel()
|
||
db.update_channel(channel.get('telegram_id', ''), post_time)
|
||
return jsonify({'success': True})
|
||
|
||
|
||
@app.route('/api/settings/location', methods=['POST'])
|
||
def api_set_location():
|
||
data = request.get_json()
|
||
location_id = data.get('location_id')
|
||
if not location_id:
|
||
return jsonify({'success': False, 'error': 'Keine Location-ID'})
|
||
db.set_location(location_id)
|
||
loc = db.get_current_location()
|
||
morning, afternoon = db.get_reminder_times_in_berlin(loc)
|
||
return jsonify({
|
||
'success': True,
|
||
'location': loc,
|
||
'reminders_berlin': {
|
||
'morning': f"{morning[0]:02d}:{morning[1]:02d}",
|
||
'afternoon': f"{afternoon[0]:02d}:{afternoon[1]:02d}"
|
||
}
|
||
})
|
||
|
||
|
||
@app.route('/api/balance')
|
||
def api_balance():
|
||
try:
|
||
balance = asyncio.run(openrouter.get_balance())
|
||
return jsonify(balance)
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 500
|
||
|
||
|
||
@app.route('/api/article/<date_str>')
|
||
def api_article(date_str):
|
||
post_time = request.args.get('post_time')
|
||
article = db.get_article_by_date(date_str, post_time)
|
||
if not article:
|
||
return jsonify({'error': 'Nicht gefunden'}), 404
|
||
return jsonify(article)
|
||
|
||
|
||
@app.route('/api/articles/<date_str>')
|
||
def api_articles_for_date(date_str):
|
||
"""Alle Artikel eines Tages (alle Slots)."""
|
||
articles = db.get_articles_by_date(date_str)
|
||
return jsonify(articles)
|
||
|
||
|
||
@app.route('/api/article/<date_str>/approve', methods=['POST'])
|
||
def api_approve(date_str):
|
||
data = request.get_json() or {}
|
||
post_time = data.get('post_time', POST_TIME)
|
||
db.update_article_status(date_str, 'approved', post_time=post_time)
|
||
flog.article_approved(date_str, post_time, 0)
|
||
return jsonify({'success': True})
|
||
|
||
|
||
@app.route('/api/article/<date_str>/skip', methods=['POST'])
|
||
def api_skip(date_str):
|
||
data = request.get_json() or {}
|
||
post_time = data.get('post_time', POST_TIME)
|
||
db.update_article_status(date_str, 'skipped', post_time=post_time)
|
||
flog.article_skipped(date_str, post_time)
|
||
return jsonify({'success': True})
|
||
|
||
|
||
# == 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()
|
||
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):
|
||
conn = db.get_conn()
|
||
conn.execute(
|
||
"INSERT INTO auto_5vor8_settings (key, value) VALUES (?, ?) "
|
||
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
||
(key, str(value))
|
||
)
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
|
||
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()
|
||
style_info = _auto5vor8_style_info()
|
||
return render_template("auto5vor8.html", settings=settings, style_info=style_info)
|
||
|
||
|
||
@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"))
|
||
|
||
|
||
@app.route("/auto5vor8/settings", methods=["POST"])
|
||
def auto5vor8_save_settings():
|
||
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"))
|
||
|
||
|
||
if __name__ == '__main__':
|
||
db.init_db()
|
||
app.run(host='0.0.0.0', port=8080, debug=False)
|