All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
Profil: machineBuilder-Felder im POST-Body, PATCH-Handler Scope: API-Route (GET/POST), ScopeDecisionTab Props + Buttons, Export-Druckansicht HTML Anwendung: PUT-Handler, Bearbeiten-Button, Pagination/Search Import: Verlauf laden, DELETE-Route, Offline-Badge, ObjectURL Memory-Leak fix Screening: Security-Backlog Button verdrahtet, Scan-Verlauf Module: Detail-Seite, GET-Proxy, Konfigurieren-Button, Modul-erstellen-Modal, Error-Toast Quellen: 10 Proxy-Routen, Tab-Komponenten umgestellt, Dashboard-Tab, blocked_today Bug fix, Datum-Filter Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
285 lines
12 KiB
TypeScript
285 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
|
|
interface OperationPermission {
|
|
id: string
|
|
source_id: string
|
|
operation: string
|
|
allowed: boolean
|
|
conditions?: string
|
|
}
|
|
|
|
interface SourceWithOperations {
|
|
id: string
|
|
domain: string
|
|
name: string
|
|
license?: string
|
|
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 [sourcesRes, opsRes] = await Promise.all([
|
|
fetch(`${apiBase}/sources`),
|
|
fetch(`${apiBase}/operations-matrix`),
|
|
])
|
|
if (!sourcesRes.ok || !opsRes.ok) throw new Error('Fehler beim Laden')
|
|
|
|
const sourcesData = await sourcesRes.json()
|
|
const opsData = await opsRes.json()
|
|
|
|
// Join: group operations by source_id
|
|
const opsBySource = new Map<string, OperationPermission[]>()
|
|
for (const op of opsData.operations || []) {
|
|
const list = opsBySource.get(op.source_id) || []
|
|
list.push(op)
|
|
opsBySource.set(op.source_id, list)
|
|
}
|
|
|
|
const joined: SourceWithOperations[] = (sourcesData.sources || []).map((s: any) => ({
|
|
id: s.id,
|
|
domain: s.domain,
|
|
name: s.name,
|
|
license: s.license,
|
|
active: s.active,
|
|
operations: opsBySource.get(s.id) || [],
|
|
}))
|
|
|
|
setSources(joined)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const togglePermission = async (
|
|
source: SourceWithOperations,
|
|
operationId: string
|
|
) => {
|
|
// Find the permission
|
|
const permission = source.operations.find((op) => op.operation === operationId)
|
|
if (!permission) return
|
|
|
|
// Block enabling training
|
|
if (operationId === 'training' && !permission.allowed) {
|
|
setError('Training mit externen Daten ist VERBOTEN und kann nicht aktiviert werden.')
|
|
return
|
|
}
|
|
|
|
const updateId = `${permission.id}-allowed`
|
|
setUpdating(updateId)
|
|
|
|
try {
|
|
const res = await fetch(`${apiBase}/operations/${permission.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ allowed: !permission.allowed }),
|
|
})
|
|
|
|
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.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?.allowed ?? false
|
|
const hasConditions = !!permission?.conditions
|
|
const isUpdating = updating === `${permission?.id}-allowed`
|
|
|
|
return (
|
|
<td key={op.id} className="px-4 py-3 text-center">
|
|
<div className="flex flex-col items-center gap-2">
|
|
{/* Allowed Toggle */}
|
|
<button
|
|
onClick={() => togglePermission(source, op.id)}
|
|
disabled={isTraining || isUpdating || !source.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>
|
|
|
|
{/* Conditions indicator (read-only) */}
|
|
{isAllowed && !isTraining && hasConditions && (
|
|
<span
|
|
className="px-2 py-1 text-xs rounded bg-amber-100 text-amber-700"
|
|
title={permission?.conditions || ''}
|
|
>
|
|
Bedingung
|
|
</span>
|
|
)}
|
|
</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>
|
|
)
|
|
}
|