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
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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user