The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
805 lines
42 KiB
TypeScript
805 lines
42 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Source Policy Management Page
|
|
*
|
|
* Whitelist-based data source management for edu-search-service.
|
|
* For auditors: Full audit trail for all changes.
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
import { SourcesTab } from './components/SourcesTab'
|
|
import { OperationsMatrixTab } from './components/OperationsMatrixTab'
|
|
import { PIIRulesTab } from './components/PIIRulesTab'
|
|
import { AuditTab } from './components/AuditTab'
|
|
|
|
// API base URL for edu-search-service
|
|
// Uses nginx HTTPS proxy on port 8089 when accessed remotely
|
|
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'
|
|
}
|
|
// Use nginx HTTPS proxy on port 8089 (proxies to edu-search-service: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 [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(() => {
|
|
// Set API base on client side - only runs in browser
|
|
const base = getApiBase()
|
|
setApiBase(base)
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
// Only fetch when apiBase has been set by the first 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')
|
|
// Set default stats on error
|
|
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>
|
|
{/* Page Purpose */}
|
|
<PagePurpose
|
|
title="Quellen-Policy"
|
|
purpose="Whitelist-basiertes Datenquellen-Management fuer das Bildungssuch-System. Nur offizielle Open-Data-Portale und amtliche Quellen (§5 UrhG). Training mit externen Daten ist VERBOTEN. Fuer Auditoren pruefbar mit vollstaendigem Audit-Trail."
|
|
audience={['DSB', 'Compliance Officer', 'Auditor']}
|
|
gdprArticles={[
|
|
'Art. 5 (Rechtmaessigkeit)',
|
|
'Art. 6 (Rechtsgrundlage)',
|
|
'Art. 24 (Verantwortung)',
|
|
]}
|
|
architecture={{
|
|
services: ['edu-search-service (Go)', 'PostgreSQL'],
|
|
databases: ['source_policies', 'allowed_sources', 'pii_rules', 'policy_audit_log'],
|
|
}}
|
|
relatedPages={[
|
|
{ name: 'Audit Report', href: '/compliance/audit-report', description: 'Compliance-Berichte' },
|
|
{ name: 'Controls', href: '/compliance/controls', description: 'Technische Kontrollen' },
|
|
{ name: 'Education Search', href: '/education/edu-search', description: 'Bildungsquellen' },
|
|
]}
|
|
collapsible={true}
|
|
defaultCollapsed={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 mb-6">
|
|
<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 mb-6 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' && (
|
|
<DashboardTab stats={stats} loading={loading} apiBase={apiBase} />
|
|
)}
|
|
|
|
{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>
|
|
)
|
|
}
|
|
|
|
// Dashboard Tab Component
|
|
function DashboardTab({
|
|
stats,
|
|
loading,
|
|
apiBase,
|
|
}: {
|
|
stats: PolicyStats | null
|
|
loading: boolean
|
|
apiBase: string
|
|
}) {
|
|
const [complianceCheck, setComplianceCheck] = useState({
|
|
url: '',
|
|
operation: 'lookup',
|
|
})
|
|
const [checkResult, setCheckResult] = useState<any>(null)
|
|
const [checking, setChecking] = useState(false)
|
|
|
|
const runComplianceCheck = async () => {
|
|
if (!complianceCheck.url) return
|
|
|
|
try {
|
|
setChecking(true)
|
|
const res = await fetch(`${apiBase}/v1/admin/check-compliance`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(complianceCheck),
|
|
})
|
|
|
|
const data = await res.json()
|
|
setCheckResult(data)
|
|
} catch (err) {
|
|
setCheckResult({ error: 'Fehler bei der Pruefung' })
|
|
} finally {
|
|
setChecking(false)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="text-center py-12 text-slate-500">Lade Dashboard...</div>
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Important Notice */}
|
|
<div className="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 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 className="font-semibold text-red-800">Training mit externen Daten: VERBOTEN</h3>
|
|
<p className="text-sm text-red-700 mt-1">
|
|
Gemaess unserer Datenschutz-Policy ist das Training von KI-Modellen mit gecrawlten Daten
|
|
strengstens untersagt. Diese Einschraenkung kann nicht ueber die UI geaendert werden.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Compliance Check */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Schnell-Pruefung</h3>
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Pruefen Sie, ob eine URL in der Whitelist enthalten ist und welche Operationen erlaubt sind.
|
|
</p>
|
|
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
<input
|
|
type="url"
|
|
value={complianceCheck.url}
|
|
onChange={(e) => setComplianceCheck({ ...complianceCheck, url: e.target.value })}
|
|
placeholder="https://nibis.de/beispiel-seite"
|
|
className="flex-1 px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
/>
|
|
<select
|
|
value={complianceCheck.operation}
|
|
onChange={(e) => setComplianceCheck({ ...complianceCheck, operation: e.target.value })}
|
|
className="px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
>
|
|
<option value="lookup">Lookup (Anzeigen)</option>
|
|
<option value="rag">RAG (Retrieval)</option>
|
|
<option value="export">Export</option>
|
|
<option value="training">Training</option>
|
|
</select>
|
|
<button
|
|
onClick={runComplianceCheck}
|
|
disabled={checking || !complianceCheck.url}
|
|
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
{checking ? 'Pruefe...' : 'Pruefen'}
|
|
</button>
|
|
</div>
|
|
|
|
{checkResult && (
|
|
<div className={`mt-4 p-4 rounded-lg ${checkResult.is_allowed ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
{checkResult.is_allowed ? (
|
|
<>
|
|
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span className="font-medium text-green-800">Erlaubt</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<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="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
<span className="font-medium text-red-800">
|
|
Blockiert: {checkResult.block_reason || 'Nicht in Whitelist'}
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
{checkResult.source && (
|
|
<div className="text-sm text-slate-600">
|
|
<p><strong>Quelle:</strong> {checkResult.source.name}</p>
|
|
<p><strong>Lizenz:</strong> {checkResult.license}</p>
|
|
{checkResult.requires_citation && (
|
|
<p className="text-amber-600">Zitation erforderlich</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Operations Matrix by Source Type */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Zulaessige Operationen nach Quellentyp</h3>
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Uebersicht welche Operationen fuer welche Datenquellen erlaubt sind.
|
|
</p>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-slate-200 bg-slate-50">
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Quelle / Typ</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Daten</th>
|
|
<th className="text-center py-2 px-2 font-medium text-slate-700">Lookup</th>
|
|
<th className="text-center py-2 px-2 font-medium text-slate-700">RAG</th>
|
|
<th className="text-center py-2 px-2 font-medium text-slate-700">Training</th>
|
|
<th className="text-center py-2 px-2 font-medium text-slate-700">Export</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Rechtsgrundlage</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Auflagen / Controls</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{[
|
|
{ source: 'Landes-Open-Data-Portale (alle Laender)', data: 'SMD', lookup: 'yes', rag: 'yes', training: 'no', export: 'warn', basis: 'DL-DE-BY-2.0', note: 'Namensnennung, Quellenlink, Zweckbindung' },
|
|
{ source: 'Landes-Open-Data-Portale', data: 'PBD', lookup: 'no', rag: 'no', training: 'no', export: 'no', basis: '—', note: 'Technisch filtern (Schema-Block)' },
|
|
{ source: 'Regelwerke / Schulordnungen (Ministerien)', data: 'DOK', lookup: 'yes', rag: 'yes', training: 'warn', export: 'warn', basis: 'UrhG §5 / CC / DL', note: 'Nur amtliche Texte, Versions-Hash' },
|
|
{ source: 'GovData', data: 'SMD', lookup: 'yes', rag: 'yes', training: 'no', export: 'warn', basis: 'DL-DE-BY-2.0', note: 'Bundesweiter Fallback' },
|
|
{ source: 'Einzelschul-Websites', data: 'SMD', lookup: 'warn', rag: 'no', training: 'no', export: 'no', basis: '§60d greift nicht', note: 'Nur manuell, kein Crawling' },
|
|
{ source: 'Private Schulverzeichnisse', data: 'SMD', lookup: 'no', rag: 'no', training: 'no', export: 'no', basis: 'Datenbankrecht', note: 'Nicht zulaessig' },
|
|
{ source: 'Vom Lehrer eingegebene Daten', data: 'SMD', lookup: 'yes', rag: 'yes', training: 'warn', export: 'warn', basis: 'Art. 6(1)b DSGVO', note: 'Zweckbindung, Namespace' },
|
|
{ source: 'Vom Lehrer hochgeladene Dokumente', data: 'DOK', lookup: 'yes', rag: 'yes', training: 'no', export: 'no', basis: 'Art. 6(1)b DSGVO', note: 'Kein Training, nur Session-RAG' },
|
|
].map((row, idx) => (
|
|
<tr key={idx} className={`border-b border-slate-100 hover:bg-slate-50 ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/30'}`}>
|
|
<td className="py-2 px-3 font-medium text-slate-800 text-xs">{row.source}</td>
|
|
<td className="py-2 px-3">
|
|
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
|
row.data === 'SMD' ? 'bg-blue-100 text-blue-700' : row.data === 'PBD' ? 'bg-red-100 text-red-700' : 'bg-purple-100 text-purple-700'
|
|
}`}>{row.data}</span>
|
|
</td>
|
|
<td className="py-2 px-2 text-center">{row.lookup === 'yes' ? '✅' : row.lookup === 'warn' ? '⚠️' : '❌'}</td>
|
|
<td className="py-2 px-2 text-center">{row.rag === 'yes' ? '✅' : row.rag === 'warn' ? '⚠️' : '❌'}</td>
|
|
<td className="py-2 px-2 text-center">{row.training === 'yes' ? '✅' : row.training === 'warn' ? '⚠️' : '❌'}</td>
|
|
<td className="py-2 px-2 text-center">{row.export === 'yes' ? '✅' : row.export === 'warn' ? '⚠️' : '❌'}</td>
|
|
<td className="py-2 px-3 text-xs">
|
|
<span className={`px-1.5 py-0.5 rounded ${
|
|
row.basis === '—' ? 'bg-slate-100 text-slate-500' :
|
|
row.basis.includes('DSGVO') ? 'bg-blue-100 text-blue-700' :
|
|
row.basis.includes('DL-DE') ? 'bg-green-100 text-green-700' :
|
|
row.basis.includes('UrhG') || row.basis.includes('CC') ? 'bg-amber-100 text-amber-700' :
|
|
'bg-red-100 text-red-700'
|
|
}`}>{row.basis}</span>
|
|
</td>
|
|
<td className="py-2 px-3 text-slate-600 text-xs">{row.note}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/* Legend and Explanation */}
|
|
<div className="mt-4 p-4 bg-slate-50 rounded-lg">
|
|
<h4 className="font-medium text-slate-800 mb-3">Geltungsbereich der Matrix</h4>
|
|
|
|
{/* Datenarten */}
|
|
<div className="mb-4">
|
|
<div className="text-sm font-medium text-slate-700 mb-2">Datenarten</div>
|
|
<div className="flex flex-wrap gap-3 text-xs">
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded">SMD</span>
|
|
<span className="text-slate-600">= Schul-Metadaten (Name, Nummer, Schulform, Ort, Traeger)</span>
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="px-1.5 py-0.5 bg-red-100 text-red-700 rounded">PBD</span>
|
|
<span className="text-slate-600">= Personenbezogene Daten (Leitung, E-Mail, Telefon)</span>
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="px-1.5 py-0.5 bg-purple-100 text-purple-700 rounded">DOK</span>
|
|
<span className="text-slate-600">= Regelwerke / Ordnungen / Lehrplaene</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Verarbeitungsarten mit aufklappbarer Erklärung */}
|
|
<details className="group">
|
|
<summary className="cursor-pointer text-sm font-medium text-slate-700 mb-2 flex items-center gap-2 hover:text-purple-600">
|
|
<svg className="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
Verarbeitungsarten (Details anzeigen)
|
|
</summary>
|
|
<div className="ml-6 mt-2 space-y-3 text-sm">
|
|
<div className="p-3 bg-white rounded border border-slate-200">
|
|
<div className="font-medium text-slate-800 flex items-center gap-2">
|
|
<span className="text-green-600">Lookup</span>
|
|
<span className="text-slate-400">=</span>
|
|
<span className="text-slate-600">Auswahl / Validierung / Anzeige</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
Daten werden abgerufen und dem Nutzer angezeigt, z.B. bei der Schulauswahl im Onboarding
|
|
oder zur Validierung eingegebener Schulnummern. Keine dauerhafte Speicherung oder Weiterverarbeitung.
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-white rounded border border-slate-200">
|
|
<div className="font-medium text-slate-800 flex items-center gap-2">
|
|
<span className="text-blue-600">RAG</span>
|
|
<span className="text-slate-400">=</span>
|
|
<span className="text-slate-600">Retrieval-Index (Kontext, Zitierquelle)</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
Daten werden in einen Vektor-Index aufgenommen und koennen als Kontext fuer KI-Antworten
|
|
herangezogen werden. Die Quelle wird zitiert. Keine Veraenderung der Modelgewichte.
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-white rounded border border-slate-200">
|
|
<div className="font-medium text-slate-800 flex items-center gap-2">
|
|
<span className="text-red-600">Training</span>
|
|
<span className="text-slate-400">=</span>
|
|
<span className="text-slate-600">Modellanpassung / Fine-Tuning</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
Daten fliessen in das Training oder Fine-Tuning eines KI-Modells ein und veraendern
|
|
dessen Gewichte permanent. <strong className="text-red-600">Grundsaetzlich VERBOTEN</strong> fuer
|
|
externe Daten gemaess unserer Datenschutz-Policy.
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-white rounded border border-slate-200">
|
|
<div className="font-medium text-slate-800 flex items-center gap-2">
|
|
<span className="text-amber-600">Export</span>
|
|
<span className="text-slate-400">=</span>
|
|
<span className="text-slate-600">Weitergabe / Download / API</span>
|
|
</div>
|
|
<p className="text-xs text-slate-500 mt-1">
|
|
Daten werden an Dritte weitergegeben, zum Download bereitgestellt oder ueber eine API
|
|
ausgegeben. Erfordert Pruefung der Lizenzbedingungen und ggf. Namensnennung.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
|
|
{/* KI Use-Case Risk Matrix */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">KI-Use-Case Risikomatrix</h3>
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Zulaessigkeit von KI-Anwendungsfaellen nach Datenquelle.
|
|
</p>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-slate-200 bg-slate-50">
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">KI-Use-Case</th>
|
|
<th className="text-center py-2 px-3 font-medium text-slate-700">Open-Data SMD</th>
|
|
<th className="text-center py-2 px-3 font-medium text-slate-700">Regelwerke</th>
|
|
<th className="text-center py-2 px-3 font-medium text-slate-700">Lehrer-Uploads</th>
|
|
<th className="text-center py-2 px-3 font-medium text-slate-700">Risiko</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{[
|
|
{ useCase: 'Schul-Auswahl / Onboarding', openData: 'yes', rules: 'na', uploads: 'na', risk: 'low' },
|
|
{ useCase: 'Erwartungshorizont-Suche', openData: 'na', rules: 'yes', uploads: 'warn', risk: 'medium' },
|
|
{ useCase: 'Klausur-Korrektur (RAG)', openData: 'na', rules: 'warn', uploads: 'yes', risk: 'medium' },
|
|
{ useCase: 'Modell-Training', openData: 'no', rules: 'warn', uploads: 'no', risk: 'high' },
|
|
{ useCase: 'Auto-Schulerkennung', openData: 'no', rules: 'no', uploads: 'no', risk: 'high' },
|
|
].map((row, idx) => (
|
|
<tr key={idx} className={`border-b border-slate-100 hover:bg-slate-50 ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/30'}`}>
|
|
<td className="py-2 px-3 font-medium text-slate-800">{row.useCase}</td>
|
|
<td className="py-2 px-3 text-center">{row.openData === 'yes' ? '✅' : row.openData === 'warn' ? '⚠️' : row.openData === 'no' ? '❌' : '—'}</td>
|
|
<td className="py-2 px-3 text-center">{row.rules === 'yes' ? '✅' : row.rules === 'warn' ? '⚠️' : row.rules === 'no' ? '❌' : '—'}</td>
|
|
<td className="py-2 px-3 text-center">{row.uploads === 'yes' ? '✅' : row.uploads === 'warn' ? '⚠️' : row.uploads === 'no' ? '❌' : '—'}</td>
|
|
<td className="py-2 px-3 text-center">
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
|
row.risk === 'low' ? 'bg-green-100 text-green-700' :
|
|
row.risk === 'medium' ? 'bg-amber-100 text-amber-700' :
|
|
'bg-red-100 text-red-700'
|
|
}`}>
|
|
{row.risk === 'low' ? 'Niedrig' : row.risk === 'medium' ? 'Mittel' : 'Hoch'}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Licenses Info */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Unterstuetzte Lizenzen</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
<div className="p-4 bg-slate-50 rounded-lg">
|
|
<div className="font-medium text-slate-800">DL-DE-BY-2.0</div>
|
|
<div className="text-xs text-slate-500 mt-1">Datenlizenz Deutschland - Namensnennung</div>
|
|
<div className="text-xs text-green-600 mt-2">Attribution erforderlich</div>
|
|
</div>
|
|
<div className="p-4 bg-slate-50 rounded-lg">
|
|
<div className="font-medium text-slate-800">CC-BY</div>
|
|
<div className="text-xs text-slate-500 mt-1">Creative Commons Attribution</div>
|
|
<div className="text-xs text-green-600 mt-2">Attribution erforderlich</div>
|
|
</div>
|
|
<div className="p-4 bg-slate-50 rounded-lg">
|
|
<div className="font-medium text-slate-800">CC-BY-SA</div>
|
|
<div className="text-xs text-slate-500 mt-1">CC Attribution-ShareAlike</div>
|
|
<div className="text-xs text-amber-600 mt-2">Attribution + ShareAlike</div>
|
|
</div>
|
|
<div className="p-4 bg-slate-50 rounded-lg">
|
|
<div className="font-medium text-slate-800">CC0</div>
|
|
<div className="text-xs text-slate-500 mt-1">Public Domain</div>
|
|
<div className="text-xs text-slate-400 mt-2">Keine Attribution noetig</div>
|
|
</div>
|
|
<div className="p-4 bg-slate-50 rounded-lg">
|
|
<div className="font-medium text-slate-800">§5 UrhG</div>
|
|
<div className="text-xs text-slate-500 mt-1">Amtliche Werke</div>
|
|
<div className="text-xs text-green-600 mt-2">Quellenangabe erforderlich</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Technische Controls fuer Attribution */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Technische Controls fuer Attribution</h3>
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Massnahmen zur Sicherstellung der lizenzkonformen Quellenangabe im System.
|
|
</p>
|
|
<div className="space-y-3">
|
|
{[
|
|
{
|
|
id: 'CTRL-SRC-001',
|
|
name: 'Attribution bei Schulsuche',
|
|
description: 'Bei jedem Suchergebnis aus Open-Data-Portalen wird die Datenquelle, Lizenz und ein Link zum Bereitsteller angezeigt.',
|
|
status: 'implemented',
|
|
location: 'studio-v2/components/SchoolSearch.tsx',
|
|
},
|
|
{
|
|
id: 'CTRL-SRC-002',
|
|
name: 'Attribution bei RAG-Ergebnissen',
|
|
description: 'Pro EH-Vorschlag werden Dokumentname, Herausgeber und Lizenz angezeigt. Bei Einfuegen in Gutachten wird Zitation automatisch ergaenzt.',
|
|
status: 'implemented',
|
|
location: 'studio-v2/components/korrektur/EHSuggestionPanel.tsx',
|
|
},
|
|
{
|
|
id: 'CTRL-SRC-003',
|
|
name: 'Export-Attribution',
|
|
description: 'Bei PDF-Export wird ein Quellenverzeichnis am Ende eingefuegt. Bei Daten-Export werden Attribution-Metadaten mitgeliefert.',
|
|
status: 'planned',
|
|
location: 'klausur-service/export',
|
|
},
|
|
{
|
|
id: 'CTRL-SRC-004',
|
|
name: 'Attribution-Audit-Trail',
|
|
description: 'Logging welche Quellen fuer welche Outputs verwendet wurden. Nachweis fuer Auditoren ueber policy_audit_log.',
|
|
status: 'planned',
|
|
location: 'edu-search-service/internal/policy/audit.go',
|
|
},
|
|
].map((ctrl) => (
|
|
<div key={ctrl.id} className="p-4 border border-slate-200 rounded-lg hover:bg-slate-50">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="font-mono text-xs px-2 py-0.5 bg-purple-100 text-purple-700 rounded">{ctrl.id}</span>
|
|
<span className="font-medium text-slate-800">{ctrl.name}</span>
|
|
</div>
|
|
<p className="text-sm text-slate-600">{ctrl.description}</p>
|
|
<p className="text-xs text-slate-400 mt-1 font-mono">{ctrl.location}</p>
|
|
</div>
|
|
<span className={`flex-shrink-0 px-2 py-1 rounded text-xs font-medium ${
|
|
ctrl.status === 'implemented' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
|
|
}`}>
|
|
{ctrl.status === 'implemented' ? 'Implementiert' : 'Geplant'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Erlaubte Referenz-Domains */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Erlaubte Referenz-Domains (Audit-Dokumentation)</h3>
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Domains, auf die das System zu Referenz- und Compliance-Zwecken zugreifen darf.
|
|
Diese Zugriffe dienen ausschliesslich der rechtssicheren Klassifikation und Dokumentation.
|
|
</p>
|
|
<div className="space-y-3">
|
|
{[
|
|
{
|
|
domain: 'govdata.de',
|
|
reason: 'Der Zugriff auf govdata.de ist dauerhaft erlaubt, da es sich um ein amtliches Open-Data-Portal mit klarer Lizenz handelt. Die Nutzung erfolgt ausschliesslich zu Recherche- und Referenzzwecken, nicht fuer KI-Training.',
|
|
type: 'Datenquelle',
|
|
},
|
|
{
|
|
domain: 'creativecommons.org',
|
|
reason: 'Der Zugriff auf creativecommons.org ist dauerhaft erlaubt, da es sich um offizielle Lizenztexte handelt, die fuer die rechtssichere Klassifikation und Nutzung von Open-Data-Quellen erforderlich sind.',
|
|
type: 'Lizenz-Referenz',
|
|
},
|
|
{
|
|
domain: 'wiki.creativecommons.org',
|
|
reason: 'Der Zugriff auf wiki.creativecommons.org ist dauerhaft erlaubt, da es sich um offizielle Lizenzdokumentation handelt, die zur rechtssicheren Klassifikation von Datenquellen erforderlich ist.',
|
|
type: 'Lizenz-Dokumentation',
|
|
},
|
|
{
|
|
domain: 'gesetze-im-internet.de',
|
|
reason: 'Der Zugriff auf gesetze-im-internet.de ist dauerhaft erlaubt, da es sich um amtliche, urheberrechtsfreie Rechtsquellen (§5 UrhG) handelt, die zur rechtlichen Einordnung und Compliance-Dokumentation erforderlich sind.',
|
|
type: 'Rechtsquelle',
|
|
},
|
|
{
|
|
domain: 'nibis.de',
|
|
reason: 'Der Zugriff auf nibis.de (Niedersaechsischer Bildungsserver) ist dauerhaft erlaubt fuer den Abruf von Kerncurricula und Erwartungshorizonten. Die Nutzung erfolgt unter DL-DE-BY-2.0 mit Attribution.',
|
|
type: 'Bildungsquelle',
|
|
},
|
|
{
|
|
domain: 'kmk.org',
|
|
reason: 'Der Zugriff auf kmk.org (Kultusministerkonferenz) ist dauerhaft erlaubt, da KMK-Beschluesse als amtliche Werke nach §5 UrhG frei nutzbar sind. Quellenangabe erforderlich.',
|
|
type: 'Amtliche Quelle',
|
|
},
|
|
].map((item, idx) => (
|
|
<div key={item.domain} className="p-4 bg-slate-50 rounded-lg border border-slate-200">
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
|
|
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className="font-mono text-sm font-medium text-slate-800">{item.domain}</span>
|
|
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">{item.type}</span>
|
|
</div>
|
|
<p className="text-sm text-slate-600 italic">"{item.reason}"</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
|
|
<strong>Fuer Auditoren:</strong> Diese Statements dokumentieren die rechtliche Grundlage fuer den Systemzugriff auf externe Domains.
|
|
Alle Zugriffe werden im Audit-Log protokolliert.
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bundesweite Quellen */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Bundesweite Quellen</h3>
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Uebergreifende Open-Data-Portale und amtliche Quellen auf Bundesebene.
|
|
</p>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-slate-200">
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Quelle</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Typ</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Lizenz</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Einsatz</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr className="border-b border-slate-100 hover:bg-slate-50">
|
|
<td className="py-2 px-3 font-medium text-slate-800">GovData</td>
|
|
<td className="py-2 px-3">
|
|
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">Bund-ODP</span>
|
|
</td>
|
|
<td className="py-2 px-3">
|
|
<span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">DL-DE-BY-2.0</span>
|
|
</td>
|
|
<td className="py-2 px-3 text-slate-600">Aggregation / Fallback</td>
|
|
</tr>
|
|
<tr className="hover:bg-slate-50">
|
|
<td className="py-2 px-3 font-medium text-slate-800">Statistische Landesaemter</td>
|
|
<td className="py-2 px-3">
|
|
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">Amtlich</span>
|
|
</td>
|
|
<td className="py-2 px-3">
|
|
<span className="px-2 py-0.5 bg-amber-100 text-amber-700 rounded text-xs">variabel</span>
|
|
</td>
|
|
<td className="py-2 px-3 text-slate-600">Plausibilisierung</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bundeslaender Open Data Portale */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
<h3 className="font-semibold text-slate-900 mb-4">Bundeslaender Open Data Portale</h3>
|
|
<p className="text-sm text-slate-600 mb-4">
|
|
Zulaessige Landes-Open-Data-Portale fuer Schulstammdaten und Bildungsinformationen.
|
|
</p>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-slate-200">
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Bundesland</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Zulaessige Quelle</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700">Lizenz</th>
|
|
<th className="text-left py-2 px-3 font-medium text-slate-700 hidden md:table-cell">Hinweise</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{[
|
|
{ bl: 'BW', name: 'Baden-Wuerttemberg', source: 'Open Data Baden-Wuerttemberg', license: 'DL-DE-BY-2.0', note: 'Schulverzeichnisse ueber Ministerium / Kommunen' },
|
|
{ bl: 'BY', name: 'Bayern', source: 'Open Data Bayern', license: 'DL-DE-BY-2.0', note: 'Amtliche Schulnummern, Standorte' },
|
|
{ bl: 'BE', name: 'Berlin', source: 'Datenportal Berlin', license: 'CC-BY', note: 'Sehr gut gepflegte Schulstammdaten' },
|
|
{ bl: 'BB', name: 'Brandenburg', source: 'Daten Brandenburg', license: 'DL-DE-BY-2.0', note: 'Kommunale Ergaenzungen pruefen' },
|
|
{ bl: 'HB', name: 'Bremen', source: 'Open Data Bremen', license: 'CC-BY', note: 'Kleine Datenmenge, sauber' },
|
|
{ bl: 'HH', name: 'Hamburg', source: 'Transparenzportal Hamburg', license: 'DL-DE-BY-2.0', note: 'Sehr gute Metadaten' },
|
|
{ bl: 'HE', name: 'Hessen', source: 'Open Data Hessen', license: 'DL-DE-BY-2.0', note: 'Schultraegerdaten' },
|
|
{ bl: 'MV', name: 'Mecklenburg-Vorpommern', source: 'Open Data MV', license: 'DL-DE-BY-2.0', note: 'Teilweise CSV/Excel' },
|
|
{ bl: 'NI', name: 'Niedersachsen', source: 'Open Data Niedersachsen', license: 'DL-DE-BY-2.0', note: 'Ergaenzend: NIBIS nur Regelwerke, nicht Personen' },
|
|
{ bl: 'NW', name: 'Nordrhein-Westfalen', source: 'Open.NRW', license: 'DL-DE-BY-2.0', note: 'Umfangreich, kommunale Qualitaet pruefen' },
|
|
{ bl: 'RP', name: 'Rheinland-Pfalz', source: 'Open Data Rheinland-Pfalz', license: 'DL-DE-BY-2.0', note: 'Schulformen & Standorte' },
|
|
{ bl: 'SL', name: 'Saarland', source: 'Open Data Saarland', license: 'DL-DE-BY-2.0', note: 'Klein, aber zulaessig' },
|
|
{ bl: 'SN', name: 'Sachsen', source: 'Datenportal Sachsen', license: 'DL-DE-BY-2.0', note: 'Gute Pflege' },
|
|
{ bl: 'ST', name: 'Sachsen-Anhalt', source: 'Open Data Sachsen-Anhalt', license: 'DL-DE-BY-2.0', note: 'CSV/JSON verfuegbar' },
|
|
{ bl: 'SH', name: 'Schleswig-Holstein', source: 'Open Data Schleswig-Holstein', license: 'DL-DE-BY-2.0', note: 'Einheitliche IDs' },
|
|
{ bl: 'TH', name: 'Thueringen', source: 'Open Data Thueringen', license: 'DL-DE-BY-2.0', note: 'Kommunale Ergaenzungen' },
|
|
].map((item, idx) => (
|
|
<tr key={item.bl} className={`border-b border-slate-100 hover:bg-slate-50 ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/50'}`}>
|
|
<td className="py-2 px-3">
|
|
<span className="inline-flex items-center gap-2">
|
|
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs font-mono">{item.bl}</span>
|
|
<span className="text-slate-700 hidden sm:inline">{item.name}</span>
|
|
</span>
|
|
</td>
|
|
<td className="py-2 px-3 font-medium text-slate-800">{item.source}</td>
|
|
<td className="py-2 px-3">
|
|
<span className={`px-2 py-0.5 rounded text-xs ${
|
|
item.license === 'CC-BY'
|
|
? 'bg-blue-100 text-blue-700'
|
|
: 'bg-green-100 text-green-700'
|
|
}`}>
|
|
{item.license}
|
|
</span>
|
|
</td>
|
|
<td className="py-2 px-3 text-slate-500 text-xs hidden md:table-cell">{item.note}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800">
|
|
<strong>Hinweis:</strong> Alle Landes-ODP sind vom Typ "Landes-ODP" und erfordern Attribution gemaess der jeweiligen Lizenz.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|