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>
203 lines
7.3 KiB
TypeScript
203 lines
7.3 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Source Policy Management Page (SDK Version)
|
|
*
|
|
* Whitelist-based data source management for edu-search-service.
|
|
* For auditors: Full audit trail for all changes.
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import StepHeader from '@/components/sdk/StepHeader/StepHeader'
|
|
import { SourcesTab } from '@/components/sdk/source-policy/SourcesTab'
|
|
import { OperationsMatrixTab } from '@/components/sdk/source-policy/OperationsMatrixTab'
|
|
import { PIIRulesTab } from '@/components/sdk/source-policy/PIIRulesTab'
|
|
import { AuditTab } from '@/components/sdk/source-policy/AuditTab'
|
|
|
|
// API base URL for edu-search-service
|
|
const getApiBase = () => {
|
|
if (typeof window === 'undefined') return 'http://localhost:8088'
|
|
const hostname = window.location.hostname
|
|
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
return 'http://localhost:8088'
|
|
}
|
|
return `https://${hostname}:8089`
|
|
}
|
|
|
|
interface PolicyStats {
|
|
active_policies: number
|
|
allowed_sources: number
|
|
pii_rules: number
|
|
blocked_today: number
|
|
blocked_total: number
|
|
}
|
|
|
|
type TabId = 'dashboard' | 'sources' | 'operations' | 'pii' | 'audit'
|
|
|
|
export default function SourcePolicyPage() {
|
|
const { state } = useSDK()
|
|
const [activeTab, setActiveTab] = useState<TabId>('dashboard')
|
|
const [stats, setStats] = useState<PolicyStats | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [apiBase, setApiBase] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
const base = getApiBase()
|
|
setApiBase(base)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (apiBase !== null) {
|
|
fetchStats()
|
|
}
|
|
}, [apiBase])
|
|
|
|
const fetchStats = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch(`${apiBase}/v1/admin/policy-stats`)
|
|
|
|
if (!res.ok) {
|
|
throw new Error('Fehler beim Laden der Statistiken')
|
|
}
|
|
|
|
const data = await res.json()
|
|
setStats(data)
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
setStats({
|
|
active_policies: 0,
|
|
allowed_sources: 0,
|
|
pii_rules: 0,
|
|
blocked_today: 0,
|
|
blocked_total: 0,
|
|
})
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const tabs: { id: TabId; name: string; icon: JSX.Element }[] = [
|
|
{
|
|
id: 'dashboard',
|
|
name: 'Dashboard',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'sources',
|
|
name: 'Quellen',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'operations',
|
|
name: 'Operations',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
|
</svg>
|
|
),
|
|
},
|
|
{
|
|
id: 'pii',
|
|
name: 'PII-Regeln',
|
|
icon: (
|
|
<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>
|
|
),
|
|
},
|
|
{
|
|
id: 'audit',
|
|
name: 'Audit',
|
|
icon: (
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<StepHeader stepId="source-policy" showProgress={true} />
|
|
|
|
{/* 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>
|
|
)}
|
|
|
|
{/* Stats Cards */}
|
|
{stats && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-2xl font-bold text-purple-600">{stats.active_policies}</div>
|
|
<div className="text-sm text-slate-500">Aktive Policies</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-2xl font-bold text-green-600">{stats.allowed_sources}</div>
|
|
<div className="text-sm text-slate-500">Zugelassene Quellen</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-2xl font-bold text-red-600">{stats.blocked_today}</div>
|
|
<div className="text-sm text-slate-500">Blockiert (heute)</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-2xl font-bold text-blue-600">{stats.pii_rules}</div>
|
|
<div className="text-sm text-slate-500">PII-Regeln</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tabs */}
|
|
<div className="flex gap-2 flex-wrap">
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 ${
|
|
activeTab === tab.id
|
|
? 'bg-purple-600 text-white'
|
|
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
|
}`}
|
|
>
|
|
{tab.icon}
|
|
{tab.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
{apiBase === null ? (
|
|
<div className="text-center py-12 text-slate-500">Initialisiere...</div>
|
|
) : (
|
|
<>
|
|
{activeTab === 'dashboard' && (
|
|
<div className="text-center py-12 text-slate-500">
|
|
{loading ? 'Lade Dashboard...' : 'Dashboard-Ansicht - Wechseln Sie zu einem Tab fuer Details.'}
|
|
</div>
|
|
)}
|
|
{activeTab === 'sources' && <SourcesTab apiBase={apiBase} onUpdate={fetchStats} />}
|
|
{activeTab === 'operations' && <OperationsMatrixTab apiBase={apiBase} />}
|
|
{activeTab === 'pii' && <PIIRulesTab apiBase={apiBase} onUpdate={fetchStats} />}
|
|
{activeTab === 'audit' && <AuditTab apiBase={apiBase} />}
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|