Files
breakpilot-lehrer/website/app/admin/alerts/_components/DocumentationTab.tsx
Benjamin Admin 0b37c5e692 [split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split):
- compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20)
- quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11)
- i18n.ts (1,173 → 8 language files)
- unity-bridge (1,094 → 12), backlog (1,087 → 6)
- training (1,066 → 8), rag (1,063 → 8)
- Deleted index_original.ts (4,899 LOC dead backup)

Studio-v2 (5 monoliths split):
- meet/page.tsx (1,481 → 9), messages (1,166 → 9)
- AlertsB2BContext.tsx (1,165 → 5 modules)
- alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6)

All existing imports preserved. Zero new TypeScript errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 17:52:36 +02:00

431 lines
28 KiB
TypeScript

'use client'
export default function DocumentationTab() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6 overflow-auto max-h-[calc(100vh-200px)]">
<div className="prose prose-slate max-w-none prose-headings:font-semibold prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg prose-pre:bg-slate-900 prose-pre:text-slate-100">
{/* Header */}
<div className="not-prose mb-8 pb-6 border-b border-slate-200">
<h1 className="text-2xl font-bold text-slate-900">BreakPilot Alerts Agent</h1>
<p className="text-sm text-slate-500 mt-1">Version: 1.0.0 | Stand: Januar 2026 | Autor: BreakPilot Development Team</p>
</div>
{/* Audit Box */}
<div className="not-prose bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-blue-900 mb-2">Audit-Relevante Informationen</h3>
<p className="text-sm text-blue-800">
Dieses Dokument dient als technische Dokumentation fuer das Alert-Monitoring-System der BreakPilot Plattform.
Es ist fuer Audits durch Bildungstraeger und Datenschutzbeauftragte konzipiert.
</p>
</div>
{/* Ziel des Systems */}
<h2>Ziel des Alert-Systems</h2>
<p>Das System ermoeglicht automatisierte Ueberwachung von Bildungsthemen mit:</p>
<ul>
<li><strong>Google Alerts Integration</strong>: RSS-Feeds von Google Alerts automatisch abrufen</li>
<li><strong>RSS/Atom Feeds</strong>: Beliebige Nachrichtenquellen einbinden</li>
<li><strong>KI-Relevanzpruefung</strong>: Automatische Bewertung der Relevanz durch LLM</li>
<li><strong>Regelbasierte Filterung</strong>: Flexible Regeln fuer automatische Sortierung</li>
<li><strong>Multi-Channel Actions</strong>: E-Mail, Webhook, Slack Benachrichtigungen</li>
<li><strong>Few-Shot Learning</strong>: Profil verbessert sich durch Nutzerfeedback</li>
</ul>
{/* Datenschutz Compliance */}
<h2>Datenschutz-Compliance</h2>
<DocTable
headers={['Massnahme', 'Umsetzung', 'Wirkung']}
rows={[
['100% Self-Hosted', 'Alle Dienste auf eigenen Servern', 'Keine Cloud-Abhaengigkeit'],
['Lokale KI', 'Ollama/vLLM on-premise', 'Keine Daten an OpenAI etc.'],
['URL-Deduplizierung', 'SHA256-Hash, Tracking entfernt', 'Minimale Datenspeicherung'],
['Soft-Delete', 'Archivierung statt Loeschung', 'Audit-Trail erhalten'],
['RBAC', 'Rollenbasierte Zugriffskontrolle', 'Nur autorisierter Zugriff'],
]}
/>
{/* Architektur */}
<h2>1. Systemarchitektur</h2>
<h3>Gesamtarchitektur</h3>
<pre className="text-xs leading-tight overflow-x-auto">{ARCHITECTURE_DIAGRAM}</pre>
{/* Datenfluss */}
<h3>Datenfluss bei Alert-Verarbeitung</h3>
<pre className="text-xs leading-tight overflow-x-auto">{DATAFLOW_DIAGRAM}</pre>
{/* Feed Ingestion */}
<h2>2. Feed Ingestion</h2>
<h3>RSS Fetcher</h3>
<DocTable
headers={['Eigenschaft', 'Wert', 'Beschreibung']}
rows={[
['Parser', 'feedparser 6.x', 'Standard RSS/Atom Parser'],
['HTTP Client', 'httpx (async)', 'Non-blocking Fetches'],
['Timeout', '30 Sekunden', 'Konfigurierbar'],
['Parallelitaet', 'Ja (asyncio.gather)', 'Mehrere Feeds gleichzeitig'],
]}
/>
<h3>Deduplizierung</h3>
<p>Die Deduplizierung verhindert doppelte Alerts durch:</p>
<ol>
<li><strong>URL-Normalisierung</strong>: Tracking-Parameter entfernen (utm_*, fbclid, gclid), Hostname lowercase, Trailing Slash entfernen</li>
<li><strong>URL-Hash</strong>: SHA256 der normalisierten URL, erste 16 Zeichen als Index</li>
</ol>
{/* Rule Engine */}
<h2>3. Rule Engine</h2>
<h3>Unterstuetzte Operatoren</h3>
<DocTable
headers={['Operator', 'Beschreibung', 'Beispiel']}
rows={[
['contains', 'Text enthaelt', 'title contains "Inklusion"'],
['not_contains', 'Text enthaelt nicht', 'title not_contains "Werbung"'],
['equals', 'Exakte Uebereinstimmung', 'status equals "new"'],
['regex', 'Regulaerer Ausdruck', 'title regex "\\d{4}"'],
['gt / lt', 'Groesser/Kleiner', 'relevance_score gt 0.8'],
['in', 'In Liste enthalten', 'title in ["KI", "AI"]'],
]}
monoFirstCol
/>
<h3>Verfuegbare Felder</h3>
<DocTable
headers={['Feld', 'Typ', 'Beschreibung']}
rows={[
['title', 'String', 'Alert-Titel'],
['snippet', 'String', 'Textausschnitt'],
['url', 'String', 'Artikel-URL'],
['source', 'Enum', 'google_alerts_rss, rss_feed, manual'],
['relevance_score', 'Float', '0.0 - 1.0'],
['relevance_decision', 'Enum', 'KEEP, DROP, REVIEW'],
]}
monoFirstCol
/>
<h3>Aktionen</h3>
<DocTable
headers={['Aktion', 'Beschreibung', 'Konfiguration']}
rows={[
['keep', 'Als relevant markieren', '-'],
['drop', 'Archivieren', '-'],
['tag', 'Tags hinzufuegen', '{"tags": ["wichtig"]}'],
['email', 'E-Mail senden', '{"to": "x@y.de"}'],
['webhook', 'HTTP POST', '{"url": "https://..."}'],
['slack', 'Slack-Nachricht', '{"webhook_url": "..."}'],
]}
monoFirstCol
/>
{/* KI Relevanzpruefung */}
<h2>4. KI-Relevanzpruefung</h2>
<h3>LLM Scorer</h3>
<DocTable
headers={['Eigenschaft', 'Wert', 'Beschreibung']}
rows={[
['Gateway URL', 'http://localhost:8000/llm', 'LLM Gateway Endpoint'],
['Modell', 'breakpilot-teacher-8b', 'Fein-getuntes Llama 3.1'],
['Temperatur', '0.3', 'Niedrig fuer Konsistenz'],
['Max Tokens', '500', 'Response-Limit'],
]}
/>
<h3>Bewertungskriterien</h3>
<div className="not-prose overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-2 text-left font-semibold">Entscheidung</th>
<th className="px-4 py-2 text-left font-semibold">Score-Bereich</th>
<th className="px-4 py-2 text-left font-semibold">Bedeutung</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr className="bg-green-50"><td className="px-4 py-2 font-semibold text-green-800">KEEP</td><td className="px-4 py-2">0.7 - 1.0</td><td className="px-4 py-2">Klar relevant, in Inbox anzeigen</td></tr>
<tr className="bg-amber-50"><td className="px-4 py-2 font-semibold text-amber-800">REVIEW</td><td className="px-4 py-2">0.4 - 0.7</td><td className="px-4 py-2">Unsicher, Nutzer entscheidet</td></tr>
<tr className="bg-red-50"><td className="px-4 py-2 font-semibold text-red-800">DROP</td><td className="px-4 py-2">0.0 - 0.4</td><td className="px-4 py-2">Irrelevant, automatisch archivieren</td></tr>
</tbody>
</table>
</div>
<h3>Few-Shot Learning</h3>
<p>Das Profil verbessert sich durch Nutzerfeedback:</p>
<ol>
<li>Nutzer markiert Alert als relevant/irrelevant</li>
<li>Alert wird als positives/negatives Beispiel gespeichert</li>
<li>Beispiele werden in den Prompt eingefuegt (max. 5 pro Kategorie)</li>
<li>LLM lernt aus konkreten Beispielen des Nutzers</li>
</ol>
{/* Relevanz Profile */}
<h2>5. Relevanz-Profile</h2>
<h3>Standard-Bildungsprofil</h3>
<DocTable
headers={['Prioritaet', 'Gewicht', 'Keywords']}
rows={[
['Inklusion', '0.9', 'inklusiv, Foerderbedarf, Behinderung'],
['Datenschutz Schule', '0.85', 'DSGVO, Schuelerfotos, Einwilligung'],
['Schulrecht Bayern', '0.8', 'BayEUG, Schulordnung, KM'],
['Digitalisierung Schule', '0.7', 'DigitalPakt, Tablet-Klasse'],
]}
/>
<h3>Standard-Ausschluesse</h3>
<ul>
<li>Stellenanzeige</li>
<li>Praktikum gesucht</li>
<li>Werbung</li>
<li>Pressemitteilung</li>
</ul>
{/* API Endpoints */}
<h2>6. API Endpoints</h2>
<h3>Alerts API</h3>
<DocTable
headers={['Endpoint', 'Methode', 'Beschreibung']}
rows={[
['/api/alerts/inbox', 'GET', 'Inbox Items abrufen'],
['/api/alerts/ingest', 'POST', 'Manuell Alert importieren'],
['/api/alerts/run', 'POST', 'Scoring-Pipeline starten'],
['/api/alerts/feedback', 'POST', 'Relevanz-Feedback geben'],
['/api/alerts/stats', 'GET', 'Statistiken abrufen'],
]}
monoFirstCol
/>
<h3>Topics API</h3>
<DocTable
headers={['Endpoint', 'Methode', 'Beschreibung']}
rows={[
['/api/alerts/topics', 'GET', 'Topics auflisten'],
['/api/alerts/topics', 'POST', 'Neues Topic erstellen'],
['/api/alerts/topics/{id}', 'PUT', 'Topic aktualisieren'],
['/api/alerts/topics/{id}', 'DELETE', 'Topic loeschen (CASCADE)'],
['/api/alerts/topics/{id}/fetch', 'POST', 'Manueller Feed-Abruf'],
]}
monoFirstCol
/>
<h3>Rules & Profile API</h3>
<DocTable
headers={['Endpoint', 'Methode', 'Beschreibung']}
rows={[
['/api/alerts/rules', 'GET/POST', 'Regeln verwalten'],
['/api/alerts/rules/{id}', 'PUT/DELETE', 'Regel bearbeiten/loeschen'],
['/api/alerts/profile', 'GET', 'Profil abrufen'],
['/api/alerts/profile', 'PUT', 'Profil aktualisieren'],
['/api/alerts/scheduler/status', 'GET', 'Scheduler-Status'],
]}
monoFirstCol
/>
{/* Datenbank Schema */}
<h2>7. Datenbank-Schema</h2>
<h3>Tabellen</h3>
<DocTable
headers={['Tabelle', 'Beschreibung', 'Wichtige Felder']}
rows={[
['alert_topics', 'Feed-Quellen', 'name, feed_url, feed_type, is_active, fetch_interval'],
['alert_items', 'Einzelne Alerts', 'title, url, url_hash, relevance_score, relevance_decision'],
['alert_rules', 'Filterregeln', 'name, conditions (JSON), action_type, priority'],
['alert_profiles', 'Nutzer-Profile', 'priorities, exclusions, positive/negative_examples'],
]}
monoFirstCol
/>
{/* DSGVO */}
<h2>8. DSGVO-Konformitaet</h2>
<h3>Rechtsgrundlage (Art. 6 DSGVO)</h3>
<DocTable
headers={['Verarbeitung', 'Rechtsgrundlage', 'Umsetzung']}
rows={[
['Feed-Abruf', 'Art. 6(1)(f) - Berechtigtes Interesse', 'Informationsbeschaffung'],
['Alert-Speicherung', 'Art. 6(1)(f) - Berechtigtes Interesse', 'Nur oeffentliche Informationen'],
['LLM-Scoring', 'Art. 6(1)(f) - Berechtigtes Interesse', 'On-Premise, keine PII'],
['Profil-Learning', 'Art. 6(1)(a) - Einwilligung', 'Opt-in durch Nutzung'],
]}
/>
<h3>Technische Datenschutz-Massnahmen</h3>
<ul>
<li><strong>Datenminimierung</strong>: Nur Titel, URL, Snippet - keine personenbezogenen Daten</li>
<li><strong>Lokale Verarbeitung</strong>: Ollama/vLLM on-premise - kein Datenabfluss an Cloud</li>
<li><strong>Pseudonymisierung</strong>: UUIDs statt Namen</li>
<li><strong>Automatische Loeschung</strong>: 90 Tage Retention fuer archivierte Alerts</li>
<li><strong>Audit-Logging</strong>: Stats und Match-Counts fuer Nachvollziehbarkeit</li>
</ul>
{/* Open Source */}
<h2>9. Open Source Lizenzen (SBOM)</h2>
<h3>Python Dependencies</h3>
<DocTable
headers={['Komponente', 'Lizenz', 'Kommerziell']}
rows={[
['FastAPI', 'MIT', 'Ja'],
['SQLAlchemy', 'MIT', 'Ja'],
['httpx', 'BSD-3-Clause', 'Ja'],
['feedparser', 'BSD-2-Clause', 'Ja'],
['APScheduler', 'MIT', 'Ja'],
]}
greenLastCol
/>
<h3>KI-Komponenten</h3>
<DocTable
headers={['Komponente', 'Lizenz', 'Kommerziell']}
rows={[
['Ollama', 'MIT', 'Ja'],
['Llama 3.1', 'Meta Llama 3', 'Ja*'],
['vLLM', 'Apache-2.0', 'Ja'],
]}
greenLastCol
/>
{/* Kontakt */}
<h2>10. Kontakt & Support</h2>
<DocTable
headers={['Kontakt', 'Adresse']}
rows={[
['Technischer Support', 'support@breakpilot.de'],
['Datenschutzbeauftragter', 'dsb@breakpilot.de'],
['Dokumentation', 'docs.breakpilot.de'],
['GitHub', 'github.com/breakpilot'],
]}
/>
{/* Footer */}
<div className="not-prose mt-8 pt-6 border-t border-slate-200 text-sm text-slate-500">
<p>Dokumentation erstellt: Januar 2026 | Version: 1.0.0</p>
</div>
</div>
</div>
)
}
/* ------------------------------------------------------------------ */
/* Helper component for repetitive doc tables */
/* ------------------------------------------------------------------ */
interface DocTableProps {
headers: string[]
rows: string[][]
monoFirstCol?: boolean
greenLastCol?: boolean
}
function DocTable({ headers, rows, monoFirstCol, greenLastCol }: DocTableProps) {
return (
<div className="not-prose overflow-x-auto">
<table className="min-w-full text-sm">
<thead className="bg-slate-50">
<tr>
{headers.map((h) => (
<th key={h} className="px-4 py-2 text-left font-semibold">{h}</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{rows.map((row, ri) => (
<tr key={ri}>
{row.map((cell, ci) => (
<td
key={ci}
className={`px-4 py-2${ci === 0 && monoFirstCol ? ' font-mono text-xs' : ''}${ci === row.length - 1 && greenLastCol ? ' text-green-600' : ''}`}
>
{cell}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
/* ------------------------------------------------------------------ */
/* ASCII diagrams kept as constants to avoid JSX noise */
/* ------------------------------------------------------------------ */
const ARCHITECTURE_DIAGRAM = `\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
\u2502 BreakPilot Alerts Frontend \u2502
\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
\u2502 \u2502 Dashboard \u2502 \u2502 Inbox \u2502 \u2502 Topics \u2502 \u2502 Profile \u2502 \u2502
\u2502 \u2502 (Stats) \u2502 \u2502 (Review) \u2502 \u2502 (Feeds) \u2502 \u2502 (Learning) \u2502 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
\u2502
v
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
\u2502 Ingestion Layer \u2502
\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
\u2502 \u2502 RSS Fetcher \u2502 \u2502 Email Parser \u2502 \u2502 APScheduler \u2502 \u2502
\u2502 \u2502 (feedparser) \u2502 \u2502 (geplant) \u2502 \u2502 (AsyncIO) \u2502 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2502 \u2502 \u2502
\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
\u2502 \u2502 Deduplication (URL-Hash + SimHash) \u2502 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
\u2502
v
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
\u2502 Processing Layer \u2502
\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
\u2502 \u2502 Rule Engine \u2502 \u2502
\u2502 \u2502 Operatoren: contains, regex, gt/lt, in, starts_with \u2502 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2502 \u2502 \u2502
\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
\u2502 \u2502 LLM Relevance Scorer \u2502 \u2502
\u2502 \u2502 Output: { score, decision: KEEP/DROP/REVIEW, summary } \u2502 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
\u2502
v
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
\u2502 Action Layer \u2502
\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
\u2502 \u2502 Email Action \u2502 \u2502 Webhook Action \u2502 \u2502 Slack Action \u2502 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
\u2502
v
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
\u2502 Storage Layer \u2502
\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502
\u2502 \u2502 PostgreSQL \u2502 \u2502 Valkey \u2502 \u2502 LLM Gateway \u2502 \u2502
\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2502
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518`
const DATAFLOW_DIAGRAM = `1. APScheduler triggert Fetch (alle 60 Min. default)
\u2502
v
2. RSS Fetcher holt Feed von Google Alerts
\u2502
v
3. Deduplizierung prueft URL-Hash
\u2502
\u251c\u2500\u2500 URL bekannt \u2500\u2500> Uebersprungen
\u2514\u2500\u2500 URL neu \u2500\u2500> Weiter
\u2502
v
4. Alert in Datenbank gespeichert (Status: NEW)
\u2502
v
5. Rule Engine evaluiert aktive Regeln
\u2502
\u251c\u2500\u2500 Regel matcht \u2500\u2500> Aktion ausfuehren
\u2514\u2500\u2500 Keine Regel \u2500\u2500> LLM Scoring
\u2502
v
6. LLM Relevance Scorer
\u2502
\u251c\u2500\u2500 KEEP (>= 0.7) \u2500\u2500> Inbox
\u251c\u2500\u2500 REVIEW (0.4-0.7) \u2500\u2500> Inbox (Pruefung)
\u2514\u2500\u2500 DROP (< 0.4) \u2500\u2500> Archiviert
\u2502
v
7. Nutzer-Feedback \u2500\u2500> Profile aktualisieren`