'use client' /** * Alerts Monitoring Admin Page * * Google Alerts & Feed-Ueberwachung Dashboard * Provides inbox management, topic configuration, rule builder, and relevance profiles */ import AdminLayout from '@/components/admin/AdminLayout' import SystemInfoSection from '@/components/admin/SystemInfoSection' import { useEffect, useState, useCallback } from 'react' // Types interface AlertItem { id: string title: string url: string snippet: string topic_name: string relevance_score: number | null relevance_decision: string | null status: string fetched_at: string published_at: string | null matched_rule: string | null tags: string[] } interface Topic { id: string name: string feed_url: string feed_type: string is_active: boolean fetch_interval_minutes: number last_fetched_at: string | null alert_count: number } interface Rule { id: string name: string topic_id: string | null conditions: Array<{ field: string operator: string value: string | number }> action_type: string action_config: Record priority: number is_active: boolean } interface Profile { priorities: string[] exclusions: string[] positive_examples: Array<{ title: string; url: string }> negative_examples: Array<{ title: string; url: string }> policies: { keep_threshold: number drop_threshold: number } } interface Stats { total_alerts: number new_alerts: number kept_alerts: number review_alerts: number dropped_alerts: number total_topics: number active_topics: number total_rules: number } // Tab type type TabId = 'dashboard' | 'inbox' | 'topics' | 'rules' | 'profile' | 'audit' | 'documentation' export default function AlertsPage() { const [activeTab, setActiveTab] = useState('dashboard') const [stats, setStats] = useState(null) const [alerts, setAlerts] = useState([]) const [topics, setTopics] = useState([]) const [rules, setRules] = useState([]) const [profile, setProfile] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [inboxFilter, setInboxFilter] = useState('all') const API_BASE = '/api/alerts' const fetchData = useCallback(async () => { try { const [statsRes, alertsRes, topicsRes, rulesRes, profileRes] = await Promise.all([ fetch(`${API_BASE}/stats`), fetch(`${API_BASE}/inbox?limit=50`), fetch(`${API_BASE}/topics`), fetch(`${API_BASE}/rules`), fetch(`${API_BASE}/profile`), ]) if (statsRes.ok) setStats(await statsRes.json()) if (alertsRes.ok) { const data = await alertsRes.json() setAlerts(data.items || []) } if (topicsRes.ok) { const data = await topicsRes.json() setTopics(data.items || data || []) } if (rulesRes.ok) { const data = await rulesRes.json() setRules(data.items || data || []) } if (profileRes.ok) setProfile(await profileRes.json()) setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'Verbindungsfehler') // Set demo data setStats({ total_alerts: 147, new_alerts: 23, kept_alerts: 89, review_alerts: 12, dropped_alerts: 23, total_topics: 5, active_topics: 4, total_rules: 8, }) setAlerts([ { id: 'demo_1', title: 'Neue Studie zur digitalen Bildung an Schulen', url: 'https://example.com/artikel1', snippet: 'Eine aktuelle Studie zeigt, dass digitale Lernmittel den Lernerfolg steigern koennen...', topic_name: 'Digitale Bildung', relevance_score: 0.85, relevance_decision: 'KEEP', status: 'new', fetched_at: new Date().toISOString(), published_at: null, matched_rule: null, tags: ['bildung', 'digital'], }, { id: 'demo_2', title: 'Inklusion: Fortbildungen fuer Lehrkraefte', url: 'https://example.com/artikel2', snippet: 'Das Kultusministerium bietet neue Fortbildungsangebote zum Thema Inklusion an...', topic_name: 'Inklusion', relevance_score: 0.72, relevance_decision: 'KEEP', status: 'new', fetched_at: new Date(Date.now() - 3600000).toISOString(), published_at: null, matched_rule: null, tags: ['inklusion'], }, ]) setTopics([ { id: 'topic_1', name: 'Digitale Bildung', feed_url: 'https://google.com/alerts/feeds/123', feed_type: 'rss', is_active: true, fetch_interval_minutes: 60, last_fetched_at: new Date().toISOString(), alert_count: 47, }, { id: 'topic_2', name: 'Inklusion', feed_url: 'https://google.com/alerts/feeds/456', feed_type: 'rss', is_active: true, fetch_interval_minutes: 60, last_fetched_at: new Date(Date.now() - 1800000).toISOString(), alert_count: 32, }, ]) setRules([ { id: 'rule_1', name: 'Stellenanzeigen ausschliessen', topic_id: null, conditions: [{ field: 'title', operator: 'contains', value: 'Stellenangebot' }], action_type: 'drop', action_config: {}, priority: 10, is_active: true, }, ]) setProfile({ priorities: ['Inklusion', 'digitale Bildung'], exclusions: ['Stellenanzeigen', 'Werbung'], positive_examples: [], negative_examples: [], policies: { keep_threshold: 0.7, drop_threshold: 0.3 }, }) } finally { setLoading(false) } }, []) useEffect(() => { fetchData() }, [fetchData]) const formatTimeAgo = (dateStr: string | null) => { if (!dateStr) return '-' const date = new Date(dateStr) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) if (diffMins < 1) return 'gerade eben' if (diffMins < 60) return `vor ${diffMins} Min.` if (diffMins < 1440) return `vor ${Math.floor(diffMins / 60)} Std.` return `vor ${Math.floor(diffMins / 1440)} Tagen` } const getScoreBadge = (score: number | null) => { if (score === null) return null const pct = Math.round(score * 100) let cls = 'bg-slate-100 text-slate-600' if (pct >= 70) cls = 'bg-green-100 text-green-800' else if (pct >= 40) cls = 'bg-amber-100 text-amber-800' else cls = 'bg-red-100 text-red-800' return {pct}% } const getDecisionBadge = (decision: string | null) => { if (!decision) return null const styles: Record = { KEEP: 'bg-green-100 text-green-800', REVIEW: 'bg-amber-100 text-amber-800', DROP: 'bg-red-100 text-red-800', } return ( {decision} ) } const filteredAlerts = alerts.filter((alert) => { if (inboxFilter === 'all') return true if (inboxFilter === 'new') return alert.status === 'new' if (inboxFilter === 'keep') return alert.relevance_decision === 'KEEP' if (inboxFilter === 'review') return alert.relevance_decision === 'REVIEW' return true }) // System Info configuration for Audit/Documentation tabs const alertsSystemConfig = { title: 'Alerts Agent System', description: 'Google Alerts & Feed-Ueberwachung mit KI-gestuetzter Relevanzpruefung', version: '1.0.0', architecture: { layers: [ { title: 'Ingestion Layer', components: ['RSS Fetcher', 'Email Parser', 'Webhook Receiver', 'APScheduler'], color: '#3b82f6', }, { title: 'Processing Layer', components: ['Deduplication', 'Rule Engine', 'LLM Relevance Scorer'], color: '#8b5cf6', }, { title: 'Action Layer', components: ['Email Actions', 'Webhook Actions', 'Slack Actions'], color: '#22c55e', }, { title: 'Storage Layer', components: ['PostgreSQL', 'Valkey Cache'], color: '#f59e0b', }, ], }, features: [ { name: 'RSS Feed Parsing', status: 'active' as const, description: 'Google Alerts und andere RSS/Atom Feeds' }, { name: 'LLM Relevance Scoring', status: 'active' as const, description: 'KI-basierte Relevanzpruefung mit Few-Shot Learning' }, { name: 'Rule Engine', status: 'active' as const, description: 'Regelbasierte Filterung mit Conditions' }, { name: 'Email Actions', status: 'active' as const, description: 'E-Mail-Benachrichtigungen bei Matches' }, { name: 'Webhook Actions', status: 'active' as const, description: 'HTTP Webhooks fuer Integrationen' }, { name: 'Slack Actions', status: 'active' as const, description: 'Slack Block Kit Nachrichten' }, { name: 'Email Parsing', status: 'planned' as const, description: 'Google Alerts per E-Mail empfangen' }, { name: 'Microsoft Teams', status: 'planned' as const, description: 'Teams Adaptive Cards' }, ], roadmap: [ { phase: 'Phase 1 (Completed)', priority: 'high' as const, items: ['PostgreSQL Persistenz', 'Repository Pattern', 'Alembic Migrations'], }, { phase: 'Phase 2 (Completed)', priority: 'high' as const, items: ['Topic CRUD API', 'APScheduler Integration', 'Email Parser'], }, { phase: 'Phase 3 (Completed)', priority: 'medium' as const, items: ['Rule Engine', 'Condition Operators', 'Rule API'], }, { phase: 'Phase 4 (Completed)', priority: 'medium' as const, items: ['Action Dispatcher', 'Email/Webhook/Slack Actions'], }, { phase: 'Phase 5 (Current)', priority: 'high' as const, items: ['Studio Frontend', 'Admin Frontend', 'Audit & Documentation'], }, ], technicalDetails: [ { component: 'Backend', technology: 'FastAPI', version: '0.100+', description: 'Async REST API' }, { component: 'ORM', technology: 'SQLAlchemy', version: '2.0', description: 'Async ORM mit PostgreSQL' }, { component: 'Scheduler', technology: 'APScheduler', version: '3.x', description: 'AsyncIO Scheduler' }, { component: 'HTTP Client', technology: 'httpx', description: 'Async HTTP fuer Webhooks' }, { component: 'Feed Parser', technology: 'feedparser', version: '6.x', description: 'RSS/Atom Parsing' }, { component: 'LLM Gateway', technology: 'Ollama/vLLM/Claude', description: 'Multi-Provider LLM' }, ], privacyNotes: [ 'Alle Daten werden in Deutschland gespeichert (PostgreSQL)', 'Keine personenbezogenen Daten in Alerts (nur URLs und Snippets)', 'LLM-Verarbeitung kann on-premise mit Ollama/vLLM erfolgen', 'DSGVO-konforme Datenverarbeitung', ], auditInfo: [ { category: 'Datenbank', items: [ { label: 'Tabellen', value: '4 (topics, items, rules, profiles)', status: 'ok' as const }, { label: 'Indizes', value: 'URL-Hash, Topic-ID, Status', status: 'ok' as const }, { label: 'Backups', value: 'PostgreSQL pg_dump', status: 'ok' as const }, ], }, { category: 'API Sicherheit', items: [ { label: 'Authentifizierung', value: 'Bearer Token (geplant)', status: 'warning' as const }, { label: 'Rate Limiting', value: 'Nicht implementiert', status: 'warning' as const }, { label: 'Input Validation', value: 'Pydantic Models', status: 'ok' as const }, ], }, { category: 'Logging & Monitoring', items: [ { label: 'Structured Logging', value: 'Python logging', status: 'ok' as const }, { label: 'Metriken', value: 'Stats Endpoint', status: 'ok' as const }, { label: 'Health Checks', value: '/api/alerts/health', status: 'ok' as const }, ], }, ], fullDocumentation: `

Alerts Agent - Entwicklerdokumentation

API Endpoints

  • GET /api/alerts/inbox - Alerts auflisten
  • POST /api/alerts/ingest - Alert hinzufuegen
  • GET /api/alerts/topics - Topics auflisten
  • POST /api/alerts/topics - Topic erstellen
  • GET /api/alerts/rules - Regeln auflisten
  • POST /api/alerts/rules - Regel erstellen
  • GET /api/alerts/profile - Profil abrufen
  • PUT /api/alerts/profile - Profil aktualisieren

Architektur

Der Alerts Agent verwendet ein Pipeline-basiertes Design:

  1. Ingestion: RSS Feeds werden periodisch abgerufen
  2. Deduplication: SimHash-basierte Duplikaterkennung
  3. Scoring: LLM-basierte Relevanzpruefung
  4. Rules: Regelbasierte Filterung und Aktionen
  5. Actions: Email/Webhook/Slack Benachrichtigungen
`, } const tabs: { id: TabId; label: string; badge?: number }[] = [ { id: 'dashboard', label: 'Dashboard' }, { id: 'inbox', label: 'Inbox', badge: stats?.new_alerts || 0 }, { id: 'topics', label: 'Topics' }, { id: 'rules', label: 'Regeln' }, { id: 'profile', label: 'Profil' }, { id: 'audit', label: 'Audit' }, { id: 'documentation', label: 'Dokumentation' }, ] return ( {/* Tab Navigation */}
{/* Dashboard Tab */} {activeTab === 'dashboard' && (
{/* Stats Grid */}
{stats?.total_alerts || 0}
Alerts gesamt
{stats?.new_alerts || 0}
Neue Alerts
{stats?.kept_alerts || 0}
Relevant
{stats?.review_alerts || 0}
Zur Pruefung
{/* Quick Actions */}

Aktive Topics

{topics.slice(0, 5).map((topic) => (
{topic.name}
{topic.alert_count} Alerts
{topic.is_active ? 'Aktiv' : 'Pausiert'}
))}

Letzte Alerts

{alerts.slice(0, 5).map((alert) => (
{alert.title}
{alert.topic_name} {getScoreBadge(alert.relevance_score)}
))}
{error && (

Hinweis: API nicht erreichbar. Demo-Daten werden angezeigt.

)}
)} {/* Inbox Tab */} {activeTab === 'inbox' && (
{/* Filters */}
{['all', 'new', 'keep', 'review'].map((filter) => ( ))}
{/* Alerts Table */}
{filteredAlerts.map((alert) => ( ))}
Alert Topic Score Decision Zeit
{alert.title}

{alert.snippet}

{alert.topic_name} {getScoreBadge(alert.relevance_score)} {getDecisionBadge(alert.relevance_decision)} {formatTimeAgo(alert.fetched_at)}
)} {/* Topics Tab */} {activeTab === 'topics' && (

Feed Topics

{topics.map((topic) => (
{topic.is_active ? 'Aktiv' : 'Pausiert'}

{topic.name}

{topic.feed_url}

{topic.alert_count} Alerts
{formatTimeAgo(topic.last_fetched_at)}
))}
)} {/* Rules Tab */} {activeTab === 'rules' && (

Filterregeln

{rules.map((rule) => (
{rule.name}
Wenn: {rule.conditions[0]?.field} {rule.conditions[0]?.operator} "{rule.conditions[0]?.value}"
{rule.action_type}
))}
)} {/* Profile Tab */} {activeTab === 'profile' && (

Relevanzprofil