This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/sdk/source-policy/OperationsMatrixTab.tsx
Benjamin Admin 70f2b0ae64 refactor: Consolidate standalone services into admin-v2, add new SDK modules
Remove standalone services (ai-compliance-sdk root, developer-portal,
dsms-gateway, dsms-node, night-scheduler) and legacy compliance/dsgvo pages.
Add new SDK pipeline modules (academy, document-crawler, dsb-portal,
incidents, whistleblower, reporting, sso, multi-tenant, industry-templates).
Add drafting engine, legal corpus files (AT/CH/DE), pitch-deck,
blog and Förderantrag pages.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 09:05:18 +01:00

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">
&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.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>
)
}