'use client' import { useState, useEffect } from 'react' interface AllowedSource { id: string policy_id: string domain: string name: string license: string legal_basis?: string citation_template?: string trust_boost: number is_active: boolean created_at: string updated_at: string } interface SourcesTabProps { apiBase: string onUpdate?: () => void } const LICENSES = [ { value: 'DL-DE-BY-2.0', label: 'Datenlizenz Deutschland' }, { value: 'CC-BY', label: 'Creative Commons BY' }, { value: 'CC-BY-SA', label: 'Creative Commons BY-SA' }, { value: 'CC0', label: 'Public Domain' }, { value: '§5 UrhG', label: 'Amtliche Werke (§5 UrhG)' }, ] const BUNDESLAENDER = [ { value: '', label: 'Bundesebene' }, { value: 'NI', label: 'Niedersachsen' }, { value: 'BY', label: 'Bayern' }, { value: 'BW', label: 'Baden-Wuerttemberg' }, { value: 'NW', label: 'Nordrhein-Westfalen' }, { value: 'HE', label: 'Hessen' }, { value: 'SN', label: 'Sachsen' }, { value: 'BE', label: 'Berlin' }, { value: 'HH', label: 'Hamburg' }, ] export function SourcesTab({ apiBase, onUpdate }: SourcesTabProps) { const [sources, setSources] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Filters const [searchTerm, setSearchTerm] = useState('') const [licenseFilter, setLicenseFilter] = useState('') const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all') // Edit modal const [editingSource, setEditingSource] = useState(null) const [isNewSource, setIsNewSource] = useState(false) const [saving, setSaving] = useState(false) // New source form const [newSource, setNewSource] = useState({ domain: '', name: '', license: 'DL-DE-BY-2.0', legal_basis: '', citation_template: '', trust_boost: 0.5, is_active: true, policy_id: '', // Will be set from policies }) useEffect(() => { fetchSources() }, [licenseFilter, statusFilter]) const fetchSources = async () => { try { setLoading(true) const params = new URLSearchParams() if (licenseFilter) params.append('license', licenseFilter) if (statusFilter !== 'all') params.append('active_only', statusFilter === 'active' ? 'true' : 'false') const res = await fetch(`${apiBase}/v1/admin/sources?${params}`) if (!res.ok) throw new Error('Fehler beim Laden') const data = await res.json() setSources(data.sources || []) } catch (err) { setError(err instanceof Error ? err.message : 'Unbekannter Fehler') } finally { setLoading(false) } } const createSource = async () => { try { setSaving(true) const res = await fetch(`${apiBase}/v1/admin/sources`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newSource), }) if (!res.ok) throw new Error('Fehler beim Erstellen') setNewSource({ domain: '', name: '', license: 'DL-DE-BY-2.0', legal_basis: '', citation_template: '', trust_boost: 0.5, is_active: true, policy_id: '', }) setIsNewSource(false) fetchSources() onUpdate?.() } catch (err) { setError(err instanceof Error ? err.message : 'Unbekannter Fehler') } finally { setSaving(false) } } const updateSource = async () => { if (!editingSource) return try { setSaving(true) const res = await fetch(`${apiBase}/v1/admin/sources/${editingSource.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(editingSource), }) if (!res.ok) throw new Error('Fehler beim Aktualisieren') setEditingSource(null) fetchSources() onUpdate?.() } catch (err) { setError(err instanceof Error ? err.message : 'Unbekannter Fehler') } finally { setSaving(false) } } const deleteSource = async (id: string) => { if (!confirm('Quelle wirklich loeschen? Diese Aktion wird im Audit-Log protokolliert.')) return try { const res = await fetch(`${apiBase}/v1/admin/sources/${id}`, { method: 'DELETE', }) if (!res.ok) throw new Error('Fehler beim Loeschen') fetchSources() onUpdate?.() } catch (err) { setError(err instanceof Error ? err.message : 'Unbekannter Fehler') } } const toggleSourceStatus = async (source: AllowedSource) => { try { const res = await fetch(`${apiBase}/v1/admin/sources/${source.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_active: !source.is_active }), }) if (!res.ok) throw new Error('Fehler beim Aendern des Status') fetchSources() onUpdate?.() } catch (err) { setError(err instanceof Error ? err.message : 'Unbekannter Fehler') } } const filteredSources = sources.filter((source) => { if (searchTerm) { const term = searchTerm.toLowerCase() if (!source.domain.toLowerCase().includes(term) && !source.name.toLowerCase().includes(term)) { return false } } return true }) return (
{/* Error Display */} {error && (
{error}
)} {/* Filters & Actions */}
setSearchTerm(e.target.value)} placeholder="Domain oder Name suchen..." className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
{/* Sources Table */} {loading ? (
Lade Quellen...
) : filteredSources.length === 0 ? (

Keine Quellen gefunden

Fuegen Sie neue Quellen zur Whitelist hinzu.

) : (
{filteredSources.map((source) => ( ))}
Domain Name Lizenz Trust Status Aktionen
{source.domain} {source.name} {source.license} {(source.trust_boost * 100).toFixed(0)}%
)} {/* New Source Modal */} {isNewSource && (

Neue Quelle hinzufuegen

setNewSource({ ...newSource, domain: e.target.value })} placeholder="z.B. nibis.de" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
setNewSource({ ...newSource, name: e.target.value })} placeholder="z.B. NiBiS Bildungsserver" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
setNewSource({ ...newSource, legal_basis: e.target.value })} placeholder="z.B. §5 UrhG (Amtliche Werke)" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
setNewSource({ ...newSource, trust_boost: parseFloat(e.target.value) })} className="w-full" />
{(newSource.trust_boost * 100).toFixed(0)}%
)} {/* Edit Source Modal */} {editingSource && (

Quelle bearbeiten

setEditingSource({ ...editingSource, name: e.target.value })} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
setEditingSource({ ...editingSource, legal_basis: e.target.value })} className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
setEditingSource({ ...editingSource, citation_template: e.target.value })} placeholder="Quelle: {source}, {title}, {date}" className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
setEditingSource({ ...editingSource, trust_boost: parseFloat(e.target.value) })} className="w-full" />
{(editingSource.trust_boost * 100).toFixed(0)}%
setEditingSource({ ...editingSource, is_active: e.target.checked })} className="w-4 h-4 text-purple-600" />
)}
) }