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>
299 lines
9.8 KiB
TypeScript
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) }}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|