Files
Benjamin Admin d079886819
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
feat: 7 Vorbereitungs-Module auf 100% — Frontend, Proxy-Routen, Backend-Fixes
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>
2026-03-02 15:08:13 +01:00

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">
&times;
</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>
)
}