'use client' /** * Alerts Monitoring Admin Page (migrated from website/admin/alerts) * * Google Alerts & Feed-Ueberwachung Dashboard * Provides inbox management, topic configuration, rule builder, and relevance profiles */ import { useEffect, useState, useCallback } from 'react' import { PagePurpose } from '@/components/common/PagePurpose' // 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.topics || data.items || []) } if (rulesRes.ok) { const data = await rulesRes.json() setRules(data.rules || data.items || []) } 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 }) 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' }, ] if (loading) { return (
) } return (
{/* Page Purpose */} {/* Stats Overview */}
{stats?.total_alerts || 0}
Alerts gesamt
{stats?.new_alerts || 0}
Neue Alerts
{stats?.kept_alerts || 0}
Relevant
{stats?.review_alerts || 0}
Zur Pruefung
{/* Tab Navigation */}
{/* Dashboard Tab */} {activeTab === 'dashboard' && (
{/* Quick Actions */}

Aktive Topics

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

Letzte Alerts

{alerts.slice(0, 5).map((alert) => (
{alert.title}
{alert.topic_name} {getScoreBadge(alert.relevance_score)}
))} {alerts.length === 0 && (
Keine Alerts vorhanden
)}
{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) => ( ))} {filteredAlerts.length === 0 && ( )}
Alert Topic Score Decision Zeit
{alert.title}

{alert.snippet}

{alert.topic_name} {getScoreBadge(alert.relevance_score)} {getDecisionBadge(alert.relevance_decision)} {formatTimeAgo(alert.fetched_at)}
Keine Alerts gefunden
)} {/* 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)}
))} {topics.length === 0 && (
Keine Topics konfiguriert
)}
)} {/* 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}
))} {rules.length === 0 && (
Keine Regeln konfiguriert
)}
)} {/* Profile Tab */} {activeTab === 'profile' && (

Relevanzprofil