refactor(admin): split controls, training, control-provenance, iace/verification pages
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>
This commit is contained in:
197
admin-compliance/app/sdk/controls/_hooks/useControlsData.ts
Normal file
197
admin-compliance/app/sdk/controls/_hooks/useControlsData.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
'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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { RAGControlSuggestion } from '../_types'
|
||||
|
||||
export function useRAGSuggestions(setError: (msg: string | null) => void) {
|
||||
const [ragLoading, setRagLoading] = useState(false)
|
||||
const [ragSuggestions, setRagSuggestions] = useState<RAGControlSuggestion[]>([])
|
||||
const [showRagPanel, setShowRagPanel] = useState(false)
|
||||
const [selectedRequirementId, setSelectedRequirementId] = useState<string>('')
|
||||
|
||||
const suggestControlsFromRAG = async () => {
|
||||
if (!selectedRequirementId) {
|
||||
setError('Bitte eine Anforderungs-ID eingeben.')
|
||||
return
|
||||
}
|
||||
setRagLoading(true)
|
||||
setRagSuggestions([])
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/compliance/ai/suggest-controls', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ requirement_id: selectedRequirementId }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const msg = await res.text()
|
||||
throw new Error(msg || `HTTP ${res.status}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
setRagSuggestions(data.suggestions || [])
|
||||
setShowRagPanel(true)
|
||||
} catch (e) {
|
||||
setError(`KI-Vorschlaege fehlgeschlagen: ${e instanceof Error ? e.message : 'Unbekannter Fehler'}`)
|
||||
} finally {
|
||||
setRagLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const removeSuggestion = (controlId: string) => {
|
||||
setRagSuggestions(prev => prev.filter(s => s.control_id !== controlId))
|
||||
}
|
||||
|
||||
return {
|
||||
ragLoading,
|
||||
ragSuggestions,
|
||||
showRagPanel,
|
||||
setShowRagPanel,
|
||||
selectedRequirementId,
|
||||
setSelectedRequirementId,
|
||||
suggestControlsFromRAG,
|
||||
removeSuggestion,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user