feat: Alle 5 verbleibenden SDK-Module auf 100% — RAG, Security-Backlog, Quality, Notfallplan, Loeschfristen
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 34s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s

Paket A — RAG Proxy:
- NEU: admin-compliance/app/api/sdk/v1/rag/[[...path]]/route.ts
  → Proxy zu ai-compliance-sdk:8090, GET+POST, UUID-Validierung
- UPDATE: rag/page.tsx — setTimeout Mock → echte API-Calls
  GET /regulations → dynamische suggestedQuestions
  POST /search → Qdrant-Ergebnisse mit score, title, reference

Paket B — Security-Backlog + Quality:
- NEU: migrations/014_security_backlog.sql + 015_quality.sql
- NEU: compliance/api/security_backlog_routes.py — CRUD + Stats
- NEU: compliance/api/quality_routes.py — Metrics + Tests CRUD + Stats
- UPDATE: security-backlog/page.tsx — mockItems → API
- UPDATE: quality/page.tsx — mockMetrics/mockTests → API
- UPDATE: compliance/api/__init__.py — Router-Registrierung
- NEU: tests/test_security_backlog_routes.py (48 Tests — 48/48 bestanden)
- NEU: tests/test_quality_routes.py (67 Tests — 67/67 bestanden)

Paket C — Notfallplan Incidents + Templates:
- NEU: migrations/016_notfallplan_incidents.sql
  compliance_notfallplan_incidents + compliance_notfallplan_templates
- UPDATE: notfallplan_routes.py — GET/POST/PUT/DELETE für /incidents + /templates
- UPDATE: notfallplan/page.tsx — Incidents-Tab + Templates-Tab → API
- UPDATE: tests/test_notfallplan_routes.py (+76 neue Tests — alle bestanden)

Paket D — Loeschfristen localStorage → API:
- NEU: migrations/017_loeschfristen.sql (JSONB: legal_holds, storage_locations, ...)
- NEU: compliance/api/loeschfristen_routes.py — CRUD + Stats + Status-Update
- UPDATE: loeschfristen/page.tsx — vollständige localStorage → API Migration
  createNewPolicy → POST (API-UUID als id), deletePolicy → DELETE,
  handleSaveAndClose → PUT, adoptGeneratedPolicies → POST je Policy
  apiToPolicy() + policyToPayload() Mapper, saving-State für Buttons
- NEU: tests/test_loeschfristen_routes.py (58 Tests — alle bestanden)

Gesamt: 253 neue Tests, alle bestanden (48 + 67 + 76 + 58 + bestehende)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-03 18:04:53 +01:00
parent 9143b84daa
commit 25d5da78ef
19 changed files with 5718 additions and 524 deletions

View File

@@ -387,10 +387,9 @@ export default function NotfallplanPage() {
const [savingExercise, setSavingExercise] = useState(false)
useEffect(() => {
// Only load config and exercises from localStorage (incidents/templates use API)
const data = loadFromStorage()
setConfig(data.config)
setIncidents(data.incidents)
setTemplates(data.templates)
setExercises(data.exercises)
}, [])
@@ -401,10 +400,12 @@ export default function NotfallplanPage() {
async function loadApiData() {
setApiLoading(true)
try {
const [contactsRes, scenariosRes, exercisesRes] = await Promise.all([
const [contactsRes, scenariosRes, exercisesRes, incidentsRes, templatesRes] = await Promise.all([
fetch(`${NOTFALLPLAN_API}/contacts`),
fetch(`${NOTFALLPLAN_API}/scenarios`),
fetch(`${NOTFALLPLAN_API}/exercises`),
fetch(`${NOTFALLPLAN_API}/incidents`),
fetch(`${NOTFALLPLAN_API}/templates`),
])
if (contactsRes.ok) {
const data = await contactsRes.json()
@@ -418,6 +419,14 @@ export default function NotfallplanPage() {
const data = await exercisesRes.json()
setApiExercises(Array.isArray(data) ? data : [])
}
if (incidentsRes.ok) {
const data = await incidentsRes.json()
setIncidents(Array.isArray(data) ? data.map(apiToIncident) : [])
}
if (templatesRes.ok) {
const data = await templatesRes.json()
setTemplates(Array.isArray(data) && data.length > 0 ? data : DEFAULT_TEMPLATES)
}
} catch (err) {
console.error('Failed to load Notfallplan API data:', err)
} finally {
@@ -425,6 +434,106 @@ export default function NotfallplanPage() {
}
}
function apiToIncident(raw: any): Incident {
return {
id: raw.id,
title: raw.title,
description: raw.description || '',
detectedAt: raw.detected_at,
detectedBy: raw.detected_by || 'Admin',
status: raw.status,
severity: raw.severity,
affectedDataCategories: raw.affected_data_categories || [],
estimatedAffectedPersons: raw.estimated_affected_persons || 0,
measures: raw.measures || [],
art34Required: raw.art34_required || false,
art34Justification: raw.art34_justification || '',
reportedToAuthorityAt: raw.reported_to_authority_at,
notifiedAffectedAt: raw.notified_affected_at,
closedAt: raw.closed_at,
closedBy: raw.closed_by,
lessonsLearned: raw.lessons_learned,
}
}
async function apiAddIncident(newIncident: Partial<Incident>) {
try {
const res = await fetch(`${NOTFALLPLAN_API}/incidents`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: newIncident.title,
description: newIncident.description,
severity: newIncident.severity || 'medium',
estimated_affected_persons: newIncident.estimatedAffectedPersons || 0,
art34_required: newIncident.art34Required || false,
art34_justification: newIncident.art34Justification || '',
}),
})
if (res.ok) {
const created = await res.json()
setIncidents(prev => [apiToIncident(created), ...prev])
}
} catch (err) {
console.error('Failed to create incident:', err)
}
}
async function apiUpdateIncidentStatus(id: string, status: IncidentStatus) {
try {
const body: any = { status }
if (status === 'reported') body.reported_to_authority_at = new Date().toISOString()
if (status === 'closed') { body.closed_at = new Date().toISOString(); body.closed_by = 'Admin' }
const res = await fetch(`${NOTFALLPLAN_API}/incidents/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (res.ok) {
const updated = await res.json()
setIncidents(prev => prev.map(inc => inc.id === id ? apiToIncident(updated) : inc))
}
} catch (err) {
console.error('Failed to update incident status:', err)
}
}
async function apiDeleteIncident(id: string) {
try {
const res = await fetch(`${NOTFALLPLAN_API}/incidents/${id}`, { method: 'DELETE' })
if (res.ok || res.status === 204) {
setIncidents(prev => prev.filter(inc => inc.id !== id))
}
} catch (err) {
console.error('Failed to delete incident:', err)
}
}
async function apiSaveTemplate(template: MeldeTemplate) {
try {
const isNew = !template.id.startsWith('tpl-')
const method = isNew || template.id.length < 10 ? 'POST' : 'PUT'
const url = method === 'POST'
? `${NOTFALLPLAN_API}/templates`
: `${NOTFALLPLAN_API}/templates/${template.id}`
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: template.type, title: template.title, content: template.content }),
})
if (res.ok) {
const saved = await res.json()
setTemplates(prev => {
const existing = prev.find(t => t.id === template.id)
if (existing) return prev.map(t => t.id === template.id ? { ...t, ...saved } : t)
return [...prev, saved]
})
}
} catch (err) {
console.error('Failed to save template:', err)
}
}
async function handleCreateContact() {
if (!newContact.name) return
setSavingContact(true)
@@ -529,10 +638,11 @@ export default function NotfallplanPage() {
}
const handleSave = useCallback(() => {
saveToStorage({ config, incidents, templates, exercises })
// Only config and exercises are persisted to localStorage; incidents/templates use API
saveToStorage({ config, incidents: [], templates: DEFAULT_TEMPLATES, exercises })
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}, [config, incidents, templates, exercises])
}, [config, exercises])
const tabs: { id: Tab; label: string; count?: number }[] = [
{ id: 'config', label: 'Notfallplan' },
@@ -697,10 +807,13 @@ export default function NotfallplanPage() {
setIncidents={setIncidents}
showAdd={showAddIncident}
setShowAdd={setShowAddIncident}
onAdd={apiAddIncident}
onStatusChange={apiUpdateIncidentStatus}
onDelete={apiDeleteIncident}
/>
)}
{activeTab === 'templates' && (
<TemplatesTab templates={templates} setTemplates={setTemplates} />
<TemplatesTab templates={templates} setTemplates={setTemplates} onSave={apiSaveTemplate} />
)}
{activeTab === 'exercises' && (
<>
@@ -1252,11 +1365,17 @@ function IncidentsTab({
setIncidents,
showAdd,
setShowAdd,
onAdd,
onStatusChange,
onDelete,
}: {
incidents: Incident[]
setIncidents: React.Dispatch<React.SetStateAction<Incident[]>>
showAdd: boolean
setShowAdd: (v: boolean) => void
onAdd?: (incident: Partial<Incident>) => Promise<void>
onStatusChange?: (id: string, status: IncidentStatus) => Promise<void>
onDelete?: (id: string) => Promise<void>
}) {
const [newIncident, setNewIncident] = useState<Partial<Incident>>({
title: '',
@@ -1269,23 +1388,27 @@ function IncidentsTab({
art34Justification: '',
})
function addIncident() {
async function addIncident() {
if (!newIncident.title) return
const incident: Incident = {
id: `INC-${Date.now()}`,
title: newIncident.title || '',
description: newIncident.description || '',
detectedAt: new Date().toISOString(),
detectedBy: 'Admin',
status: 'detected',
severity: newIncident.severity as IncidentSeverity || 'medium',
affectedDataCategories: newIncident.affectedDataCategories || [],
estimatedAffectedPersons: newIncident.estimatedAffectedPersons || 0,
measures: newIncident.measures || [],
art34Required: newIncident.art34Required || false,
art34Justification: newIncident.art34Justification || '',
if (onAdd) {
await onAdd(newIncident)
} else {
const incident: Incident = {
id: `INC-${Date.now()}`,
title: newIncident.title || '',
description: newIncident.description || '',
detectedAt: new Date().toISOString(),
detectedBy: 'Admin',
status: 'detected',
severity: newIncident.severity as IncidentSeverity || 'medium',
affectedDataCategories: newIncident.affectedDataCategories || [],
estimatedAffectedPersons: newIncident.estimatedAffectedPersons || 0,
measures: newIncident.measures || [],
art34Required: newIncident.art34Required || false,
art34Justification: newIncident.art34Justification || '',
}
setIncidents(prev => [incident, ...prev])
}
setIncidents(prev => [incident, ...prev])
setShowAdd(false)
setNewIncident({
title: '', description: '', severity: 'medium',
@@ -1294,17 +1417,21 @@ function IncidentsTab({
})
}
function updateStatus(id: string, status: IncidentStatus) {
setIncidents(prev => prev.map(inc =>
inc.id === id
? {
...inc,
status,
...(status === 'reported' ? { reportedToAuthorityAt: new Date().toISOString() } : {}),
...(status === 'closed' ? { closedAt: new Date().toISOString(), closedBy: 'Admin' } : {}),
}
: inc
))
async function updateStatus(id: string, status: IncidentStatus) {
if (onStatusChange) {
await onStatusChange(id, status)
} else {
setIncidents(prev => prev.map(inc =>
inc.id === id
? {
...inc,
status,
...(status === 'reported' ? { reportedToAuthorityAt: new Date().toISOString() } : {}),
...(status === 'closed' ? { closedAt: new Date().toISOString(), closedBy: 'Admin' } : {}),
}
: inc
))
}
}
return (
@@ -1464,6 +1591,14 @@ function IncidentsTab({
Abschliessen
</button>
)}
{onDelete && (
<button
onClick={() => { if (window.confirm('Incident loeschen?')) onDelete(incident.id) }}
className="text-xs px-2 py-1 text-red-400 hover:text-red-600 hover:bg-red-50 rounded ml-auto"
>
Loeschen
</button>
)}
</div>
)}
</div>
@@ -1481,10 +1616,24 @@ function IncidentsTab({
function TemplatesTab({
templates,
setTemplates,
onSave,
}: {
templates: MeldeTemplate[]
setTemplates: React.Dispatch<React.SetStateAction<MeldeTemplate[]>>
onSave?: (template: MeldeTemplate) => Promise<void>
}) {
const [saving, setSaving] = useState<string | null>(null)
async function handleSave(template: MeldeTemplate) {
if (!onSave) return
setSaving(template.id)
try {
await onSave(template)
} finally {
setSaving(null)
}
}
return (
<div className="space-y-6">
<div>
@@ -1496,15 +1645,26 @@ function TemplatesTab({
{templates.map(template => (
<div key={template.id} className="bg-white rounded-lg border p-6">
<div className="flex items-center gap-2 mb-3">
<span className={`px-2 py-1 rounded text-xs font-bold ${
template.type === 'art33'
? 'bg-blue-100 text-blue-800'
: 'bg-purple-100 text-purple-800'
}`}>
{template.type === 'art33' ? 'Art. 33' : 'Art. 34'}
</span>
<h4 className="font-medium">{template.title}</h4>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-bold ${
template.type === 'art33'
? 'bg-blue-100 text-blue-800'
: 'bg-purple-100 text-purple-800'
}`}>
{template.type === 'art33' ? 'Art. 33' : 'Art. 34'}
</span>
<h4 className="font-medium">{template.title}</h4>
</div>
{onSave && (
<button
onClick={() => handleSave(template)}
disabled={saving === template.id}
className="px-3 py-1 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{saving === template.id ? 'Gespeichert...' : 'Speichern'}
</button>
)}
</div>
<textarea
value={template.content}