Redax-WP (Redakteur): - WordPressMirrorClient: Multi-Publish an mehrere WP-Instanzen - Target-Toggles im Dashboard (Checkbox, server-side rendering) - WP-Admin Direktzugang via socat-Proxy (bypass Cloudflare WAF) - Drag & Drop im Redaktionsplan - Artikel-Karten mit Titel + SEO-Snippet sichtbar - Entwürfe ohne Datum in separater Sektion - DB-Cleanup-Job (Sonntag 03:00 Uhr) - openrouter.py: sync generate() Wrapper - mirror_posts Tabelle in DB ESP32-Serie (Arakava News): - Teil 1 veröffentlicht (Post 1209) - Teil 2 als WP-Entwurf erstellt (Post 1340) - Animiertes Hydraulikschema (SVG, 4 Betriebsmodi) in Teil 2 eingebaut - Hardware liegt in DE, Einbau ab April nach Kambodscha-Rückkehr Doku: - STATE.md Redax-WP vollständig aktualisiert - STATE.md Arakava-News: Serie-Status + Hardware-Timeline Made-with: Cursor
612 lines
22 KiB
Python
612 lines
22 KiB
Python
import os
|
|
import json
|
|
from datetime import date, timedelta, datetime
|
|
|
|
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
|
|
from apscheduler.schedulers.background import BackgroundScheduler
|
|
import pytz
|
|
|
|
import database as db
|
|
import logger as flog
|
|
import openrouter
|
|
import rss_fetcher
|
|
from wordpress import WordPressClient, WordPressMirrorClient
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
|
|
|
|
DASHBOARD_USER = os.environ.get('DASHBOARD_USER', 'admin')
|
|
DASHBOARD_PASSWORD = os.environ.get('DASHBOARD_PASSWORD', 'changeme')
|
|
TIMEZONE = os.environ.get('TIMEZONE', 'Europe/Berlin')
|
|
TZ = pytz.timezone(TIMEZONE)
|
|
|
|
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
|
|
TELEGRAM_CHANNEL_ID = os.environ.get('TELEGRAM_CHANNEL_ID', '')
|
|
TELEGRAM_REVIEWER_IDS = [
|
|
i.strip() for i in os.environ.get('TELEGRAM_REVIEWER_IDS', '').split(',') if i.strip()
|
|
]
|
|
|
|
|
|
# ── Scheduler ─────────────────────────────────────────────────────────────────
|
|
|
|
def job_publish_due():
|
|
articles = db.get_due_articles()
|
|
for art in articles:
|
|
try:
|
|
mirror_client = WordPressMirrorClient()
|
|
|
|
# Bild zuerst auf Primary hochladen
|
|
primary_client = WordPressClient()
|
|
media_id = None
|
|
if art.get('featured_image_url'):
|
|
media_id = primary_client.upload_media(art['featured_image_url'])
|
|
|
|
# Manuell deaktivierte Targets aus DB laden
|
|
for t in mirror_client.targets:
|
|
if db.get_setting(f'target_disabled_{t["name"]}', '0') == '1':
|
|
t['enabled'] = False
|
|
|
|
# Auf Primary + alle aktiven Mirrors veröffentlichen
|
|
results = mirror_client.publish_to_all(
|
|
title=art['title'] or 'Ohne Titel',
|
|
content=art['content'] or '',
|
|
status='publish',
|
|
category_ids=[art['category_id']] if art.get('category_id') else [],
|
|
featured_media_id=media_id,
|
|
seo_title=art.get('seo_title'),
|
|
seo_description=art.get('seo_description'),
|
|
focus_keyword=art.get('focus_keyword'),
|
|
)
|
|
|
|
primary_result = results.get('primary')
|
|
if primary_result:
|
|
db.update_article(art['id'], {
|
|
'status': 'published',
|
|
'wp_post_id': primary_result['id'],
|
|
'wp_url': primary_result['url'],
|
|
'published_at': datetime.utcnow().isoformat(),
|
|
})
|
|
db.save_post_history(art['id'], primary_result['id'], primary_result['url'])
|
|
flog.info('article_published', article_id=art['id'], wp_url=primary_result['url'])
|
|
|
|
if art.get('send_to_telegram') and art.get('article_type') == 'ki':
|
|
_send_telegram_teaser(art, primary_result['url'])
|
|
|
|
# Mirror-Ergebnisse speichern
|
|
for m in results.get('mirrors', []):
|
|
db.save_mirror_post(
|
|
article_id=art['id'],
|
|
mirror_name=m['name'],
|
|
mirror_label=m['label'],
|
|
mirror_wp_id=m.get('id'),
|
|
mirror_url=m.get('url'),
|
|
status='ok' if not m.get('error') else 'error',
|
|
error=m.get('error'),
|
|
)
|
|
|
|
except Exception as e:
|
|
flog.error('publish_failed', article_id=art['id'], error=str(e))
|
|
_send_error_alarm(f"❌ Publish fehlgeschlagen: Artikel #{art['id']}\n{str(e)}")
|
|
|
|
|
|
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')
|
|
today_arts = db.get_articles_for_date(today)
|
|
tomorrow_arts = db.get_articles_for_date(tomorrow)
|
|
queue = db.get_feed_queue(status='new', limit=10)
|
|
|
|
lines = ["📰 *Morgen-Briefing Redax-WP*\n"]
|
|
lines.append(f"*Heute ({today}):* {len(today_arts)} Artikel geplant")
|
|
for a in today_arts:
|
|
status_icon = {'published': '✅', 'scheduled': '🗓️', 'draft': '📝'}.get(a['status'], '❓')
|
|
lines.append(f" {status_icon} {a['post_time']} — {(a['title'] or 'Kein Titel')[:50]}")
|
|
|
|
lines.append(f"\n*Morgen ({tomorrow}):* {len(tomorrow_arts)} Artikel")
|
|
lines.append(f"*RSS-Queue:* {len(queue)} neue Artikel warten")
|
|
|
|
msg = '\n'.join(lines)
|
|
for chat_id in TELEGRAM_REVIEWER_IDS:
|
|
_tg_send(chat_id, msg, parse_mode='Markdown')
|
|
|
|
|
|
def _send_telegram_teaser(article: dict, wp_url: str):
|
|
title = article.get('title', '')
|
|
seo_desc = article.get('seo_description', '')
|
|
teaser = seo_desc[:200] if seo_desc else ''
|
|
msg = f"📰 *{title}*\n\n{teaser}\n\n🔗 {wp_url}"
|
|
_tg_send(TELEGRAM_CHANNEL_ID, msg, parse_mode='Markdown')
|
|
|
|
|
|
def _send_error_alarm(message: str):
|
|
for chat_id in TELEGRAM_REVIEWER_IDS:
|
|
_tg_send(chat_id, f"🚨 *Redax-WP Fehler*\n{message}", parse_mode='Markdown')
|
|
|
|
|
|
def _tg_send(chat_id: str, text: str, parse_mode: str = None):
|
|
if not TELEGRAM_BOT_TOKEN or not chat_id:
|
|
return
|
|
import requests as req
|
|
try:
|
|
req.post(
|
|
f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage",
|
|
json={'chat_id': chat_id, 'text': text, 'parse_mode': parse_mode},
|
|
timeout=10
|
|
)
|
|
except Exception as e:
|
|
flog.error('tg_send_failed', error=str(e))
|
|
|
|
|
|
# ── Auth ──────────────────────────────────────────────────────────────────────
|
|
|
|
def logged_in():
|
|
return session.get('user') == DASHBOARD_USER
|
|
|
|
|
|
@app.before_request
|
|
def require_login():
|
|
open_routes = ['login', 'static']
|
|
if request.endpoint not in open_routes and not logged_in():
|
|
return redirect(url_for('login'))
|
|
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
error = None
|
|
if request.method == 'POST':
|
|
if request.form.get('user') == DASHBOARD_USER and \
|
|
request.form.get('password') == DASHBOARD_PASSWORD:
|
|
session['user'] = DASHBOARD_USER
|
|
return redirect(url_for('index'))
|
|
error = 'Falsche Zugangsdaten'
|
|
return render_template('login.html', error=error)
|
|
|
|
|
|
@app.route('/logout')
|
|
def logout():
|
|
session.clear()
|
|
return redirect(url_for('login'))
|
|
|
|
|
|
# ── Main Dashboard ─────────────────────────────────────────────────────────────
|
|
|
|
@app.route('/')
|
|
def index():
|
|
today = date.today().strftime('%Y-%m-%d')
|
|
plan_days = [(date.today() + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(7)]
|
|
|
|
plan_raw = db.get_week_articles(plan_days[0], plan_days[-1])
|
|
plan_articles = {}
|
|
for a in plan_raw:
|
|
plan_articles.setdefault(a['post_date'], []).append(a)
|
|
|
|
wp_categories = []
|
|
try:
|
|
wp = WordPressClient()
|
|
if wp.is_reachable():
|
|
wp_categories = wp.get_categories()
|
|
except Exception:
|
|
pass
|
|
|
|
last_published = db.get_last_published()
|
|
feeds = db.get_feeds()
|
|
queue_count = len(db.get_feed_queue(status='new'))
|
|
prompts = db.get_prompts()
|
|
|
|
# Targets für serverseitiges Rendering
|
|
wp_targets = []
|
|
try:
|
|
mirror_client = WordPressMirrorClient()
|
|
for t in mirror_client.targets:
|
|
disabled = db.get_setting(f'target_disabled_{t["name"]}', '0') == '1'
|
|
if t['primary']:
|
|
admin_pw = os.environ.get('WP_ADMIN_PASSWORD', '')
|
|
admin_direct = os.environ.get('WP_ADMIN_DIRECT_URL', t['url'].rstrip('/'))
|
|
else:
|
|
idx = t['name'].replace('mirror_', '')
|
|
suffix = '' if idx == '1' else idx
|
|
admin_pw = os.environ.get(f'WP_MIRROR{suffix}_ADMIN_PASSWORD', '')
|
|
admin_direct = os.environ.get(f'WP_MIRROR{suffix}_ADMIN_DIRECT_URL', t['url'].rstrip('/'))
|
|
|
|
wp_targets.append({
|
|
'name': t['name'],
|
|
'label': t['label'],
|
|
'url': t['url'],
|
|
'admin_url': admin_direct + '/wp-admin',
|
|
'login_url': admin_direct + '/wp-login.php',
|
|
'username': t['username'],
|
|
'admin_pw': admin_pw,
|
|
'admin_direct': admin_direct,
|
|
'primary': t['primary'],
|
|
'enabled': not disabled,
|
|
})
|
|
except Exception:
|
|
pass
|
|
|
|
undated_drafts = db.get_articles(limit=20, status='draft')
|
|
undated_drafts = [a for a in undated_drafts if not a.get('post_date')]
|
|
|
|
return render_template('index.html',
|
|
today=today,
|
|
plan_days=plan_days,
|
|
plan_articles=plan_articles,
|
|
wp_categories=wp_categories,
|
|
last_published=last_published,
|
|
feeds=feeds,
|
|
queue_count=queue_count,
|
|
prompts=prompts,
|
|
wp_url=os.getenv('WP_URL', '').rstrip('/'),
|
|
wp_admin_direct=os.getenv('WP_ADMIN_DIRECT_URL', os.getenv('WP_URL', '')).rstrip('/'),
|
|
wp_targets=wp_targets,
|
|
undated_drafts=undated_drafts)
|
|
|
|
|
|
# ── API: Artikel ──────────────────────────────────────────────────────────────
|
|
|
|
@app.route('/api/generate', methods=['POST'])
|
|
def api_generate():
|
|
data = request.json
|
|
source = data.get('source', '')
|
|
tone = data.get('tone', 'informativ')
|
|
|
|
prompt = db.get_default_prompt()
|
|
if not prompt:
|
|
return jsonify({'error': 'Kein Prompt konfiguriert'}), 400
|
|
|
|
system = prompt['system_prompt']
|
|
system = system.replace('{tone}', tone).replace('{date}', date.today().strftime('%d.%m.%Y'))
|
|
|
|
try:
|
|
raw = openrouter.generate(system, source)
|
|
content, seo_title, seo_desc, keyword = rss_fetcher._parse_ki_output(raw)
|
|
|
|
# Erste Zeile als Titel extrahieren
|
|
lines = content.strip().split('\n')
|
|
title = lines[0].lstrip('#').strip() if lines else ''
|
|
content_body = '\n'.join(lines[1:]).strip() if len(lines) > 1 else content
|
|
|
|
flog.info('article_generated', tone=tone)
|
|
return jsonify({
|
|
'title': title,
|
|
'content': content_body,
|
|
'seo_title': seo_title,
|
|
'seo_description': seo_desc,
|
|
'focus_keyword': keyword,
|
|
})
|
|
except Exception as e:
|
|
flog.error('generate_failed', error=str(e))
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@app.route('/api/article/save', methods=['POST'])
|
|
def api_save_article():
|
|
data = request.json
|
|
article_id = data.get('id')
|
|
wp_post_id = data.get('wp_post_id')
|
|
wp_preview_url = None
|
|
|
|
if article_id:
|
|
db.update_article(article_id, data)
|
|
else:
|
|
article_id = db.create_article({**data, 'article_type': 'ki', 'status': 'draft'})
|
|
|
|
# Als WP-Draft pushen (neu oder aktualisieren)
|
|
try:
|
|
wp = WordPressClient()
|
|
art = db.get_article(article_id)
|
|
if wp_post_id:
|
|
# Bereits in WP vorhanden — aktualisieren
|
|
result = wp.update_post(
|
|
wp_post_id,
|
|
title=art.get('title') or 'Ohne Titel',
|
|
content=art.get('content') or '',
|
|
status='draft',
|
|
)
|
|
else:
|
|
# Neu als Draft anlegen
|
|
result = wp.create_post(
|
|
title=art.get('title') or 'Ohne Titel',
|
|
content=art.get('content') or '',
|
|
status='draft',
|
|
category_ids=[art['category_id']] if art.get('category_id') else [],
|
|
)
|
|
wp_post_id = result['id']
|
|
db.update_article(article_id, {'wp_post_id': wp_post_id})
|
|
|
|
wp_base = os.getenv('WP_URL', '').rstrip('/')
|
|
wp_preview_url = f"{wp_base}/?p={wp_post_id}&preview=true"
|
|
flog.info('article_saved_as_draft', article_id=article_id, wp_post_id=wp_post_id)
|
|
except Exception as e:
|
|
flog.warn('draft_push_failed', article_id=article_id, error=str(e))
|
|
|
|
return jsonify({'success': True, 'id': article_id, 'wp_post_id': wp_post_id, 'wp_preview_url': wp_preview_url})
|
|
|
|
|
|
@app.route('/api/article/schedule', methods=['POST'])
|
|
def api_schedule_article():
|
|
data = request.json
|
|
article_id = data.get('id')
|
|
post_date = data.get('post_date')
|
|
post_time = data.get('post_time')
|
|
send_to_telegram = 1 if data.get('article_type', 'ki') == 'ki' else 0
|
|
|
|
if article_id:
|
|
db.update_article(article_id, {
|
|
'post_date': post_date,
|
|
'post_time': post_time,
|
|
'status': 'scheduled',
|
|
'send_to_telegram': send_to_telegram,
|
|
})
|
|
else:
|
|
article_id = db.create_article({
|
|
**data,
|
|
'article_type': 'ki',
|
|
'status': 'scheduled',
|
|
'send_to_telegram': send_to_telegram,
|
|
})
|
|
|
|
flog.info('article_scheduled', article_id=article_id, date=post_date, time=post_time)
|
|
return jsonify({'success': True, 'id': article_id})
|
|
|
|
|
|
@app.route('/api/article/<int:article_id>/reschedule', methods=['POST'])
|
|
def api_reschedule(article_id):
|
|
data = request.json
|
|
ok, msg = db.reschedule_article(article_id, data['post_date'], data['post_time'])
|
|
return jsonify({'success': ok, 'error': msg if not ok else None})
|
|
|
|
|
|
@app.route('/api/article/<int:article_id>/delete', methods=['POST'])
|
|
def api_delete_article(article_id):
|
|
db.delete_article(article_id)
|
|
flog.info('article_deleted', article_id=article_id)
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@app.route('/api/article/<int:article_id>', methods=['GET'])
|
|
def api_get_article(article_id):
|
|
art = db.get_article(article_id)
|
|
if not art:
|
|
return jsonify({'error': 'Not found'}), 404
|
|
return jsonify(art)
|
|
|
|
|
|
@app.route('/api/slots/<date_str>')
|
|
def api_slots(date_str):
|
|
return jsonify({'taken': db.get_taken_slots(date_str)})
|
|
|
|
|
|
@app.route('/api/og-image', methods=['POST'])
|
|
def api_og_image():
|
|
url = request.json.get('url', '')
|
|
image = rss_fetcher._extract_og_image(url)
|
|
return jsonify({'image': image})
|
|
|
|
|
|
@app.route('/api/wp/categories')
|
|
def api_wp_categories():
|
|
try:
|
|
wp = WordPressClient()
|
|
return jsonify(wp.get_categories())
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
@app.route('/api/wp/targets')
|
|
def api_wp_targets():
|
|
"""Gibt alle konfigurierten WordPress-Targets zurück (inkl. manuell deaktivierter)."""
|
|
mirror_client = WordPressMirrorClient()
|
|
targets_info = []
|
|
for t in mirror_client.targets:
|
|
# Manuelle Deaktivierung aus DB-Settings prüfen
|
|
disabled = db.get_setting(f'target_disabled_{t["name"]}', '0') == '1'
|
|
enabled = not disabled
|
|
client = mirror_client._client_for(t)
|
|
reachable = client.is_reachable() if enabled else False
|
|
targets_info.append({
|
|
'name': t['name'],
|
|
'label': t['label'],
|
|
'url': t['url'],
|
|
'primary': t['primary'],
|
|
'enabled': enabled,
|
|
'reachable': reachable,
|
|
})
|
|
return jsonify(targets_info)
|
|
|
|
|
|
@app.route('/targets/toggle', methods=['POST'])
|
|
def toggle_target():
|
|
name = request.form.get('name')
|
|
if not name:
|
|
return redirect(url_for('index'))
|
|
mirror_client = WordPressMirrorClient()
|
|
target = next((t for t in mirror_client.targets if t['name'] == name), None)
|
|
if target and not target['primary']:
|
|
current = db.get_setting(f'target_disabled_{name}', '0')
|
|
db.set_setting(f'target_disabled_{name}', '0' if current == '1' else '1')
|
|
return redirect(url_for('index'))
|
|
|
|
|
|
@app.route('/api/article/<int:article_id>/mirrors')
|
|
def api_article_mirrors(article_id):
|
|
mirrors = db.get_mirror_posts(article_id)
|
|
return jsonify(mirrors)
|
|
|
|
|
|
# ── API: Feeds ────────────────────────────────────────────────────────────────
|
|
|
|
@app.route('/feeds')
|
|
def feeds_page():
|
|
feeds = db.get_feeds()
|
|
queue = db.get_feed_queue(status='new', limit=30)
|
|
return render_template('feeds.html', feeds=feeds, queue=queue)
|
|
|
|
|
|
@app.route('/api/feeds', methods=['GET'])
|
|
def api_get_feeds():
|
|
return jsonify(db.get_feeds())
|
|
|
|
|
|
@app.route('/api/feeds/add', methods=['POST'])
|
|
def api_add_feed():
|
|
data = request.json
|
|
fid = db.create_feed(data)
|
|
flog.info('feed_added', feed_id=fid, name=data.get('name'))
|
|
return jsonify({'success': True, 'id': fid})
|
|
|
|
|
|
@app.route('/api/feeds/<int:feed_id>/update', methods=['POST'])
|
|
def api_update_feed(feed_id):
|
|
db.update_feed(feed_id, request.json)
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@app.route('/api/feeds/<int:feed_id>/delete', methods=['POST'])
|
|
def api_delete_feed(feed_id):
|
|
db.delete_feed(feed_id)
|
|
flog.info('feed_deleted', feed_id=feed_id)
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@app.route('/api/feeds/<int:feed_id>/fetch', methods=['POST'])
|
|
def api_fetch_feed(feed_id):
|
|
feed = db.get_feed(feed_id)
|
|
if not feed:
|
|
return jsonify({'error': 'Feed not found'}), 404
|
|
count = rss_fetcher.fetch_feed(feed)
|
|
return jsonify({'success': True, 'new_items': count})
|
|
|
|
|
|
@app.route('/api/queue/<int:item_id>/approve', methods=['POST'])
|
|
def api_approve_queue_item(item_id):
|
|
items = db.get_feed_queue(status='new')
|
|
item = next((i for i in items if i['id'] == item_id), None)
|
|
if not item:
|
|
return jsonify({'error': 'Item not found'}), 404
|
|
feed = db.get_feed(item['feed_id'])
|
|
article_id = rss_fetcher.process_auto_publish(feed, item)
|
|
return jsonify({'success': True, 'article_id': article_id})
|
|
|
|
|
|
@app.route('/api/queue/<int:item_id>/reject', methods=['POST'])
|
|
def api_reject_queue_item(item_id):
|
|
db.update_feed_item_status(item_id, 'rejected')
|
|
return jsonify({'success': True})
|
|
|
|
|
|
# ── Prompts ───────────────────────────────────────────────────────────────────
|
|
|
|
@app.route('/prompts')
|
|
def prompts_page():
|
|
return render_template('prompts.html', prompts=db.get_prompts())
|
|
|
|
|
|
@app.route('/api/prompts/save', methods=['POST'])
|
|
def api_save_prompt():
|
|
data = request.json
|
|
conn = db.get_conn()
|
|
if data.get('id'):
|
|
conn.execute(
|
|
"UPDATE prompts SET name=?, system_prompt=? WHERE id=?",
|
|
(data['name'], data['system_prompt'], data['id'])
|
|
)
|
|
else:
|
|
conn.execute(
|
|
"INSERT INTO prompts (name, system_prompt) VALUES (?,?)",
|
|
(data['name'], data['system_prompt'])
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@app.route('/api/prompts/<int:pid>/default', methods=['POST'])
|
|
def api_set_default_prompt(pid):
|
|
conn = db.get_conn()
|
|
conn.execute("UPDATE prompts SET is_default=0")
|
|
conn.execute("UPDATE prompts SET is_default=1 WHERE id=?", (pid,))
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': True})
|
|
|
|
|
|
@app.route('/api/prompts/<int:pid>/delete', methods=['POST'])
|
|
def api_delete_prompt(pid):
|
|
conn = db.get_conn()
|
|
conn.execute("DELETE FROM prompts WHERE id=? AND is_default=0", (pid,))
|
|
conn.commit()
|
|
conn.close()
|
|
return jsonify({'success': True})
|
|
|
|
|
|
# ── History ───────────────────────────────────────────────────────────────────
|
|
|
|
@app.route('/history')
|
|
def history():
|
|
articles = db.get_articles(limit=50, status='published')
|
|
return render_template('history.html', articles=articles)
|
|
|
|
|
|
# ── Settings ──────────────────────────────────────────────────────────────────
|
|
|
|
@app.route('/settings')
|
|
def settings():
|
|
return render_template('settings.html',
|
|
wp_url=os.environ.get('WP_URL', ''),
|
|
wp_username=os.environ.get('WP_USERNAME', ''),
|
|
timezone=TIMEZONE)
|
|
|
|
|
|
# ── Hilfe ─────────────────────────────────────────────────────────────────────
|
|
|
|
@app.route('/hilfe')
|
|
def hilfe():
|
|
return render_template('hilfe.html')
|
|
|
|
|
|
# ── Startup ───────────────────────────────────────────────────────────────────
|
|
|
|
def start_scheduler():
|
|
scheduler = BackgroundScheduler(timezone=TZ)
|
|
# Artikel-Publishing: jede Minute prüfen
|
|
scheduler.add_job(job_publish_due, 'interval', minutes=1, id='publish_due')
|
|
# RSS-Feeds: alle 30 Minuten
|
|
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')
|
|
|
|
|
|
db.init_db()
|
|
start_scheduler()
|
|
flog.info('app_started')
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=8080, debug=False)
|