Each page.tsx exceeded the 500-LOC hard cap. Extracted components and hooks into colocated _components/ and _hooks/ directories; page.tsx is now a thin orchestrator. - controls/page.tsx: 944 → 180 LOC; extracted ControlCard, AddControlForm, LoadingSkeleton, TransitionErrorBanner, StatsCards, FilterBar, RAGPanel into _components/ and useControlsData, useRAGSuggestions into _hooks/; types into _types.ts - training/page.tsx: 780 → 288 LOC; extracted ContentTab (inline content generator tab) into _components/ContentTab.tsx - control-provenance/page.tsx: 739 → 122 LOC; extracted MarkdownRenderer, UsageBadge, PermBadge, LicenseMatrix, SourceRegistry into _components/; PROVENANCE_SECTIONS static data into _data/provenance-sections.ts - iace/[projectId]/verification/page.tsx: 673 → 196 LOC; extracted StatusBadge, VerificationForm, CompleteModal, SuggestEvidenceModal, VerificationTable into _components/ Zero behavior changes; logic relocated verbatim. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
198 lines
7.3 KiB
TypeScript
198 lines
7.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useSDK, Control as SDKControl, ControlType, ImplementationStatus } from '@/lib/sdk'
|
|
import { mapControlTypeToDisplay, mapStatusToDisplay } from '../_types'
|
|
import type { DisplayControl, RAGControlSuggestion } from '../_types'
|
|
|
|
export function useControlsData() {
|
|
const { state, dispatch } = useSDK()
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [effectivenessMap, setEffectivenessMap] = useState<Record<string, number>>({})
|
|
const [evidenceMap, setEvidenceMap] = useState<Record<string, { id: string; title: string; status: string }[]>>({})
|
|
const [transitionError, setTransitionError] = useState<{ controlId: string; violations: string[] } | null>(null)
|
|
|
|
const fetchEvidenceForControls = async (_controlIds: string[]) => {
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/compliance/evidence')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const allEvidence = data.evidence || data
|
|
if (Array.isArray(allEvidence)) {
|
|
const map: Record<string, { id: string; title: string; status: string; confidenceLevel?: string }[]> = {}
|
|
for (const ev of allEvidence) {
|
|
const ctrlId = ev.control_id || ''
|
|
if (!map[ctrlId]) map[ctrlId] = []
|
|
map[ctrlId].push({
|
|
id: ev.id,
|
|
title: ev.title || ev.name || 'Nachweis',
|
|
status: ev.status || 'pending',
|
|
confidenceLevel: ev.confidence_level || undefined,
|
|
})
|
|
}
|
|
setEvidenceMap(map)
|
|
}
|
|
}
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
const fetchControls = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const res = await fetch('/api/sdk/v1/compliance/controls')
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
const backendControls = data.controls || data
|
|
if (Array.isArray(backendControls) && backendControls.length > 0) {
|
|
const mapped: SDKControl[] = backendControls.map((c: Record<string, unknown>) => ({
|
|
id: (c.control_id || c.id) as string,
|
|
name: (c.name || c.title || '') as string,
|
|
description: (c.description || '') as string,
|
|
type: ((c.type || c.control_type || 'TECHNICAL') as string).toUpperCase() as ControlType,
|
|
category: (c.category || '') as string,
|
|
implementationStatus: ((c.implementation_status || c.status || 'NOT_IMPLEMENTED') as string).toUpperCase() as ImplementationStatus,
|
|
effectiveness: (c.effectiveness || 'LOW') as 'LOW' | 'MEDIUM' | 'HIGH',
|
|
evidence: (c.evidence || []) as string[],
|
|
owner: (c.owner || null) as string | null,
|
|
dueDate: c.due_date ? new Date(c.due_date as string) : null,
|
|
}))
|
|
dispatch({ type: 'SET_STATE', payload: { controls: mapped } })
|
|
setError(null)
|
|
fetchEvidenceForControls(mapped.map(c => c.id))
|
|
return
|
|
}
|
|
}
|
|
} catch {
|
|
// API not available — show empty state
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
fetchControls()
|
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const displayControls: DisplayControl[] = state.controls.map(ctrl => {
|
|
const effectivenessPercent = effectivenessMap[ctrl.id] ??
|
|
(ctrl.implementationStatus === 'IMPLEMENTED' ? 85 :
|
|
ctrl.implementationStatus === 'PARTIAL' ? 50 : 0)
|
|
return {
|
|
id: ctrl.id,
|
|
name: ctrl.name,
|
|
description: ctrl.description,
|
|
type: ctrl.type,
|
|
category: ctrl.category,
|
|
implementationStatus: ctrl.implementationStatus,
|
|
evidence: ctrl.evidence,
|
|
owner: ctrl.owner,
|
|
dueDate: ctrl.dueDate,
|
|
code: ctrl.id,
|
|
displayType: 'preventive' as const,
|
|
displayCategory: mapControlTypeToDisplay(ctrl.type),
|
|
displayStatus: mapStatusToDisplay(ctrl.implementationStatus),
|
|
effectivenessPercent,
|
|
linkedRequirements: [],
|
|
linkedEvidence: evidenceMap[ctrl.id] || [],
|
|
lastReview: new Date(),
|
|
}
|
|
})
|
|
|
|
const handleStatusChange = async (controlId: string, newStatus: ImplementationStatus) => {
|
|
const oldControl = state.controls.find(c => c.id === controlId)
|
|
const oldStatus = oldControl?.implementationStatus
|
|
|
|
dispatch({ type: 'UPDATE_CONTROL', payload: { id: controlId, data: { implementationStatus: newStatus } } })
|
|
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ implementation_status: newStatus }),
|
|
})
|
|
if (!res.ok) {
|
|
if (oldStatus) {
|
|
dispatch({ type: 'UPDATE_CONTROL', payload: { id: controlId, data: { implementationStatus: oldStatus } } })
|
|
}
|
|
const err = await res.json().catch(() => ({ detail: 'Status-Aenderung fehlgeschlagen' }))
|
|
if (res.status === 409 && err.detail?.violations) {
|
|
setTransitionError({ controlId, violations: err.detail.violations })
|
|
} else {
|
|
const msg = typeof err.detail === 'string' ? err.detail : err.detail?.error || 'Status-Aenderung fehlgeschlagen'
|
|
setError(msg)
|
|
}
|
|
} else {
|
|
setTransitionError(prev => prev?.controlId === controlId ? null : prev)
|
|
}
|
|
} catch {
|
|
if (oldStatus) {
|
|
dispatch({ type: 'UPDATE_CONTROL', payload: { id: controlId, data: { implementationStatus: oldStatus } } })
|
|
}
|
|
setError('Netzwerkfehler bei Status-Aenderung')
|
|
}
|
|
}
|
|
|
|
const handleEffectivenessChange = async (controlId: string, effectiveness: number) => {
|
|
setEffectivenessMap(prev => ({ ...prev, [controlId]: effectiveness }))
|
|
try {
|
|
await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ effectiveness_score: effectiveness }),
|
|
})
|
|
} catch {
|
|
// Silently fail
|
|
}
|
|
}
|
|
|
|
const handleAddControl = (data: { name: string; description: string; type: ControlType; category: string; owner: string }) => {
|
|
const newControl: SDKControl = {
|
|
id: `ctrl-${Date.now()}`,
|
|
name: data.name,
|
|
description: data.description,
|
|
type: data.type,
|
|
category: data.category,
|
|
implementationStatus: 'NOT_IMPLEMENTED',
|
|
effectiveness: 'LOW',
|
|
evidence: [],
|
|
owner: data.owner || null,
|
|
dueDate: null,
|
|
}
|
|
dispatch({ type: 'ADD_CONTROL', payload: newControl })
|
|
}
|
|
|
|
const addSuggestedControl = (suggestion: RAGControlSuggestion) => {
|
|
const newControl: SDKControl = {
|
|
id: `rag-${suggestion.control_id}-${Date.now()}`,
|
|
name: suggestion.title,
|
|
description: suggestion.description,
|
|
type: 'TECHNICAL',
|
|
category: suggestion.domain,
|
|
implementationStatus: 'NOT_IMPLEMENTED',
|
|
effectiveness: 'LOW',
|
|
evidence: [],
|
|
owner: null,
|
|
dueDate: null,
|
|
}
|
|
dispatch({ type: 'ADD_CONTROL', payload: newControl })
|
|
}
|
|
|
|
return {
|
|
state,
|
|
loading,
|
|
error,
|
|
setError,
|
|
effectivenessMap,
|
|
evidenceMap,
|
|
displayControls,
|
|
transitionError,
|
|
setTransitionError,
|
|
handleStatusChange,
|
|
handleEffectivenessChange,
|
|
handleAddControl,
|
|
addSuggestedControl,
|
|
}
|
|
}
|