diff --git a/plans/2026-07-14-bsn-chatsystem-v2.1-final.html b/plans/2026-07-14-bsn-chatsystem-v2.1-final.html
new file mode 100644
index 0000000..c85c984
--- /dev/null
+++ b/plans/2026-07-14-bsn-chatsystem-v2.1-final.html
@@ -0,0 +1,871 @@
+
+
+
+BSN-Chatsystem v2.1 — Implementierungsplan (geprüft)
+
+
+
+
+🚀 BSN-Chatsystem v2.1 — Implementierungsplan (extern geprüft)
+
+
+
+| Datum | 14. Juli 2026 |
+| Version | 5.1 — Externes Review eingearbeitet |
+| Autor | Cody (Hermes Agent), mit Korrekturen von externem Prüfer |
+| Repository | git@git.datenhimmel.work:danielkrause/BSN-Chatsystem.git |
+| Status | ✅ Umsetzungsreif |
+
+
+
+
+
+
+1. Ziel & Rahmenbedingungen
+
+
Ziel: Das BSN-Chatsystem (chat.datenhimmel.work) 1:1 funktional auf dedizierten dogado-Server migrieren, Architektur produktionsreif.
+
+| Rahmenbedingung | Wert |
+| Funktionsumfang | 100% identisch zum aktuellen System |
+| LLM | OpenRouter → Mistral 4 (europäisch, DSGVO) |
+| TTS | Deaktiviert (später optional) |
+| Queue | Eigener systemd-Service (nicht Daemon-Thread) |
+| Skalierungsziel | ~1000 Nutzer/h |
+| Admin-Schutz | Cloudflare Access (vor Go-Live) |
+| Backup | Offsite via rsync zum Hermes-Server |
+| Service-User | bsn (nicht root) |
+
+
+
+
+2. Server
+
+
+| Eigenschaft | Wert |
+| IP | 89.22.110.107 (dogado GmbH) |
+| CPU | Xeon E5-2680 v3, 6 Cores @ 2.50 GHz |
+| RAM | 12 GB |
+| Disk | 492 GB SSD |
+| OS Ziel | Ubuntu 24.04 LTS minimal (KEIN Plesk) |
+| Offene Ports | NUR 22 (SSH), 80, 443 |
+
+
+
+
+3. Architektur
+
+
+┌──────────────────────────────────────────────────┐
+│ 🌍 Cloudflare │
+│ Cache Rules: / + /beitrag/* → 5min │
+│ Bypass: /api/*, /webhook, /admin, /telegram │
+│ Access (OTP): /admin/* │
+│ DDoS, WAF, weltweit Edge │
+└────────┬─────────────────────────────────────────┘
+ │ cloudflared Tunnel → localhost:80
+┌────────▼─────────────────────────────────────────┐
+│ nginx Reverse-Proxy :80 │
+│ limit_req_zone im HTTP-Kontext │
+│ /static/* → direkt von Disk │
+│ /media/* → direkt von Disk │
+│ proxy_pass → gunicorn :8000 │
+│ KEIN nginx-Cache (macht Cloudflare) │
+└────────┬─────────────────────────────────────────┘
+ │ HTTP :8000
+┌────────▼──────────┐ ┌─────────────────────────┐
+│ bsn-chatbot │ │ bsn-worker │
+│ gunicorn 4w×2t │ │ eigener systemd- │
+│ wsgi:app │ │ Service │
+│ User: bsn │ │ pollt submission_queue │
+│ Flask-Routen │ │ Whisper 1× geladen │
+└────────┬──────────┘ │ kein Recycling mid- │
+ │ │ Verarbeitung │
+ │ └───────────┬─────────────┘
+ │ │
+┌────────▼───────────────────────────▼─────────────┐
+│ SQLite (WAL-Mode) │
+│ bsn_intake.db │
+└──────────────────────────────────────────────────┘
+
+Extern: OpenRouter API (Mistral 4), Meta Cloud API (WhatsApp send)
+
+
+
+4. Datenfluss
+
+
READ (~95%)
+
User → Cloudflare Edge (Cache HIT, 85%) → <50ms → FERTIG
+User → Cloudflare (Cache MISS) → nginx → gunicorn → SQLite → HTML
+ → Cloudflare cached für 5min → nächster Request = HIT
+⚠️ Flask DARF KEIN Set-Cookie auf / + /beitrag/* senden!
+
+
WRITE (~5%)
+
WhatsApp → Meta → POST /whatsapp/webhook → nginx → gunicorn:
+ 1. Signatur-Prüfung (HMAC) → 403 bei Fehler
+ 2. INSERT INTO submission_queue → 200 OK in <10ms
+
+bsn-worker (eigener Prozess, pollt alle 2s):
+ 1. SELECT pending LIMIT 3
+ 2. UPDATE status=processing
+ 3. Media-Download + Transkription (Whisper) + LLM-Triage
+ 4. INSERT INTO submissions
+ 5. UPDATE status=completed
+
+
+
+5. Komponenten
+
+
+| Komponente | Technologie | Zweck |
+| OS | Ubuntu 24.04 LTS | Minimal, kein Plesk |
+| Python | 3.13 (deadsnakes PPA) | |
+| WSGI | gunicorn 23.x | 4 Worker × 2 Threads, User: bsn |
+| Reverse-Proxy | nginx 1.24+ | Buffering, Static, Rate-Limit |
+| Framework | Flask 3.x | app.py (angepasst) |
+| DB | SQLite 3.45+ (WAL) | bsn_intake.db |
+| Queue-Worker | Eigener systemd-Service | bsn-worker.service |
+| LLM | OpenRouter Mistral 4 | Triage + Chat-Assistant |
+| Transkription | faster-whisper base | 1× geladen, kein Reload |
+| Bild | Pillow 11.x | Resize 1920×1080 |
+| Tunnel | cloudflared | Gleiche Credentials auf beiden Servern |
+| Prozess | systemd | 3 Services: nginx, bsn-chatbot, bsn-worker |
+| Firewall | ufw | Nur 22, 80, 443 |
+
+
+
+
+6. Datenbank
+
+
submissions (Haupttabelle)
+
CREATE TABLE submissions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ channel TEXT NOT NULL, -- whatsapp, telegram, web
+ sender_id TEXT NOT NULL,
+ sender_name TEXT,
+ type TEXT NOT NULL, -- text, image, audio, video
+ content TEXT,
+ media_path TEXT,
+ media_publish_flags TEXT,
+ status TEXT DEFAULT 'new', -- new, done, published, flagged, gdpr_deleted
+ category TEXT,
+ summary TEXT,
+ publish_name TEXT,
+ halle TEXT,
+ spiel_titel TEXT,
+ display_text TEXT,
+ triage_tier INTEGER, -- 1=🟢, 2=🟡, 3=🔴
+ triage_reasons TEXT, -- JSON-Array
+ triage_at TEXT,
+ auto_response_sent INTEGER DEFAULT 0,
+ public INTEGER DEFAULT 0,
+ published_at TEXT,
+ deleted_at TEXT, -- Soft-Delete für DSGVO-Löschroutine
+ created_at TEXT DEFAULT (datetime('now','localtime')),
+ -- weitere Felder: email, phone, likes, views
+);
+
+
submission_queue (Worker-Queue)
+
CREATE TABLE submission_queue (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ channel TEXT NOT NULL,
+ message_id TEXT UNIQUE NOT NULL, -- WhatsApp msg ID / Telegram update_id
+ payload TEXT NOT NULL, -- JSON: Webhook-Body
+ status TEXT DEFAULT 'pending', -- pending, processing, completed, failed
+ attempt_count INTEGER DEFAULT 0,
+ last_error TEXT,
+ created_at TEXT DEFAULT (datetime('now','localtime')),
+ updated_at TEXT DEFAULT (datetime('now','localtime'))
+);
+CREATE INDEX idx_queue_status ON submission_queue(status);
+
+
+
+7. API-Endpunkte
+
+
+| Methode | Pfad | Funktion | Cloudflare | Set-Cookie? |
+| GET | / | Frontend-Feed (Masonry) | Cache 5min | ❌ NEIN |
+| GET | /beitrag/<id> | Beitrag-Detailseite | Cache 5min | ❌ NEIN |
+| GET | /health | Health-Check (schlank) | BYPASS | ❌ |
+| GET/POST | /admin/* | Admin-Dashboard | BYPASS + Access OTP | ✅ Login-Cookie |
+| POST | /api/submit | Web-Formular | BYPASS | ❌ |
+| POST | /api/chat | Chat-Assistant | BYPASS | ❌ |
+| GET/POST | /whatsapp/webhook | WhatsApp-Intake | BYPASS | ❌ |
+| POST | /telegram/webhook | Telegram-Intake | BYPASS | ❌ |
+
+
+
+
+8. Queue-System
+
+
✅ v5.1-Fix: Queue-Worker ist ein eigener systemd-Service (bsn-worker.service), kein Daemon-Thread in gunicorns __main__. Whisper-Modell wird 1× beim Start geladen und nicht mitten in der Verarbeitung recycled.
+
+
Queue-Flow
+
+POST /whatsapp/webhook → HMAC-Prüfung → INSERT submission_queue → 200 OK (<10ms)
+
+bsn-worker (eigener systemd-Prozess):
+ 1. Whisper-Modell laden (1× beim Start)
+ 2. WHILE True:
+ SELECT ... WHERE status='pending' LIMIT 3
+ FOR EACH:
+ UPDATE status='processing'
+ → Media-Download
+ → Transkription (faster-whisper, Modell bereits geladen)
+ → LLM-Triage (OpenRouter)
+ → INSERT INTO submissions
+ → UPDATE status='completed'
+ SLEEP 2
+
+
Idempotenz
+
message_id UNIQUE verhindert Doppelverarbeitung. Meta wiederholt Webhooks? → INSERT OR IGNORE.
+
+
Fehlertoleranz
+
+| Szenario | Verhalten |
+| Worker crashed | systemd startet neu. Whisper wird neu geladen. Einträge mit status=processing >10min → Reset auf pending. |
+| Transkription fehlgeschlagen | status=failed, attempt_count++. Nach 3 Versuchen → endgültig failed. |
+| LLM nicht erreichbar | Eintrag bleibt pending, Worker retried beim nächsten Poll. |
+| Server-Neustart | Alle Zustände in SQLite persistent. Worker setzt fort. |
+
+
+
+
+9. LLM (OpenRouter Mistral 4)
+
+
# .env:
+OPENROUTER_API_KEY=sk-or-v1-...n
+# API: POST https://openrouter.ai/api/v1/chat/completions
+# Modell: "mistralai/mistral-large"
+
+| Funktion | Calls/h | Tokens/Call |
+| Triage (🟢🟡🔴) | ~50 | ~500 in, ~200 out |
+| Zusammenfassung | ~30 | ~800 in, ~150 out |
+| Chat-Assistant | ~20 | ~2000 in, ~300 out |
+| Total | ~100 | ~130k Tokens/h |
+
+
Kosten: ~$0.26/h bei Volllast.
+
+
+
+10. Cloudflare Cache + Access
+
+
+
✅ v5.1-Fix: Cache Rules als expliziter Installationsschritt. Ohne diese Regeln cached Cloudflare KEIN HTML — die gesamte Skalierungsrechnung bricht zusammen.
+
+
10.1 Cache Rules (Cloudflare Dashboard → Caching → Cache Rules)
+
+| Rule | URI Path | Cache-Status | Edge TTL |
+| Frontend + Details | /* + /beitrag/* | Eligible for Cache | 5 min |
+| API + Webhooks + Admin | /api/* + /whatsapp/* + /telegram/* + /admin/* | Bypass Cache | — |
+| Statische Assets | /static/* | Eligible for Cache | 365 Tage |
+
+
+
10.2 Kein Set-Cookie auf öffentlichen Routen
+
⚠️ Flask darf auf / und /beitrag/* KEINE Cookies setzen. Sonst überspringt Cloudflare den Cache (Cookie = private response). Ggf. @app.after_request prüfen und Session-Cookies nur für /admin/* setzen.
+
+
10.3 Cloudflare Access für /admin/*
+
✅ v5.1-Fix: Cloudflare Access wird VOR Go-Live eingerichtet, nicht als Zukunftsfeature.
+
Cloudflare Dashboard → Zero Trust → Access → Applications → Add:
+ Name: "BSN Admin"
+ Domain: chat.datenhimmel.work
+ Path: /admin/*
+ Policy: Allow, One-time PIN → E-Mail an daniel@brettspiel-news.de
+
+So ist /admin/* ab Go-Live durch OTP geschützt. Kein Passwort im Flask nötig.
+
+
+
+11. Installation (14 Schritte)
+
+
+
Schritt 1: OS
+
# dogado-Kundencenter → Neuinstallation → Ubuntu 24.04 LTS minimal
+# KEIN Plesk. Root-Passwort selbst setzen.
+
+
Schritt 2: Basis
+
apt update && apt upgrade -y
+apt install -y python3.13 python3.13-venv python3.13-dev \
+ git nginx ufw fail2ban curl unzip build-essential restic
+
+
Schritt 3: Service-User
+
✅ v5.1-Fix: Eigener User bsn statt root.
+
useradd --system --home /opt/bsn-chatbot --shell /bin/bash bsn
+mkdir -p /opt/bsn-chatbot/{src,data,logs,media,backups}
+chown -R bsn:bsn /opt/bsn-chatbot
+
+
Schritt 4: Firewall
+
ufw default deny incoming
+ufw allow 22/tcp
+ufw allow 80/tcp
+ufw allow 443/tcp
+ufw enable
+
+
Schritt 5: venv
+
su - bsn
+python3.13 -m venv /opt/bsn-chatbot/venv
+source /opt/bsn-chatbot/venv/bin/activate
+pip install --upgrade pip wheel
+
+
Schritt 6: Repo
+
cd /opt/bsn-chatbot/src
+git clone git@git.datenhimmel.work:danielkrause/BSN-Chatsystem.git .
+pip install -r requirements.txt
+pip install faster-whisper gunicorn
+
+
Schritt 7: .env
+
cat > /opt/bsn-chatbot/src/.env << 'EOF'
+DATABASE_PATH=/opt/bsn-chatbot/data/bsn_intake.db
+MEDIA_DIR=/opt/bsn-chatbot/data/media
+META_TOKEN=EAAx.....n
+OPENR....I_KEY=sk-or-v1-...n
+QUEUE_POLL_INTERVAL=2
+QUEUE_BATCH_SIZE=3
+TRIAGE_DELAY_SECONDS=360
+EOF
+chmod 600 /opt/bsn-chatbot/src/.env
+
+
Schritt 8: Daten migrieren
+
scp hermes@185.162.249.159:~/workspace/bsn-chatbot/bsn_intake.db /opt/bsn-chatbot/data/
+scp -r hermes@185.162.249.159:~/workspace/bsn-chatbot/media/* /opt/bsn-chatbot/data/media/
+chown -R bsn:bsn /opt/bsn-chatbot/data
+
+
Schritt 9: cloudflared (EINMAL)
+
✅ v5.1-Fix: Gleiche Tunnel-Credentials für beide Server. Tunnel wird EINMAL erstellt, Credentials auf beide Server kopiert.
+
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
+chmod +x cloudflared-linux-amd64
+mv cloudflared-linux-amd64 /usr/local/bin/cloudflared
+
+# Login (einmalig, Browser):
+cloudflared tunnel login
+
+# Tunnel ERSTELLEN:
+cloudflared tunnel create bsn-chat
+
+# DNS-Route (CNAME):
+cloudflared tunnel route dns bsn-chat chat.datenhimmel.work
+
+# Tunnel-ID notieren (z.B. e84dedca-...)
+
+
Schritt 10: cloudflared Config
+
cat > ~/.cloudflared/config.yml << 'CFEOF'
+tunnel: <TUNNEL_ID>
+credentials-file: ~/.cloudflared/<TUNNEL_ID>.json
+ingress:
+ - hostname: chat.datenhimmel.work
+ service: http://localhost:80
+ - service: http_status:404
+CFEOF
+
+cloudflared service install
+systemctl enable --now cloudflared
+
+
Schritt 11: Tunnel-Credentials auf Ketchup kopieren
+
✅ v5.1-Fix: Gleiche Credentials auf beiden Servern — Rollback ist DNS-Umschaltung, kein Tunnel-Wechsel.
+
# Auf dogado:
+scp ~/.cloudflared/<TUNNEL_ID>.json hermes@185.162.249.159:~/.cloudflared/
+scp ~/.cloudflared/cert.pem hermes@185.162.249.159:~/.cloudflared/
+
+# Auf Ketchup:
+cp <TUNNEL_ID>.json ~/workspace/bsn-chatbot/
+# config.yml auf Ketchup auf localhost:5002 zeigen lassen
+
+
Schritt 12: Cloudflare Cache Rules
+
Cloudflare Dashboard → Caching → Cache Rules → Create Rule:
+ Field: URI Path, Operator: contains, Value: /beitrag/
+ (plus separate rule for /static/*)
+ → Eligible for Cache, Edge TTL: 5 min / 365 days
+
+
Schritt 13: Cloudflare Access
+
Cloudflare Dashboard → Zero Trust → Access → Applications → Add:
+ Application: BSN Admin
+ Domain: chat.datenhimmel.work
+ Path: /admin/*
+ → Policy: Allow, One-time PIN → daniel@brettspiel-news.de
+
+
Schritt 14: Flask prüfen — kein Set-Cookie auf /
+
# In app.py prüfen: after_request setzt keine Cookies auf öffentlichen Routen
+grep -n "set_cookie\|Set-Cookie\|session\[" /opt/bsn-chatbot/src/app.py
+# Falls Cookies gesetzt werden → auf /admin/* beschränken
+
+
+
+12. Konfigurationsdateien
+
+
+
12.1 nginx: /etc/nginx/sites-available/bsn-chatbot
+
✅ v5.1-Fix: limit_req_zone im HTTP-Kontext (nicht Server), nginx-Cache-Direktiven entfernt (macht Cloudflare), Webhook-Pfade korrigiert.
+
# /etc/nginx/nginx.conf (HTTP-Kontext):
+http {
+ limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
+ limit_req_zone $binary_remote_addr zone=webhook:10m rate=10r/s;
+ # ...
+}
+
+# /etc/nginx/sites-available/bsn-chatbot:
+server {
+ listen 80;
+ server_name _; # cloudflared verbindet per IP
+
+ client_max_body_size 50M;
+
+ # Static Files
+ location /static/ {
+ alias /opt/bsn-chatbot/src/static/;
+ expires 365d;
+ add_header Cache-Control "public, immutable";
+ }
+
+ # Media Files
+ location /media/ {
+ alias /opt/bsn-chatbot/data/media/;
+ expires 7d;
+ }
+
+ # Webhooks — exakte Pfade
+ location ~ ^/(whatsapp|telegram)/webhook {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ limit_req zone=webhook burst=20 nodelay;
+ }
+
+ # Admin
+ location /admin {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_buffering off;
+ }
+
+ # API
+ location /api/ {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ limit_req zone=api burst=20 nodelay;
+ }
+
+ # Frontend (KEIN nginx-Cache, macht Cloudflare)
+ location / {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
+
+
12.2 gunicorn: /opt/bsn-chatbot/gunicorn.conf.py
+
bind = "127.0.0.1:8000"
+workers = 4
+threads = 2
+worker_class = "gthread"
+timeout = 120
+graceful_timeout = 30
+keepalive = 5
+user = "bsn"
+group = "bsn"
+accesslog = "/opt/bsn-chatbot/logs/gunicorn-access.log"
+errorlog = "/opt/bsn-chatbot/logs/gunicorn-error.log"
+loglevel = "info"
+proc_name = "bsn-chatbot"
+max_requests = 1000
+max_requests_jitter = 100
+
+
+
+13. systemd-Services
+
+
+
13.1 bsn-chatbot.service (gunicorn)
+
[Unit]
+Description=BSN Chatbot — gunicorn WSGI
+After=network.target
+
+[Service]
+Type=notify
+User=bsn
+Group=bsn
+WorkingDirectory=/opt/bsn-chatbot/src
+EnvironmentFile=/opt/bsn-chatbot/src/.env
+ExecStart=/opt/bsn-chatbot/venv/bin/gunicorn --config /opt/bsn-chatbot/gunicorn.conf.py app:app
+ExecReload=/bin/kill -s HUP $MAINPID
+Restart=always
+RestartSec=5
+PrivateTmp=true
+NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+ReadWritePaths=/opt/bsn-chatbot/data /opt/bsn-chatbot/logs /opt/bsn-chatbot/backups
+ReadOnlyPaths=/opt/bsn-chatbot/src
+
+[Install]
+WantedBy=multi-user.target
+
+
13.2 bsn-worker.service (Queue)
+
✅ v5.1-Fix: Worker als eigener systemd-Service — kein Daemon-Thread unter gunicorn.
+
[Unit]
+Description=BSN Queue Worker — Submission-Verarbeitung
+After=bsn-chatbot.service
+Wants=bsn-chatbot.service
+
+[Service]
+Type=simple
+User=bsn
+Group=bsn
+WorkingDirectory=/opt/bsn-chatbot/src
+EnvironmentFile=/opt/bsn-chatbot/src/.env
+ExecStart=/opt/bsn-chatbot/venv/bin/python worker.py
+Restart=always
+RestartSec=10
+PrivateTmp=true
+NoNewPrivileges=true
+ProtectSystem=strict
+ProtectHome=true
+ReadWritePaths=/opt/bsn-chatbot/data /opt/bsn-chatbot/logs
+ReadOnlyPaths=/opt/bsn-chatbot/src
+
+[Install]
+WantedBy=multi-user.target
+
+
13.3 worker.py (neue Datei)
+
#!/usr/bin/env python3
+"""Standalone Queue-Worker — eigener Prozess, nicht unter gunicorn."""
+import os, sys, time, json, sqlite3
+sys.path.insert(0, '/opt/bsn-chatbot/src')
+
+from dotenv import load_dotenv
+load_dotenv()
+
+DB = os.environ['DATABASE_PATH']
+POLL = int(os.environ.get('QUEUE_POLL_INTERVAL', 2))
+BATCH = int(os.environ.get('QUEUE_BATCH_SIZE', 3))
+
+# Whisper 1× laden (kein Recycling mitten in der Verarbeitung)
+from faster_whisper import WhisperModel
+model = WhisperModel("base", device="cpu", compute_type="int8")
+
+def get_db():
+ db = sqlite3.connect(DB)
+ db.row_factory = sqlite3.Row
+ db.execute("PRAGMA journal_mode=WAL")
+ db.execute("PRAGMA busy_timeout=5000")
+ return db
+
+def process_one(db, row):
+ # → Media-Download, Transkription, LLM-Triage, DB-Insert
+ # (Implementierung aus queue_worker.py übernehmen)
+ pass
+
+def main():
+ print("[worker] Gestartet, Whisper-Modell geladen.", flush=True)
+ db = get_db()
+ while True:
+ try:
+ rows = db.execute(
+ "SELECT * FROM submission_queue WHERE status='pending' ORDER BY created_at LIMIT ?",
+ (BATCH,)).fetchall()
+ for row in rows:
+ db.execute("UPDATE submission_queue SET status='processing' WHERE id=?", (row['id'],))
+ db.commit()
+ try:
+ process_one(db, row)
+ db.execute("UPDATE submission_queue SET status='completed' WHERE id=?", (row['id'],))
+ except Exception as e:
+ db.execute(
+ "UPDATE submission_queue SET status='failed', last_error=?, attempt_count=attempt_count+1 WHERE id=?",
+ (str(e)[:500], row['id']))
+ db.commit()
+ except Exception as e:
+ print(f"[worker] Fehler: {e}", flush=True)
+ time.sleep(POLL)
+
+if __name__ == '__main__':
+ main()
+
+
13.4 Aktivierung
+
systemctl daemon-reload
+systemctl enable --now bsn-chatbot bsn-worker nginx
+# cloudflared ist bereits aktiv (Schritt 10)
+
+
+
+14. Monitoring
+
+
+| Komponente | Log |
+| gunicorn Access | /opt/bsn-chatbot/logs/gunicorn-access.log |
+| gunicorn Error | /opt/bsn-chatbot/logs/gunicorn-error.log |
+| Worker | journalctl -u bsn-worker |
+| Flask | journalctl -u bsn-chatbot |
+| nginx | /var/log/nginx/access.log |
+
+
+
/health (abgespeckt)
+
✅ v5.1-Fix: /health wurde entschlackt — nur noch status, db, whatsapp_configured.
+
GET /health → {"status":"ok","db":"bsn_intake.db","whatsapp_configured":true}
+
+
Watchdog (Hermes-Server)
+
# Cronjob alle 5min:
+curl -s -o /dev/null -w "%{http_code}" https://chat.datenhimmel.work/health | grep -q 200
+# Bei Fehler → Telegram an Daniel
+
+
+
+15. Sicherheit
+
+
+| Aspekt | Maßnahme |
+| Firewall | Nur 22, 80, 443. Flask (8000) nur localhost. |
+| SSH | Key-basiert, kein Passwort |
+| Webhook | HMAC-SHA256 Signatur-Prüfung |
+| Rate-Limit | nginx: 30 r/s (api), 10 r/s (webhook) |
+| SQL-Injection | Parameterisierte Queries |
+| XSS | Jinja2 Autoescaping |
+| Secrets | .env, chmod 600, nicht in Git |
+| systemd | ProtectSystem=strict, NoNewPrivileges |
+| Admin | Cloudflare Access OTP (E-Mail) |
+| Service-User | bsn (nicht root) |
+
+
+
+
+16. Backup & Recovery
+
+
✅ v5.1-Fix: Offsite-Backup via rsync zum Hermes-Server (185.162.249.159) statt nur lokal.
+
+
16.1 Backup-Script (/opt/bsn-chatbot/backup.sh)
+
#!/bin/bash
+set -e
+BACKUP_DIR=/opt/bsn-chatbot/backups
+HERMES="hermes@185.162.249.159:/home/hermes/backups/bsn-chatbot"
+DATE=$(date +%Y%m%d-%H%M)
+
+mkdir -p $BACKUP_DIR
+
+# Lokales DB-Backup
+sqlite3 /opt/bsn-chatbot/data/bsn_intake.db \
+ ".backup $BACKUP_DIR/bsn_intake_$DATE.db"
+
+# Offsite (zum Hermes-Server)
+rsync -avz --delete $BACKUP_DIR/ $HERMES/db/
+rsync -avz /opt/bsn-chatbot/data/media/ $HERMES/media/
+
+# Rotation: 72 lokale Backups behalten
+ls -t $BACKUP_DIR/bsn_intake_*.db | tail -n +73 | xargs rm -f
+
+
16.2 Cron für Backup
+
# /etc/cron.d/bsn-backup
+0 * * * * bsn /opt/bsn-chatbot/backup.sh
+
+
16.3 Recovery
+
LAST=$(ls -t /opt/bsn-chatbot/backups/bsn_intake_*.db | head -1)
+systemctl stop bsn-chatbot bsn-worker
+cp $LAST /opt/bsn-chatbot/data/bsn_intake.db
+systemctl start bsn-chatbot bsn-worker
+
+
+
+17. DSGVO-Löschroutine
+
+
✅ v5.1-Fix: Automatische Löschroutine ist Teil von v1, nicht Zukunftsfeature.
+
+
17.1 WhatsApp: „Meine Daten löschen"
+
# Bereits implementiert in app.py:
+# Nutzer sendet "Meine Daten löschen" → alle Submissions dieser Nummer:
+# UPDATE submissions SET deleted_at=CURRENT_TIMESTAMP, status='gdpr_deleted'
+# Medien von Platte löschen
+# Auto-Reply: Bestätigung
+
+
17.2 Web: Referenz-ID
+
# Bereits implementiert:
+# Nach Web-Submit: Referenz-ID anzeigen + Kopier-Button
+# Löschung aktuell via Kontaktaufnahme Redaktion
+# ⚠️ Selbstbedienungs-Löschformular: jetzt in v1 umsetzen
+# → /loeschen Route: ID eingeben → DELETE
+
+
17.3 Löschroutine (Cron)
+
# /opt/bsn-chatbot/purge.py (täglich via Cron):
+# SELECT * FROM submissions WHERE deleted_at IS NOT NULL
+# AND deleted_at < date('now', '-30 days')
+# → Medien von Platte löschen, DB-Eintrag hart löschen
+# → DSGVO-Vermerk bleibt in separater Tabelle
+
+
+
+18. Go-Live
+
+
+- Ketchup cloudflared STOPPEN:
systemctl stop cloudflared
+- Dogado cloudflared STARTEN:
systemctl start cloudflared
+- DNS prüfen: CNAME chat.datenhimmel.work → Tunnel-ID (Cloudflare Dashboard)
+- Warten: 2-5 Min DNS-Propagation
+- Verifikation:
curl https://chat.datenhimmel.work/health → 200
+- 10-Punkte-Checkliste durchgehen
+- 72h warten bevor alter Server endgültig abgeschaltet wird
+
+
+
+
+19. Go-Live-Checkliste
+
+
+| # | Test | Erwartet |
+| 1 | curl https://chat.datenhimmel.work/health | 200, "status":"ok" |
+| 2 | curl -sI https://chat.datenhimmel.work/ | 200, cf-cache-status: HIT/MISS |
+| 3 | curl https://chat.datenhimmel.work/beitrag/1 | 200, HTML |
+| 4 | curl "https://chat.datenhimmel.work/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=br3ttsp1el&hub.challenge=test" | "test" |
+| 5 | Web-Submit POST | 200, "status":"queued" |
+| 6 | Nach ~30s: Admin prüfen | Submission mit Triage |
+| 7 | Admin lädt | 200, Submissions-Tabelle |
+| 8 | Cloudflare Access | OTP-Mail bei /admin |
+| 9 | Chat-Assistant | 200, JSON mit answer |
+| 10 | WhatsApp „Meine Daten löschen" | Bestätigung, Daten gelöscht |
+
+
+
+
+20. Fallback
+
+
✅ v5.1-Fix: Gleiche Tunnel-Credentials auf beiden Servern. Rollback = systemctl start cloudflared auf Ketchup, systemctl stop cloudflared auf dogado. Kein DNS-Change nötig — Cloudflare routet zum aktiven Connector.
+
+
Rollback (innerhalb 72h)
+
# Auf Ketchup:
+systemctl start cloudflared
+# Auf dogado:
+systemctl stop cloudflared
+# Fertig. Cloudflare sieht den aktiven Connector auf Ketchup.
+
+
Nach 72 Stunden
+
# Auf Ketchup:
+systemctl stop bsn-chatbot
+rm ~/.cloudflared/<TUNNEL_ID>.json # Credentials entfernen
+
+
+
+21. Code-Änderungen
+
+
+| Datei | Änderung | Dauer |
+worker.py (NEU) | Standalone Queue-Worker (Extrakt aus app.py + queue_worker.py) | 45 min |
+app.py | Webhook-Routen: Queue.enqueue() statt sync. LLM: OpenRouter. TTS raus. Kein Set-Cookie auf /. | 45 min |
+app.py | /health entschlacken (3 Felder) | 5 min |
+purge.py (NEU) | DSGVO-Löschroutine (Cronjob) | 30 min |
+app.py | /loeschen Route (Selbstbedienung Web-ID) | 20 min |
+requirements.txt | gunicorn hinzufügen, DeepSeek entfernen | 5 min |
+
+
Gesamt: ~2,5 Stunden
+
+
+
+22. Skalierung (1000/h)
+
+
+| Pfad | Anteil | Req/h | Serverlast |
+| Frontend (Cache HIT) | 85% | 850 | 0% (Cloudflare) |
+| Details (Cache MISS) | 8% | 80 | ~1% CPU |
+| Daten senden | 5% | 50 | ~2% CPU |
+| Chat-Assistant | 2% | 20 | ~1% CPU + Netzwerk |
+| Total | | 1000 | ~4% CPU |
+
+
Server (12 GB, 6 Cores, NVMe) ist für 1000/h komfortabel überdimensioniert.
+
+
+
+23. Zukunft
+
+
+| Erweiterung | Auslöser |
+| Lokales LLM | OpenRouter-Kosten >100 €/Monat |
+| TTS (Piper) | WhatsApp-Sprachantworten gewünscht |
+| SQLite → PostgreSQL | >500 Submissions/h |
+
+
+
+
+24. Änderungen v5 → v5.1
+
+
+| Punkt | v5.0 | v5.1 |
+| Queue-Worker | Daemon-Thread in gunicorn __main__ | Eigener systemd-Service (worker.py) |
+| nginx limit_req_zone | Im server-Kontext (geht nicht) | Im http-Kontext |
+| nginx Cache | proxy_cache_valid gesetzt | Entfernt (macht Cloudflare) |
+| Webhook nginx | location /webhook | location ~ ^/(whatsapp|telegram)/webhook |
+| Cloudflare Cache | Nur Strategie beschrieben | Expliziter Installationsschritt + Set-Cookie-Prüfung |
+| Admin-Schutz | Zukunftsfeature | Cloudflare Access vor Go-Live |
+| Tunnel-Failover | Zwei getrennte Tunnel | Gleiche Credentials, beide Server |
+| Offsite-Backup | Nur lokal | rsync zum Hermes-Server |
+| Service-User | root | bsn |
+| DSGVO-Löschroutine | Nicht in v1 | In v1: purge.py + Cron |
+| /health | 8 Felder | 3 Felder (status, db, whatsapp) |
+| Port-5002 | Mehrfach referenziert | Nur noch :8000 (gunicorn) + :80 (nginx) |
+
+
+
+
+
Ende des Plans v5.1
+Cody (Hermes Agent) + externes Review — 14.07.2026
+Status: ✅ Umsetzungsreif
+
+
+
+