diff --git a/admin-compliance/app/sdk/controls/_components/AddControlForm.tsx b/admin-compliance/app/sdk/controls/_components/AddControlForm.tsx
new file mode 100644
index 0000000..e842844
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_components/AddControlForm.tsx
@@ -0,0 +1,96 @@
+'use client'
+
+import React, { useState } from 'react'
+import { ControlType } from '@/lib/sdk'
+
+export function AddControlForm({
+ onSubmit,
+ onCancel,
+}: {
+ onSubmit: (data: { name: string; description: string; type: ControlType; category: string; owner: string }) => void
+ onCancel: () => void
+}) {
+ const [formData, setFormData] = useState({
+ name: '',
+ description: '',
+ type: 'TECHNICAL' as ControlType,
+ category: '',
+ owner: '',
+ })
+
+ return (
+
+
+
+
+
+ {control.code}
+
+
+ {control.displayType === 'preventive' ? 'Praeventiv' :
+ control.displayType === 'detective' ? 'Detektiv' : 'Korrektiv'}
+
+
+ {control.displayCategory === 'technical' ? 'Technisch' :
+ control.displayCategory === 'organizational' ? 'Organisatorisch' : 'Physisch'}
+
+
+
{control.name}
+
{control.description}
+
+
+
+
+
+
setShowEffectivenessSlider(!showEffectivenessSlider)}
+ >
+ Wirksamkeit
+ {control.effectivenessPercent}%
+
+
+
= 80 ? 'bg-green-500' :
+ control.effectivenessPercent >= 50 ? 'bg-yellow-500' : 'bg-red-500'
+ }`}
+ style={{ width: `${control.effectivenessPercent}%` }}
+ />
+
+ {showEffectivenessSlider && (
+
+ onEffectivenessChange(Number(e.target.value))}
+ className="w-full"
+ />
+
+ )}
+
+
+
+
+ Verantwortlich:
+ {control.owner || 'Nicht zugewiesen'}
+
+
+ Letzte Pruefung: {control.lastReview.toLocaleDateString('de-DE')}
+
+
+
+
+
+ {control.linkedRequirements.slice(0, 3).map(req => (
+
+ {req}
+
+ ))}
+ {control.linkedRequirements.length > 3 && (
+
+ +{control.linkedRequirements.length - 3}
+
+ )}
+
+
+ {statusLabels[control.displayStatus]}
+
+
+
+ {/* Linked Evidence */}
+ {control.linkedEvidence.length > 0 && (
+
+
Nachweise:
+
+ {control.linkedEvidence.map(ev => (
+
+ {ev.title}
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/controls/_components/FilterBar.tsx b/admin-compliance/app/sdk/controls/_components/FilterBar.tsx
new file mode 100644
index 0000000..f9fc0f3
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_components/FilterBar.tsx
@@ -0,0 +1,45 @@
+'use client'
+
+import React from 'react'
+
+const FILTER_OPTIONS = [
+ 'all', 'implemented', 'partial', 'not-implemented',
+ 'technical', 'organizational', 'preventive', 'detective',
+]
+
+function filterLabel(f: string): string {
+ return f === 'all' ? 'Alle' :
+ f === 'implemented' ? 'Implementiert' :
+ f === 'partial' ? 'Teilweise' :
+ f === 'not-implemented' ? 'Offen' :
+ f === 'technical' ? 'Technisch' :
+ f === 'organizational' ? 'Organisatorisch' :
+ f === 'preventive' ? 'Praeventiv' : 'Detektiv'
+}
+
+export function FilterBar({
+ filter,
+ onFilterChange,
+}: {
+ filter: string
+ onFilterChange: (f: string) => void
+}) {
+ return (
+
+ Filter:
+ {FILTER_OPTIONS.map(f => (
+
+ ))}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/controls/_components/LoadingSkeleton.tsx b/admin-compliance/app/sdk/controls/_components/LoadingSkeleton.tsx
new file mode 100644
index 0000000..3a3efbd
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_components/LoadingSkeleton.tsx
@@ -0,0 +1,22 @@
+'use client'
+
+import React from 'react'
+
+export function LoadingSkeleton() {
+ return (
+
+ {[1, 2, 3].map(i => (
+
+ ))}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/controls/_components/RAGPanel.tsx b/admin-compliance/app/sdk/controls/_components/RAGPanel.tsx
new file mode 100644
index 0000000..d1955a0
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_components/RAGPanel.tsx
@@ -0,0 +1,141 @@
+'use client'
+
+import React from 'react'
+import { RAGControlSuggestion } from '../_types'
+
+export function RAGPanel({
+ selectedRequirementId,
+ onSelectedRequirementIdChange,
+ requirements,
+ onSuggestControls,
+ ragLoading,
+ ragSuggestions,
+ onAddSuggestion,
+ onClose,
+}: {
+ selectedRequirementId: string
+ onSelectedRequirementIdChange: (v: string) => void
+ requirements: { id: string; title?: string }[]
+ onSuggestControls: () => void
+ ragLoading: boolean
+ ragSuggestions: RAGControlSuggestion[]
+ onAddSuggestion: (s: RAGControlSuggestion) => void
+ onClose: () => void
+}) {
+ return (
+
+
+
+
KI-Controls aus RAG vorschlagen
+
+ Geben Sie eine Anforderungs-ID ein. Das KI-System analysiert die Anforderung mit Hilfe des RAG-Corpus
+ und schlägt passende Controls vor.
+
+
+
+
+
+
+
onSelectedRequirementIdChange(e.target.value)}
+ placeholder="Anforderungs-UUID eingeben..."
+ className="flex-1 px-4 py-2 border border-purple-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white"
+ />
+ {requirements.length > 0 && (
+
+ )}
+
+
+
+ {/* Suggestions */}
+ {ragSuggestions.length > 0 && (
+
+
{ragSuggestions.length} Vorschläge gefunden:
+ {ragSuggestions.map((suggestion) => (
+
+
+
+
+
+ {suggestion.control_id}
+
+
+ {suggestion.domain}
+
+
+ Konfidenz: {Math.round(suggestion.confidence_score * 100)}%
+
+
+
{suggestion.title}
+
{suggestion.description}
+ {suggestion.pass_criteria && (
+
+ Erfolgskriterium: {suggestion.pass_criteria}
+
+ )}
+ {suggestion.is_automated && (
+
+ Automatisierbar {suggestion.automation_tool ? `(${suggestion.automation_tool})` : ''}
+
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+ {!ragLoading && ragSuggestions.length === 0 && selectedRequirementId && (
+
+ Klicken Sie auf "Vorschläge generieren", um KI-Controls abzurufen.
+
+ )}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/controls/_components/StatsCards.tsx b/admin-compliance/app/sdk/controls/_components/StatsCards.tsx
new file mode 100644
index 0000000..5c1e098
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_components/StatsCards.tsx
@@ -0,0 +1,36 @@
+'use client'
+
+import React from 'react'
+
+export function StatsCards({
+ total,
+ implementedCount,
+ avgEffectiveness,
+ partialCount,
+}: {
+ total: number
+ implementedCount: number
+ avgEffectiveness: number
+ partialCount: number
+}) {
+ return (
+
+
+
+
Implementiert
+
{implementedCount}
+
+
+
Durchschn. Wirksamkeit
+
{avgEffectiveness}%
+
+
+
Teilweise
+
{partialCount}
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/controls/_hooks/useControlsData.ts b/admin-compliance/app/sdk/controls/_hooks/useControlsData.ts
new file mode 100644
index 0000000..cf58e9c
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_hooks/useControlsData.ts
@@ -0,0 +1,138 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { useSDK, Control as SDKControl, ControlType, ImplementationStatus } from '@/lib/sdk'
+
+export function useControlsData() {
+ const { state, dispatch } = useSDK()
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState
(null)
+
+ // Track effectiveness locally as it's not in the SDK state type
+ const [effectivenessMap, setEffectivenessMap] = useState>({})
+ // Track linked evidence per control
+ const [evidenceMap, setEvidenceMap] = useState>({})
+
+ 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 = {}
+ 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',
+ })
+ }
+ setEvidenceMap(map)
+ }
+ }
+ } catch {
+ // Silently fail
+ }
+ }
+
+ // Fetch controls from backend on mount
+ 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) => ({
+ 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 handleStatusChange = async (controlId: string, status: ImplementationStatus) => {
+ dispatch({
+ type: 'UPDATE_CONTROL',
+ payload: { id: controlId, data: { implementationStatus: status } },
+ })
+
+ try {
+ await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ implementation_status: status }),
+ })
+ } catch {
+ // Silently fail — SDK state is already updated
+ }
+ }
+
+ 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 — local state is already updated
+ }
+ }
+
+ 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 })
+ }
+
+ return {
+ state,
+ dispatch,
+ loading,
+ error,
+ setError,
+ effectivenessMap,
+ evidenceMap,
+ handleStatusChange,
+ handleEffectivenessChange,
+ handleAddControl,
+ }
+}
diff --git a/admin-compliance/app/sdk/controls/_hooks/useRAGSuggestions.ts b/admin-compliance/app/sdk/controls/_hooks/useRAGSuggestions.ts
new file mode 100644
index 0000000..c7e307d
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_hooks/useRAGSuggestions.ts
@@ -0,0 +1,68 @@
+'use client'
+
+import { useState } from 'react'
+import { useSDK } from '@/lib/sdk'
+import { RAGControlSuggestion } from '../_types'
+
+export function useRAGSuggestions(setError: (e: string | null) => void) {
+ const { dispatch } = useSDK()
+ const [ragLoading, setRagLoading] = useState(false)
+ const [ragSuggestions, setRagSuggestions] = useState([])
+ const [showRagPanel, setShowRagPanel] = useState(false)
+ const [selectedRequirementId, setSelectedRequirementId] = useState('')
+
+ 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-Vorschläge fehlgeschlagen: ${e instanceof Error ? e.message : 'Unbekannter Fehler'}`)
+ } finally {
+ setRagLoading(false)
+ }
+ }
+
+ const addSuggestedControl = (suggestion: RAGControlSuggestion) => {
+ const newControl: import('@/lib/sdk').Control = {
+ 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 })
+ setRagSuggestions(prev => prev.filter(s => s.control_id !== suggestion.control_id))
+ }
+
+ return {
+ ragLoading,
+ ragSuggestions,
+ showRagPanel,
+ setShowRagPanel,
+ selectedRequirementId,
+ setSelectedRequirementId,
+ suggestControlsFromRAG,
+ addSuggestedControl,
+ }
+}
diff --git a/admin-compliance/app/sdk/controls/_types.ts b/admin-compliance/app/sdk/controls/_types.ts
new file mode 100644
index 0000000..08ea293
--- /dev/null
+++ b/admin-compliance/app/sdk/controls/_types.ts
@@ -0,0 +1,56 @@
+import { ControlType, ImplementationStatus } from '@/lib/sdk'
+
+export type DisplayControlType = 'preventive' | 'detective' | 'corrective'
+export type DisplayCategory = 'technical' | 'organizational' | 'physical'
+export type DisplayStatus = 'implemented' | 'partial' | 'planned' | 'not-implemented'
+
+export interface DisplayControl {
+ id: string
+ name: string
+ description: string
+ type: ControlType
+ category: string
+ implementationStatus: ImplementationStatus
+ evidence: string[]
+ owner: string | null
+ dueDate: Date | null
+ code: string
+ displayType: DisplayControlType
+ displayCategory: DisplayCategory
+ displayStatus: DisplayStatus
+ effectivenessPercent: number
+ linkedRequirements: string[]
+ linkedEvidence: { id: string; title: string; status: string }[]
+ lastReview: Date
+}
+
+export interface RAGControlSuggestion {
+ control_id: string
+ domain: string
+ title: string
+ description: string
+ pass_criteria: string
+ implementation_guidance?: string
+ is_automated: boolean
+ automation_tool?: string
+ priority: number
+ confidence_score: number
+}
+
+export function mapControlTypeToDisplay(type: ControlType): DisplayCategory {
+ switch (type) {
+ case 'TECHNICAL': return 'technical'
+ case 'ORGANIZATIONAL': return 'organizational'
+ case 'PHYSICAL': return 'physical'
+ default: return 'technical'
+ }
+}
+
+export function mapStatusToDisplay(status: ImplementationStatus): DisplayStatus {
+ switch (status) {
+ case 'IMPLEMENTED': return 'implemented'
+ case 'PARTIAL': return 'partial'
+ case 'NOT_IMPLEMENTED': return 'not-implemented'
+ default: return 'not-implemented'
+ }
+}
diff --git a/admin-compliance/app/sdk/controls/page.tsx b/admin-compliance/app/sdk/controls/page.tsx
index fb9deb1..55f8267 100644
--- a/admin-compliance/app/sdk/controls/page.tsx
+++ b/admin-compliance/app/sdk/controls/page.tsx
@@ -1,446 +1,50 @@
'use client'
-import React, { useState, useEffect } from 'react'
+import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
-import { useSDK, Control as SDKControl, ControlType, ImplementationStatus } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
-
-// =============================================================================
-// TYPES
-// =============================================================================
-
-type DisplayControlType = 'preventive' | 'detective' | 'corrective'
-type DisplayCategory = 'technical' | 'organizational' | 'physical'
-type DisplayStatus = 'implemented' | 'partial' | 'planned' | 'not-implemented'
-
-interface DisplayControl {
- id: string
- name: string
- description: string
- type: ControlType
- category: string
- implementationStatus: ImplementationStatus
- evidence: string[]
- owner: string | null
- dueDate: Date | null
- code: string
- displayType: DisplayControlType
- displayCategory: DisplayCategory
- displayStatus: DisplayStatus
- effectivenessPercent: number
- linkedRequirements: string[]
- linkedEvidence: { id: string; title: string; status: string }[]
- lastReview: Date
-}
-
-// =============================================================================
-// HELPER FUNCTIONS
-// =============================================================================
-
-function mapControlTypeToDisplay(type: ControlType): DisplayCategory {
- switch (type) {
- case 'TECHNICAL': return 'technical'
- case 'ORGANIZATIONAL': return 'organizational'
- case 'PHYSICAL': return 'physical'
- default: return 'technical'
- }
-}
-
-function mapStatusToDisplay(status: ImplementationStatus): DisplayStatus {
- switch (status) {
- case 'IMPLEMENTED': return 'implemented'
- case 'PARTIAL': return 'partial'
- case 'NOT_IMPLEMENTED': return 'not-implemented'
- default: return 'not-implemented'
- }
-}
-
-
-// =============================================================================
-// COMPONENTS
-// =============================================================================
-
-function ControlCard({
- control,
- onStatusChange,
- onEffectivenessChange,
- onLinkEvidence,
-}: {
- control: DisplayControl
- onStatusChange: (status: ImplementationStatus) => void
- onEffectivenessChange: (effectivenessPercent: number) => void
- onLinkEvidence: () => void
-}) {
- const [showEffectivenessSlider, setShowEffectivenessSlider] = useState(false)
-
- const typeColors = {
- preventive: 'bg-blue-100 text-blue-700',
- detective: 'bg-purple-100 text-purple-700',
- corrective: 'bg-orange-100 text-orange-700',
- }
-
- const categoryColors = {
- technical: 'bg-green-100 text-green-700',
- organizational: 'bg-yellow-100 text-yellow-700',
- physical: 'bg-gray-100 text-gray-700',
- }
-
- const statusColors = {
- implemented: 'border-green-200 bg-green-50',
- partial: 'border-yellow-200 bg-yellow-50',
- planned: 'border-blue-200 bg-blue-50',
- 'not-implemented': 'border-red-200 bg-red-50',
- }
-
- const statusLabels = {
- implemented: 'Implementiert',
- partial: 'Teilweise',
- planned: 'Geplant',
- 'not-implemented': 'Nicht implementiert',
- }
-
- return (
-
-
-
-
-
- {control.code}
-
-
- {control.displayType === 'preventive' ? 'Praeventiv' :
- control.displayType === 'detective' ? 'Detektiv' : 'Korrektiv'}
-
-
- {control.displayCategory === 'technical' ? 'Technisch' :
- control.displayCategory === 'organizational' ? 'Organisatorisch' : 'Physisch'}
-
-
-
{control.name}
-
{control.description}
-
-
-
-
-
-
setShowEffectivenessSlider(!showEffectivenessSlider)}
- >
- Wirksamkeit
- {control.effectivenessPercent}%
-
-
-
= 80 ? 'bg-green-500' :
- control.effectivenessPercent >= 50 ? 'bg-yellow-500' : 'bg-red-500'
- }`}
- style={{ width: `${control.effectivenessPercent}%` }}
- />
-
- {showEffectivenessSlider && (
-
- onEffectivenessChange(Number(e.target.value))}
- className="w-full"
- />
-
- )}
-
-
-
-
- Verantwortlich:
- {control.owner || 'Nicht zugewiesen'}
-
-
- Letzte Pruefung: {control.lastReview.toLocaleDateString('de-DE')}
-
-
-
-
-
- {control.linkedRequirements.slice(0, 3).map(req => (
-
- {req}
-
- ))}
- {control.linkedRequirements.length > 3 && (
-
- +{control.linkedRequirements.length - 3}
-
- )}
-
-
- {statusLabels[control.displayStatus]}
-
-
-
- {/* Linked Evidence */}
- {control.linkedEvidence.length > 0 && (
-
-
Nachweise:
-
- {control.linkedEvidence.map(ev => (
-
- {ev.title}
-
- ))}
-
-
- )}
-
-
-
-
-
- )
-}
-
-function AddControlForm({
- onSubmit,
- onCancel,
-}: {
- onSubmit: (data: { name: string; description: string; type: ControlType; category: string; owner: string }) => void
- onCancel: () => void
-}) {
- const [formData, setFormData] = useState({
- name: '',
- description: '',
- type: 'TECHNICAL' as ControlType,
- category: '',
- owner: '',
- })
-
- return (
-
-
Neue Kontrolle
-
-
-
- setFormData({ ...formData, name: e.target.value })}
- placeholder="z.B. Zugriffskontrolle"
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
- />
-
-
-
-
-
-
-
-
-
-
-
- setFormData({ ...formData, category: e.target.value })}
- placeholder="z.B. Zutrittskontrolle"
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
- />
-
-
-
- setFormData({ ...formData, owner: e.target.value })}
- placeholder="z.B. IT Security"
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
- />
-
-
-
-
-
-
-
-
- )
-}
-
-function LoadingSkeleton() {
- return (
-
- {[1, 2, 3].map(i => (
-
- ))}
-
- )
-}
-
-// =============================================================================
-// MAIN PAGE
-// =============================================================================
-
-// =============================================================================
-// RAG SUGGESTION TYPES
-// =============================================================================
-
-interface RAGControlSuggestion {
- control_id: string
- domain: string
- title: string
- description: string
- pass_criteria: string
- implementation_guidance?: string
- is_automated: boolean
- automation_tool?: string
- priority: number
- confidence_score: number
-}
-
-// =============================================================================
-// MAIN PAGE
-// =============================================================================
+import {
+ DisplayControl,
+ DisplayControlType,
+ mapControlTypeToDisplay,
+ mapStatusToDisplay,
+} from './_types'
+import { ControlCard } from './_components/ControlCard'
+import { AddControlForm } from './_components/AddControlForm'
+import { LoadingSkeleton } from './_components/LoadingSkeleton'
+import { StatsCards } from './_components/StatsCards'
+import { FilterBar } from './_components/FilterBar'
+import { RAGPanel } from './_components/RAGPanel'
+import { useControlsData } from './_hooks/useControlsData'
+import { useRAGSuggestions } from './_hooks/useRAGSuggestions'
export default function ControlsPage() {
- const { state, dispatch } = useSDK()
const router = useRouter()
const [filter, setFilter] = useState
('all')
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
const [showAddForm, setShowAddForm] = useState(false)
- // RAG suggestion state
- const [ragLoading, setRagLoading] = useState(false)
- const [ragSuggestions, setRagSuggestions] = useState([])
- const [showRagPanel, setShowRagPanel] = useState(false)
- const [selectedRequirementId, setSelectedRequirementId] = useState('')
+ const {
+ state,
+ loading,
+ error,
+ setError,
+ effectivenessMap,
+ evidenceMap,
+ handleStatusChange,
+ handleEffectivenessChange,
+ handleAddControl,
+ } = useControlsData()
- // Track effectiveness locally as it's not in the SDK state type
- const [effectivenessMap, setEffectivenessMap] = useState>({})
- // Track linked evidence per control
- const [evidenceMap, setEvidenceMap] = useState>({})
-
- 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 = {}
- 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',
- })
- }
- setEvidenceMap(map)
- }
- }
- } catch {
- // Silently fail
- }
- }
-
- // Fetch controls from backend on mount
- 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) => ({
- 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)
- // Fetch evidence for all controls
- 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 {
+ ragLoading,
+ ragSuggestions,
+ showRagPanel,
+ setShowRagPanel,
+ selectedRequirementId,
+ setSelectedRequirementId,
+ suggestControlsFromRAG,
+ addSuggestedControl,
+ } = useRAGSuggestions(setError)
// Convert SDK controls to display controls
const displayControls: DisplayControl[] = state.controls.map(ctrl => {
@@ -483,100 +87,6 @@ export default function ControlsPage() {
: 0
const partialCount = displayControls.filter(c => c.displayStatus === 'partial').length
- const handleStatusChange = async (controlId: string, status: ImplementationStatus) => {
- dispatch({
- type: 'UPDATE_CONTROL',
- payload: { id: controlId, data: { implementationStatus: status } },
- })
-
- try {
- await fetch(`/api/sdk/v1/compliance/controls/${controlId}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ implementation_status: status }),
- })
- } catch {
- // Silently fail — SDK state is already updated
- }
- }
-
- const handleEffectivenessChange = async (controlId: string, effectiveness: number) => {
- setEffectivenessMap(prev => ({ ...prev, [controlId]: effectiveness }))
-
- // Persist to backend
- 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 — local state is already updated
- }
- }
-
- 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 })
- setShowAddForm(false)
- }
-
- 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-Vorschläge fehlgeschlagen: ${e instanceof Error ? e.message : 'Unbekannter Fehler'}`)
- } finally {
- setRagLoading(false)
- }
- }
-
- const addSuggestedControl = (suggestion: RAGControlSuggestion) => {
- const newControl: import('@/lib/sdk').Control = {
- 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 })
- // Remove from suggestions after adding
- setRagSuggestions(prev => prev.filter(s => s.control_id !== suggestion.control_id))
- }
-
const stepInfo = STEP_EXPLANATIONS['controls']
return (
@@ -614,127 +124,23 @@ export default function ControlsPage() {
{/* Add Form */}
{showAddForm && (
{ handleAddControl(data); setShowAddForm(false) }}
onCancel={() => setShowAddForm(false)}
/>
)}
{/* RAG Controls Panel */}
{showRagPanel && (
-
-
-
-
KI-Controls aus RAG vorschlagen
-
- Geben Sie eine Anforderungs-ID ein. Das KI-System analysiert die Anforderung mit Hilfe des RAG-Corpus
- und schlägt passende Controls vor.
-
-
-
-
-
-
-
setSelectedRequirementId(e.target.value)}
- placeholder="Anforderungs-UUID eingeben..."
- className="flex-1 px-4 py-2 border border-purple-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white"
- />
- {state.requirements.length > 0 && (
-
- )}
-
-
-
- {/* Suggestions */}
- {ragSuggestions.length > 0 && (
-
-
{ragSuggestions.length} Vorschläge gefunden:
- {ragSuggestions.map((suggestion) => (
-
-
-
-
-
- {suggestion.control_id}
-
-
- {suggestion.domain}
-
-
- Konfidenz: {Math.round(suggestion.confidence_score * 100)}%
-
-
-
{suggestion.title}
-
{suggestion.description}
- {suggestion.pass_criteria && (
-
- Erfolgskriterium: {suggestion.pass_criteria}
-
- )}
- {suggestion.is_automated && (
-
- Automatisierbar {suggestion.automation_tool ? `(${suggestion.automation_tool})` : ''}
-
- )}
-
-
-
-
- ))}
-
- )}
-
- {!ragLoading && ragSuggestions.length === 0 && selectedRequirementId && (
-
- Klicken Sie auf "Vorschläge generieren", um KI-Controls abzurufen.
-
- )}
-
+ setShowRagPanel(false)}
+ />
)}
{/* Error Banner */}
@@ -762,49 +168,14 @@ export default function ControlsPage() {
)}
- {/* Stats */}
-
-
-
Gesamt
-
{displayControls.length}
-
-
-
Implementiert
-
{implementedCount}
-
-
-
Durchschn. Wirksamkeit
-
{avgEffectiveness}%
-
-
-
Teilweise
-
{partialCount}
-
-
+
- {/* Filter */}
-
- Filter:
- {['all', 'implemented', 'partial', 'not-implemented', 'technical', 'organizational', 'preventive', 'detective'].map(f => (
-
- ))}
-
+
{/* Loading State */}
{loading && }