Remove duplicate compliance and DSGVO admin pages that have been superseded by the unified SDK pipeline. Update navigation, sidebar, roles, and module registry to reflect the new structure. Add DSFA corpus API proxy and source-policy components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
interface OperationPermission {
|
|
id: string
|
|
source_id: string
|
|
operation: string
|
|
is_allowed: boolean
|
|
requires_citation: boolean
|
|
notes?: string
|
|
}
|
|
|
|
interface SourceWithOperations {
|
|
id: string
|
|
domain: string
|
|
name: string
|
|
license: string
|
|
is_active: boolean
|
|
operations: OperationPermission[]
|
|
}
|
|
|
|
interface OperationsMatrixTabProps {
|
|
apiBase: string
|
|
}
|
|
|
|
const OPERATIONS = [
|
|
{ id: 'lookup', name: 'Lookup', description: 'Inhalt anzeigen/durchsuchen', icon: '🔍' },
|
|
{ id: 'rag', name: 'RAG', description: 'Retrieval Augmented Generation', icon: '🤖' },
|
|
{ id: 'training', name: 'Training', description: 'KI-Training (VERBOTEN)', icon: '🚫' },
|
|
{ id: 'export', name: 'Export', description: 'Daten exportieren', icon: '📤' },
|
|
]
|
|
|
|
export function OperationsMatrixTab({ apiBase }: OperationsMatrixTabProps) {
|
|
const [sources, setSources] = useState<SourceWithOperations[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [updating, setUpdating] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
fetchMatrix()
|
|
}, [])
|
|
|
|
const fetchMatrix = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch(`${apiBase}/v1/admin/operations-matrix`)
|
|
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 togglePermission = async (
|
|
source: SourceWithOperations,
|
|
operationId: string,
|
|
field: 'is_allowed' | 'requires_citation'
|
|
) => {
|
|
// Find the permission
|
|
const permission = source.operations.find((op) => op.operation === operationId)
|
|
if (!permission) return
|
|
|
|
// Block enabling training
|
|
if (operationId === 'training' && field === 'is_allowed' && !permission.is_allowed) {
|
|
setError('Training mit externen Daten ist VERBOTEN und kann nicht aktiviert werden.')
|
|
return
|
|
}
|
|
|
|
const updateId = `${permission.id}-${field}`
|
|
setUpdating(updateId)
|
|
|
|
try {
|
|
const newValue = !permission[field]
|
|
const res = await fetch(`${apiBase}/v1/admin/operations/${permission.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ [field]: newValue }),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
const errData = await res.json()
|
|
throw new Error(errData.message || errData.error || 'Fehler beim Aktualisieren')
|
|
}
|
|
|
|
fetchMatrix()
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setUpdating(null)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="text-center py-12 text-slate-500">Lade Operations-Matrix...</div>
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
{/* Error Display */}
|
|
{error && (
|
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
|
|
<span>{error}</span>
|
|
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">
|
|
×
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Legend */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6">
|
|
<h3 className="font-medium text-slate-900 mb-3">Legende</h3>
|
|
<div className="flex flex-wrap gap-4 text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-green-100 text-green-700 rounded">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</span>
|
|
<span className="text-slate-600">Erlaubt</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-red-100 text-red-700 rounded">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</span>
|
|
<span className="text-slate-600">Verboten</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-amber-100 text-amber-700 rounded text-xs">
|
|
Cite
|
|
</span>
|
|
<span className="text-slate-600">Zitation erforderlich</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-8 h-8 flex items-center justify-center bg-slate-800 text-white rounded">
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</span>
|
|
<span className="text-slate-600">System-gesperrt (Training)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Matrix Table */}
|
|
<div className="bg-white rounded-xl border border-slate-200 overflow-x-auto">
|
|
<table className="w-full min-w-[800px]">
|
|
<thead className="bg-slate-50 border-b border-slate-200">
|
|
<tr>
|
|
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">Quelle</th>
|
|
{OPERATIONS.map((op) => (
|
|
<th key={op.id} className="text-center px-4 py-3">
|
|
<div className="flex flex-col items-center gap-1">
|
|
<span className="text-lg">{op.icon}</span>
|
|
<span className="text-xs font-medium text-slate-500 uppercase">{op.name}</span>
|
|
<span className="text-xs text-slate-400 font-normal">{op.description}</span>
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-100">
|
|
{sources.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">
|
|
Keine Quellen vorhanden
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
sources.map((source) => (
|
|
<tr key={source.id} className={`hover:bg-slate-50 ${!source.is_active ? 'opacity-50' : ''}`}>
|
|
<td className="px-4 py-3">
|
|
<div>
|
|
<div className="font-medium text-slate-800">{source.name}</div>
|
|
<code className="text-xs text-slate-500">{source.domain}</code>
|
|
</div>
|
|
</td>
|
|
{OPERATIONS.map((op) => {
|
|
const permission = source.operations.find((p) => p.operation === op.id)
|
|
const isTraining = op.id === 'training'
|
|
const isAllowed = permission?.is_allowed ?? false
|
|
const requiresCitation = permission?.requires_citation ?? false
|
|
const isUpdating = updating === `${permission?.id}-is_allowed` || updating === `${permission?.id}-requires_citation`
|
|
|
|
return (
|
|
<td key={op.id} className="px-4 py-3 text-center">
|
|
<div className="flex flex-col items-center gap-2">
|
|
{/* Is Allowed Toggle */}
|
|
<button
|
|
onClick={() => togglePermission(source, op.id, 'is_allowed')}
|
|
disabled={isTraining || isUpdating || !source.is_active}
|
|
className={`w-10 h-10 flex items-center justify-center rounded transition-colors ${
|
|
isTraining
|
|
? 'bg-slate-800 text-white cursor-not-allowed'
|
|
: isAllowed
|
|
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
|
: 'bg-red-100 text-red-700 hover:bg-red-200'
|
|
} ${isUpdating ? 'opacity-50' : ''}`}
|
|
title={isTraining ? 'Training ist system-weit gesperrt' : isAllowed ? 'Klicken zum Deaktivieren' : 'Klicken zum Aktivieren'}
|
|
>
|
|
{isTraining ? (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
) : isAllowed ? (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
) : (
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
|
|
{/* Citation Required Toggle (only for allowed non-training ops) */}
|
|
{isAllowed && !isTraining && (
|
|
<button
|
|
onClick={() => togglePermission(source, op.id, 'requires_citation')}
|
|
disabled={isUpdating || !source.is_active}
|
|
className={`px-2 py-1 text-xs rounded transition-colors ${
|
|
requiresCitation
|
|
? 'bg-amber-100 text-amber-700 hover:bg-amber-200'
|
|
: 'bg-slate-100 text-slate-500 hover:bg-slate-200'
|
|
} ${isUpdating ? 'opacity-50' : ''}`}
|
|
title={requiresCitation ? 'Zitation erforderlich - Klicken zum Aendern' : 'Klicken um Zitation zu erfordern'}
|
|
>
|
|
{requiresCitation ? 'Cite ✓' : 'Cite'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Training Warning */}
|
|
<div className="mt-6 bg-red-50 border border-red-200 rounded-xl p-6">
|
|
<div className="flex items-start gap-4">
|
|
<div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center flex-shrink-0">
|
|
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-red-800">Training-Operation: System-gesperrt</h3>
|
|
<p className="text-sm text-red-700 mt-1">
|
|
Das Training von KI-Modellen mit gecrawlten externen Daten ist aufgrund von Urheberrechts- und
|
|
Datenschutzbestimmungen grundsaetzlich verboten. Diese Einschraenkung ist im System hart kodiert
|
|
und kann nicht ueber diese Oberflaeche geaendert werden.
|
|
</p>
|
|
<p className="text-sm text-red-600 mt-2 font-medium">
|
|
Ausnahmen erfordern eine schriftliche Genehmigung des DSB und eine rechtliche Pruefung.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|