Files
Sharang Parnerkar 375b34a0d8 refactor(admin): split consent-management, control-library, incidents, training pages
Agent-completed splits committed after agents hit rate limits before
committing their work. All 4 pages now under 500 LOC:

- consent-management: 1303 -> 193 LOC (+ 7 _components, _hooks, _data, _types)
- control-library: 1210 -> 298 LOC (+ _components, _types)
- incidents: 1150 -> 373 LOC (+ _components)
- training: 1127 -> 366 LOC (+ _components)

Verification: next build clean (142 pages generated).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:52:45 +02:00

299 lines
9.8 KiB
TypeScript

'use client'
import { useState, useEffect, useMemo, useCallback } from 'react'
import type { CanonicalControl, Framework, ControlFormData } from './_types'
import { EMPTY_CONTROL, BACKEND_URL, getDomain } from './_types'
import { ControlForm } from './_components/ControlForm'
import { ControlDetailView } from './_components/ControlDetailView'
import { ControlListView } from './_components/ControlListView'
import { GeneratorModal } from './_components/GeneratorModal'
export default function ControlLibraryPage() {
const [frameworks, setFrameworks] = useState<Framework[]>([])
const [controls, setControls] = useState<CanonicalControl[]>([])
const [selectedControl, setSelectedControl] = useState<CanonicalControl | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Filters
const [searchQuery, setSearchQuery] = useState('')
const [severityFilter, setSeverityFilter] = useState<string>('')
const [domainFilter, setDomainFilter] = useState<string>('')
// CRUD state
const [mode, setMode] = useState<'list' | 'detail' | 'create' | 'edit'>('list')
const [saving, setSaving] = useState(false)
// Generator state
const [showGenerator, setShowGenerator] = useState(false)
const [generating, setGenerating] = useState(false)
const [genResult, setGenResult] = useState<Record<string, unknown> | null>(null)
const [genDomain, setGenDomain] = useState('')
const [genMaxControls, setGenMaxControls] = useState(10)
const [genDryRun, setGenDryRun] = useState(true)
const [stateFilter, setStateFilter] = useState<string>('')
const [processedStats, setProcessedStats] = useState<Array<Record<string, unknown>>>([])
const [showStats, setShowStats] = useState(false)
// Load data
const loadData = useCallback(async () => {
try {
setLoading(true)
const [fwRes, ctrlRes] = await Promise.all([
fetch(`${BACKEND_URL}?endpoint=frameworks`),
fetch(`${BACKEND_URL}?endpoint=controls`),
])
if (fwRes.ok) setFrameworks(await fwRes.json())
if (ctrlRes.ok) setControls(await ctrlRes.json())
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
} finally {
setLoading(false)
}
}, [])
useEffect(() => { loadData() }, [loadData])
// Derived: unique domains
const domains = useMemo(() => {
const set = new Set(controls.map(c => getDomain(c.control_id)))
return Array.from(set).sort()
}, [controls])
// Filtered controls
const filteredControls = useMemo(() => {
return controls.filter(c => {
if (severityFilter && c.severity !== severityFilter) return false
if (domainFilter && getDomain(c.control_id) !== domainFilter) return false
if (stateFilter && c.release_state !== stateFilter) return false
if (searchQuery) {
const q = searchQuery.toLowerCase()
return (
c.control_id.toLowerCase().includes(q) ||
c.title.toLowerCase().includes(q) ||
c.objective.toLowerCase().includes(q) ||
c.tags.some(t => t.toLowerCase().includes(q))
)
}
return true
})
}, [controls, severityFilter, domainFilter, stateFilter, searchQuery])
// CRUD handlers
const handleCreate = async (data: ControlFormData) => {
setSaving(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=create-control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.json()
alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`)
return
}
await loadData()
setMode('list')
} catch {
alert('Netzwerkfehler')
} finally {
setSaving(false)
}
}
const handleUpdate = async (data: ControlFormData) => {
if (!selectedControl) return
setSaving(true)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=update-control&id=${selectedControl.control_id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) {
const err = await res.json()
alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`)
return
}
await loadData()
setSelectedControl(null)
setMode('list')
} catch {
alert('Netzwerkfehler')
} finally {
setSaving(false)
}
}
const handleDelete = async (controlId: string) => {
if (!confirm(`Control ${controlId} wirklich loeschen?`)) return
try {
const res = await fetch(`${BACKEND_URL}?id=${controlId}`, { method: 'DELETE' })
if (!res.ok && res.status !== 204) {
alert('Fehler beim Loeschen')
return
}
await loadData()
setSelectedControl(null)
setMode('list')
} catch {
alert('Netzwerkfehler')
}
}
// Generator handlers
const handleGenerate = async () => {
setGenerating(true)
setGenResult(null)
try {
const res = await fetch(`${BACKEND_URL}?endpoint=generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
domain: genDomain || null,
max_controls: genMaxControls,
dry_run: genDryRun,
skip_web_search: false,
}),
})
if (!res.ok) {
const err = await res.json()
setGenResult({ status: 'error', message: err.error || err.details || 'Fehler' })
return
}
const data = await res.json()
setGenResult(data)
if (!genDryRun) {
await loadData()
}
} catch {
setGenResult({ status: 'error', message: 'Netzwerkfehler' })
} finally {
setGenerating(false)
}
}
const loadProcessedStats = async () => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=processed-stats`)
if (res.ok) {
const data = await res.json()
setProcessedStats(data.stats || [])
}
} catch { /* ignore */ }
}
const handleReview = async (controlId: string, action: string) => {
try {
const res = await fetch(`${BACKEND_URL}?endpoint=review&id=${controlId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
})
if (res.ok) {
await loadData()
setSelectedControl(null)
setMode('list')
}
} catch { /* ignore */ }
}
if (loading) {
return (
<div className="flex items-center justify-center h-96">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-purple-600 border-t-transparent" />
</div>
)
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-800 text-sm">{error}</div>
</div>
)
}
// CREATE MODE
if (mode === 'create') {
return <ControlForm initial={EMPTY_CONTROL} onSave={handleCreate} onCancel={() => setMode('list')} saving={saving} />
}
// EDIT MODE
if (mode === 'edit' && selectedControl) {
const editData: ControlFormData = {
framework_id: frameworks[0]?.framework_id || 'bp_security_v1',
control_id: selectedControl.control_id,
title: selectedControl.title,
objective: selectedControl.objective,
rationale: selectedControl.rationale,
scope: selectedControl.scope || { platforms: [], components: [], data_classes: [] },
requirements: selectedControl.requirements.length ? selectedControl.requirements : [''],
test_procedure: selectedControl.test_procedure.length ? selectedControl.test_procedure : [''],
evidence: selectedControl.evidence.length ? selectedControl.evidence : [{ type: '', description: '' }],
severity: selectedControl.severity,
risk_score: selectedControl.risk_score,
implementation_effort: selectedControl.implementation_effort,
open_anchors: selectedControl.open_anchors.length ? selectedControl.open_anchors : [{ framework: '', ref: '', url: '' }],
release_state: selectedControl.release_state,
tags: selectedControl.tags,
}
return <ControlForm initial={editData} onSave={handleUpdate} onCancel={() => { setMode('detail') }} saving={saving} />
}
// DETAIL VIEW
if (mode === 'detail' && selectedControl) {
return (
<ControlDetailView
ctrl={selectedControl}
onBack={() => { setSelectedControl(null); setMode('list') }}
onEdit={() => setMode('edit')}
onDelete={handleDelete}
onReview={handleReview}
/>
)
}
// LIST VIEW
return (
<>
<ControlListView
controls={controls}
filteredControls={filteredControls}
frameworks={frameworks}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
severityFilter={severityFilter}
setSeverityFilter={setSeverityFilter}
domainFilter={domainFilter}
setDomainFilter={setDomainFilter}
stateFilter={stateFilter}
setStateFilter={setStateFilter}
domains={domains}
showStats={showStats}
toggleStats={() => { setShowStats(!showStats); if (!showStats) loadProcessedStats() }}
processedStats={processedStats}
onOpenGenerator={() => setShowGenerator(true)}
onCreate={() => setMode('create')}
onSelect={(ctrl) => { setSelectedControl(ctrl); setMode('detail') }}
/>
{showGenerator && (
<GeneratorModal
genDomain={genDomain}
setGenDomain={setGenDomain}
genMaxControls={genMaxControls}
setGenMaxControls={setGenMaxControls}
genDryRun={genDryRun}
setGenDryRun={setGenDryRun}
generating={generating}
genResult={genResult}
onGenerate={handleGenerate}
onClose={() => { setShowGenerator(false); setGenResult(null) }}
/>
)}
</>
)
}