From 4d02f51304732e14371567c9ff119eefbf3d8875 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 14 Jul 2026 18:10:59 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Implementierungsplan=20v5.1=20=E2=80=94?= =?UTF-8?q?=20extern=20gepr=C3=BCft,=20umsetzungsreif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-14-bsn-chatsystem-v2.1-final.html | 871 ++++++++++++++++++ 1 file changed, 871 insertions(+) create mode 100644 plans/2026-07-14-bsn-chatsystem-v2.1-final.html 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)

+ +
+ + + + + + +
Datum14. Juli 2026
Version5.1 — Externes Review eingearbeitet
AutorCody (Hermes Agent), mit Korrekturen von externem Prüfer
Repositorygit@git.datenhimmel.work:danielkrause/BSN-Chatsystem.git
Status✅ Umsetzungsreif
+
+ +
+

📑 Inhalt

+
    +
  1. Ziel & Rahmenbedingungen
  2. +
  3. Server
  4. +
  5. Architektur
  6. +
  7. Datenfluss
  8. +
  9. Komponenten
  10. +
  11. Datenbank
  12. +
  13. API-Endpunkte
  14. +
  15. Queue-System
  16. +
  17. LLM (OpenRouter Mistral 4)
  18. +
  19. Cloudflare-Cache + Access
  20. +
  21. Installation
  22. +
  23. Konfigurationsdateien
  24. +
  25. systemd-Services
  26. +
  27. Monitoring
  28. +
  29. Sicherheit
  30. +
  31. Backup & Recovery
  32. +
  33. DSGVO-Löschroutine
  34. +
  35. Go-Live
  36. +
  37. Checkliste (10 Punkte)
  38. +
  39. Fallback
  40. +
  41. Code-Änderungen
  42. +
  43. Skalierung (1000/h)
  44. +
  45. Zukunft
  46. +
  47. Änderungsprotokoll v5→v5.1
  48. +
+
+ + +

1. Ziel & Rahmenbedingungen

+
+

Ziel: Das BSN-Chatsystem (chat.datenhimmel.work) 1:1 funktional auf dedizierten dogado-Server migrieren, Architektur produktionsreif.

+ + + + + + + + + + +
RahmenbedingungWert
Funktionsumfang100% identisch zum aktuellen System
LLMOpenRouter → Mistral 4 (europäisch, DSGVO)
TTSDeaktiviert (später optional)
QueueEigener systemd-Service (nicht Daemon-Thread)
Skalierungsziel~1000 Nutzer/h
Admin-SchutzCloudflare Access (vor Go-Live)
BackupOffsite via rsync zum Hermes-Server
Service-Userbsn (nicht root)
+
+ + +

2. Server

+
+ + + + + + + + +
EigenschaftWert
IP89.22.110.107 (dogado GmbH)
CPUXeon E5-2680 v3, 6 Cores @ 2.50 GHz
RAM12 GB
Disk492 GB SSD
OS ZielUbuntu 24.04 LTS minimal (KEIN Plesk)
Offene PortsNUR 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

+
+ + + + + + + + + + + + + + + +
KomponenteTechnologieZweck
OSUbuntu 24.04 LTSMinimal, kein Plesk
Python3.13 (deadsnakes PPA)
WSGIgunicorn 23.x4 Worker × 2 Threads, User: bsn
Reverse-Proxynginx 1.24+Buffering, Static, Rate-Limit
FrameworkFlask 3.xapp.py (angepasst)
DBSQLite 3.45+ (WAL)bsn_intake.db
Queue-WorkerEigener systemd-Servicebsn-worker.service
LLMOpenRouter Mistral 4Triage + Chat-Assistant
Transkriptionfaster-whisper base1× geladen, kein Reload
BildPillow 11.xResize 1920×1080
TunnelcloudflaredGleiche Credentials auf beiden Servern
Prozesssystemd3 Services: nginx, bsn-chatbot, bsn-worker
FirewallufwNur 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

+
+ + + + + + + + + + +
MethodePfadFunktionCloudflareSet-Cookie?
GET/Frontend-Feed (Masonry)Cache 5min❌ NEIN
GET/beitrag/<id>Beitrag-DetailseiteCache 5min❌ NEIN
GET/healthHealth-Check (schlank)BYPASS
GET/POST/admin/*Admin-DashboardBYPASS + Access OTP✅ Login-Cookie
POST/api/submitWeb-FormularBYPASS
POST/api/chatChat-AssistantBYPASS
GET/POST/whatsapp/webhookWhatsApp-IntakeBYPASS
POST/telegram/webhookTelegram-IntakeBYPASS
+
+ + +

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

+ + + + + + +
SzenarioVerhalten
Worker crashedsystemd startet neu. Whisper wird neu geladen. Einträge mit status=processing >10min → Reset auf pending.
Transkription fehlgeschlagenstatus=failed, attempt_count++. Nach 3 Versuchen → endgültig failed.
LLM nicht erreichbarEintrag bleibt pending, Worker retried beim nächsten Poll.
Server-NeustartAlle 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"
+ + + + + + +
FunktionCalls/hTokens/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)

+ + + + + +
RuleURI PathCache-StatusEdge TTL
Frontend + Details/* + /beitrag/*Eligible for Cache5 min
API + Webhooks + Admin/api/* + /whatsapp/* + /telegram/* + /admin/*Bypass Cache
Statische Assets/static/*Eligible for Cache365 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

+
+ + + + + + + +
KomponenteLog
gunicorn Access/opt/bsn-chatbot/logs/gunicorn-access.log
gunicorn Error/opt/bsn-chatbot/logs/gunicorn-error.log
Workerjournalctl -u bsn-worker
Flaskjournalctl -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

+
+ + + + + + + + + + + + +
AspektMaßnahme
FirewallNur 22, 80, 443. Flask (8000) nur localhost.
SSHKey-basiert, kein Passwort
WebhookHMAC-SHA256 Signatur-Prüfung
Rate-Limitnginx: 30 r/s (api), 10 r/s (webhook)
SQL-InjectionParameterisierte Queries
XSSJinja2 Autoescaping
Secrets.env, chmod 600, nicht in Git
systemdProtectSystem=strict, NoNewPrivileges
AdminCloudflare Access OTP (E-Mail)
Service-Userbsn (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

+
+
    +
  1. Ketchup cloudflared STOPPEN: systemctl stop cloudflared
  2. +
  3. Dogado cloudflared STARTEN: systemctl start cloudflared
  4. +
  5. DNS prüfen: CNAME chat.datenhimmel.work → Tunnel-ID (Cloudflare Dashboard)
  6. +
  7. Warten: 2-5 Min DNS-Propagation
  8. +
  9. Verifikation: curl https://chat.datenhimmel.work/health → 200
  10. +
  11. 10-Punkte-Checkliste durchgehen
  12. +
  13. 72h warten bevor alter Server endgültig abgeschaltet wird
  14. +
+
+ + +

19. Go-Live-Checkliste

+
+ + + + + + + + + + + + +
#TestErwartet
1curl https://chat.datenhimmel.work/health200, "status":"ok"
2curl -sI https://chat.datenhimmel.work/200, cf-cache-status: HIT/MISS
3curl https://chat.datenhimmel.work/beitrag/1200, HTML
4curl "https://chat.datenhimmel.work/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=br3ttsp1el&hub.challenge=test""test"
5Web-Submit POST200, "status":"queued"
6Nach ~30s: Admin prüfenSubmission mit Triage
7Admin lädt200, Submissions-Tabelle
8Cloudflare AccessOTP-Mail bei /admin
9Chat-Assistant200, JSON mit answer
10WhatsApp „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ÄnderungDauer
worker.py (NEU)Standalone Queue-Worker (Extrakt aus app.py + queue_worker.py)45 min
app.pyWebhook-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.txtgunicorn hinzufügen, DeepSeek entfernen5 min
+

Gesamt: ~2,5 Stunden

+
+ + +

22. Skalierung (1000/h)

+
+ + + + + + + +
PfadAnteilReq/hServerlast
Frontend (Cache HIT)85%8500% (Cloudflare)
Details (Cache MISS)8%80~1% CPU
Daten senden5%50~2% CPU
Chat-Assistant2%20~1% CPU + Netzwerk
Total1000~4% CPU
+

Server (12 GB, 6 Cores, NVMe) ist für 1000/h komfortabel überdimensioniert.

+
+ + +

23. Zukunft

+
+ + + + + +
ErweiterungAuslöser
Lokales LLMOpenRouter-Kosten >100 €/Monat
TTS (Piper)WhatsApp-Sprachantworten gewünscht
SQLite → PostgreSQL>500 Submissions/h
+
+ + +

24. Änderungen v5 → v5.1

+
+ + + + + + + + + + + + + + +
Punktv5.0v5.1
Queue-WorkerDaemon-Thread in gunicorn __main__Eigener systemd-Service (worker.py)
nginx limit_req_zoneIm server-Kontext (geht nicht)Im http-Kontext
nginx Cacheproxy_cache_valid gesetztEntfernt (macht Cloudflare)
Webhook nginxlocation /webhooklocation ~ ^/(whatsapp|telegram)/webhook
Cloudflare CacheNur Strategie beschriebenExpliziter Installationsschritt + Set-Cookie-Prüfung
Admin-SchutzZukunftsfeatureCloudflare Access vor Go-Live
Tunnel-FailoverZwei getrennte TunnelGleiche Credentials, beide Server
Offsite-BackupNur lokalrsync zum Hermes-Server
Service-Userrootbsn
DSGVO-LöschroutineNicht in v1In v1: purge.py + Cron
/health8 Felder3 Felder (status, db, whatsapp)
Port-5002Mehrfach referenziertNur noch :8000 (gunicorn) + :80 (nginx)
+
+ +
+

Ende des Plans v5.1
+Cody (Hermes Agent) + externes Review — 14.07.2026
+Status: ✅ Umsetzungsreif

+
+ + +