feat(gci): add Gesamt-Compliance-Index scoring engine and dashboard
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 28s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 17s

Implements the 4-level GCI scoring model (Module -> Risk-Weighted -> Regulation Area -> Final GCI)
with DSGVO, NIS2, ISO 27001, and EU AI Act integration.

Backend:
- 9 Go files: engine, models, weights, validity, NIS2 roles/scoring, ISO mapping/gap-analysis, mock data
- GCI handlers with 13 API endpoints under /sdk/v1/gci/
- Routes registered in main.go

Frontend:
- TypeScript types, API client, Next.js API proxy
- Dashboard page with 6 tabs (Overview, Breakdown, NIS2, ISO 27001, Matrix, Audit Trail)
- Sidebar navigation entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-15 22:20:17 +01:00
parent 2d909a8f8e
commit 7a09086930
16 changed files with 2703 additions and 0 deletions

View File

@@ -0,0 +1,693 @@
'use client'
import React, { useState, useEffect, useCallback } from 'react'
import {
GCIResult,
GCIBreakdown,
GCIHistoryResponse,
GCIMatrixResponse,
NIS2Score,
ISOGapAnalysis,
WeightProfile,
MaturityLevel,
MATURITY_INFO,
getScoreColor,
getScoreRingColor,
} from '@/lib/sdk/gci/types'
import {
getGCIScore,
getGCIBreakdown,
getGCIHistory,
getGCIMatrix,
getNIS2Score,
getISOGapAnalysis,
getWeightProfiles,
} from '@/lib/sdk/gci/api'
// =============================================================================
// TYPES
// =============================================================================
type TabId = 'overview' | 'breakdown' | 'nis2' | 'iso' | 'matrix' | 'audit'
interface Tab {
id: TabId
label: string
}
const TABS: Tab[] = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'breakdown', label: 'Breakdown' },
{ id: 'nis2', label: 'NIS2' },
{ id: 'iso', label: 'ISO 27001' },
{ id: 'matrix', label: 'Matrix' },
{ id: 'audit', label: 'Audit Trail' },
]
// =============================================================================
// HELPER COMPONENTS
// =============================================================================
function TabNavigation({ tabs, activeTab, onTabChange }: { tabs: Tab[]; activeTab: TabId; onTabChange: (tab: TabId) => void }) {
return (
<div className="border-b border-gray-200">
<nav className="flex gap-1 -mb-px overflow-x-auto" aria-label="Tabs">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${
activeTab === tab.id
? 'border-purple-600 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
)
}
function ScoreCircle({ score, size = 144, label }: { score: number; size?: number; label?: string }) {
const radius = (size / 2) - 12
const circumference = 2 * Math.PI * radius
const strokeDashoffset = circumference - (score / 100) * circumference
return (
<div className="relative flex flex-col items-center">
<svg className="-rotate-90" width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<circle cx={size/2} cy={size/2} r={radius} stroke="#e5e7eb" strokeWidth="8" fill="none" />
<circle
cx={size/2} cy={size/2} r={radius}
stroke={getScoreRingColor(score)}
strokeWidth="8" fill="none"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={strokeDashoffset}
className="transition-all duration-1000"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className={`text-3xl font-bold ${getScoreColor(score)}`}>{score.toFixed(1)}</span>
{label && <span className="text-xs text-gray-500 mt-1">{label}</span>}
</div>
</div>
)
}
function MaturityBadge({ level }: { level: MaturityLevel }) {
const info = MATURITY_INFO[level] || MATURITY_INFO.HIGH_RISK
return (
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${info.bgColor} ${info.color} border ${info.borderColor}`}>
{info.label}
</span>
)
}
function AreaScoreBar({ name, score, weight }: { name: string; score: number; weight: number }) {
return (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="font-medium text-gray-700">{name}</span>
<span className={`font-semibold ${getScoreColor(score)}`}>{score.toFixed(1)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="h-3 rounded-full transition-all duration-700"
style={{ width: `${Math.min(score, 100)}%`, backgroundColor: getScoreRingColor(score) }}
/>
</div>
<div className="text-xs text-gray-400">Gewichtung: {(weight * 100).toFixed(0)}%</div>
</div>
)
}
function LoadingSpinner() {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
</div>
)
}
function ErrorMessage({ message, onRetry }: { message: string; onRetry?: () => void }) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
<p>{message}</p>
{onRetry && (
<button onClick={onRetry} className="mt-2 text-sm underline hover:no-underline">
Erneut versuchen
</button>
)}
</div>
)
}
// =============================================================================
// TAB: OVERVIEW
// =============================================================================
function OverviewTab({ gci, history, profiles, selectedProfile, onProfileChange }: {
gci: GCIResult
history: GCIHistoryResponse | null
profiles: WeightProfile[]
selectedProfile: string
onProfileChange: (p: string) => void
}) {
return (
<div className="space-y-6">
{/* Profile Selector */}
{profiles.length > 0 && (
<div className="flex items-center gap-3">
<label className="text-sm font-medium text-gray-700">Gewichtungsprofil:</label>
<select
value={selectedProfile}
onChange={e => onProfileChange(e.target.value)}
className="rounded-md border-gray-300 shadow-sm text-sm focus:border-purple-500 focus:ring-purple-500"
>
{profiles.map(p => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</div>
)}
{/* Main Score */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex flex-col md:flex-row items-center gap-8">
<ScoreCircle score={gci.gci_score} label="GCI Score" />
<div className="flex-1 space-y-4">
<div>
<h3 className="text-lg font-semibold text-gray-900">Gesamt-Compliance-Index</h3>
<div className="flex items-center gap-3 mt-2">
<MaturityBadge level={gci.maturity_level} />
<span className="text-sm text-gray-500">
Berechnet: {new Date(gci.calculated_at).toLocaleString('de-DE')}
</span>
</div>
</div>
<p className="text-sm text-gray-600">
{MATURITY_INFO[gci.maturity_level]?.description || ''}
</p>
</div>
</div>
</div>
{/* Area Scores */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Regulierungsbereiche</h3>
<div className="space-y-4">
{gci.area_scores.map(area => (
<AreaScoreBar
key={area.regulation_id}
name={area.regulation_name}
score={area.score}
weight={area.weight}
/>
))}
</div>
</div>
{/* History Chart (simplified) */}
{history && history.snapshots.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Verlauf</h3>
<div className="flex items-end gap-2 h-32">
{history.snapshots.map((snap, i) => (
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<span className="text-xs text-gray-500">{snap.score.toFixed(0)}</span>
<div
className="w-full rounded-t transition-all duration-500"
style={{
height: `${(snap.score / 100) * 100}%`,
backgroundColor: getScoreRingColor(snap.score),
minHeight: '4px',
}}
/>
<span className="text-[10px] text-gray-400">
{new Date(snap.calculated_at).toLocaleDateString('de-DE', { month: 'short' })}
</span>
</div>
))}
</div>
</div>
)}
{/* Adjustments */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="text-sm text-gray-500">Kritikalitaets-Multiplikator</div>
<div className="text-2xl font-bold text-gray-900">{gci.criticality_multiplier.toFixed(2)}x</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-4">
<div className="text-sm text-gray-500">Incident-Korrektur</div>
<div className={`text-2xl font-bold ${gci.incident_adjustment < 0 ? 'text-red-600' : 'text-green-600'}`}>
{gci.incident_adjustment > 0 ? '+' : ''}{gci.incident_adjustment.toFixed(1)}
</div>
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: BREAKDOWN
// =============================================================================
function BreakdownTab({ breakdown }: { breakdown: GCIBreakdown | null; loading: boolean }) {
if (!breakdown) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* Level 1: Modules */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Level 1: Modul-Scores</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 pr-4 font-medium text-gray-600">Modul</th>
<th className="text-left py-2 pr-4 font-medium text-gray-600">Kategorie</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Zugewiesen</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Abgeschlossen</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Raw Score</th>
<th className="text-right py-2 pr-4 font-medium text-gray-600">Validitaet</th>
<th className="text-right py-2 font-medium text-gray-600">Final</th>
</tr>
</thead>
<tbody>
{breakdown.level1_modules.map(m => (
<tr key={m.module_id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-2 pr-4 font-medium text-gray-900">{m.module_name}</td>
<td className="py-2 pr-4">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700">
{m.category}
</span>
</td>
<td className="py-2 pr-4 text-right text-gray-600">{m.assigned}</td>
<td className="py-2 pr-4 text-right text-gray-600">{m.completed}</td>
<td className="py-2 pr-4 text-right text-gray-600">{(m.raw_score * 100).toFixed(1)}%</td>
<td className="py-2 pr-4 text-right text-gray-600">{(m.validity_factor * 100).toFixed(0)}%</td>
<td className={`py-2 text-right font-semibold ${getScoreColor(m.final_score * 100)}`}>
{(m.final_score * 100).toFixed(1)}%
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Level 2: Areas */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Level 2: Regulierungsbereiche (risikogewichtet)</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{breakdown.level2_areas.map(area => (
<div key={area.area_id} className="border border-gray-200 rounded-lg p-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium text-gray-900">{area.area_name}</h4>
<span className={`text-lg font-bold ${getScoreColor(area.area_score)}`}>
{area.area_score.toFixed(1)}%
</span>
</div>
<div className="space-y-1">
{area.modules.map(m => (
<div key={m.module_id} className="flex justify-between text-xs text-gray-500">
<span>{m.module_name}</span>
<span>{(m.final_score * 100).toFixed(0)}% (w:{m.risk_weight.toFixed(1)})</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: NIS2
// =============================================================================
function NIS2Tab({ nis2 }: { nis2: NIS2Score | null }) {
if (!nis2) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* NIS2 Overall */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center gap-6">
<ScoreCircle score={nis2.overall_score} size={120} label="NIS2" />
<div>
<h3 className="text-lg font-semibold text-gray-900">NIS2 Compliance Score</h3>
<p className="text-sm text-gray-500 mt-1">
Network and Information Security Directive 2 (EU 2022/2555)
</p>
</div>
</div>
</div>
{/* NIS2 Areas */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">NIS2 Bereiche</h3>
<div className="space-y-3">
{nis2.areas.map(area => (
<AreaScoreBar key={area.area_id} name={area.area_name} score={area.score} weight={area.weight} />
))}
</div>
</div>
{/* NIS2 Roles */}
{nis2.role_scores && nis2.role_scores.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Rollen-Compliance</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{nis2.role_scores.map(role => (
<div key={role.role_id} className="border border-gray-200 rounded-lg p-3">
<div className="font-medium text-gray-900 text-sm">{role.role_name}</div>
<div className="flex items-center justify-between mt-2">
<span className={`text-lg font-bold ${getScoreColor(role.completion_rate * 100)}`}>
{(role.completion_rate * 100).toFixed(0)}%
</span>
<span className="text-xs text-gray-500">
{role.modules_completed}/{role.modules_required} Module
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5 mt-2">
<div
className="h-1.5 rounded-full"
style={{
width: `${Math.min(role.completion_rate * 100, 100)}%`,
backgroundColor: getScoreRingColor(role.completion_rate * 100),
}}
/>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
// =============================================================================
// TAB: ISO 27001
// =============================================================================
function ISOTab({ iso }: { iso: ISOGapAnalysis | null }) {
if (!iso) return <LoadingSpinner />
return (
<div className="space-y-6">
{/* Coverage Overview */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center gap-6">
<ScoreCircle score={iso.coverage_percent} size={120} label="Abdeckung" />
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">ISO 27001:2022 Gap-Analyse</h3>
<div className="grid grid-cols-3 gap-4 mt-3">
<div className="text-center">
<div className="text-2xl font-bold text-green-600">{iso.covered_full}</div>
<div className="text-xs text-gray-500">Voll abgedeckt</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-yellow-600">{iso.covered_partial}</div>
<div className="text-xs text-gray-500">Teilweise</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-600">{iso.not_covered}</div>
<div className="text-xs text-gray-500">Nicht abgedeckt</div>
</div>
</div>
</div>
</div>
</div>
{/* Category Summaries */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Kategorien</h3>
<div className="space-y-3">
{iso.category_summaries.map(cat => {
const coveragePercent = cat.total_controls > 0
? ((cat.covered_full + cat.covered_partial * 0.5) / cat.total_controls) * 100
: 0
return (
<div key={cat.category_id} className="space-y-1">
<div className="flex justify-between text-sm">
<span className="font-medium text-gray-700">{cat.category_id}: {cat.category_name}</span>
<span className="text-gray-500">
{cat.covered_full}/{cat.total_controls} Controls
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3 flex overflow-hidden">
<div className="h-3 bg-green-500" style={{ width: `${(cat.covered_full / cat.total_controls) * 100}%` }} />
<div className="h-3 bg-yellow-500" style={{ width: `${(cat.covered_partial / cat.total_controls) * 100}%` }} />
</div>
</div>
)
})}
</div>
</div>
{/* Gaps */}
{iso.gaps && iso.gaps.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">
Offene Gaps ({iso.gaps.length})
</h3>
<div className="space-y-2 max-h-96 overflow-y-auto">
{iso.gaps.map(gap => (
<div key={gap.control_id} className="flex items-start gap-3 p-3 border border-gray-100 rounded-lg hover:bg-gray-50">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
gap.priority === 'high' ? 'bg-red-100 text-red-700' :
gap.priority === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-gray-100 text-gray-700'
}`}>
{gap.priority}
</span>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900">{gap.control_id}: {gap.control_name}</div>
<div className="text-xs text-gray-500 mt-0.5">{gap.recommendation}</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}
// =============================================================================
// TAB: MATRIX
// =============================================================================
function MatrixTab({ matrix }: { matrix: GCIMatrixResponse | null }) {
if (!matrix || !matrix.matrix) return <LoadingSpinner />
const regulations = matrix.matrix.length > 0 ? Object.keys(matrix.matrix[0].regulations) : []
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">Compliance-Matrix (Rollen x Regulierungen)</h3>
<div className="overflow-x-auto">
<table className="min-w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-2 pr-4 font-medium text-gray-600">Rolle</th>
{regulations.map(r => (
<th key={r} className="text-center py-2 px-3 font-medium text-gray-600 uppercase">{r}</th>
))}
<th className="text-center py-2 px-3 font-medium text-gray-600">Gesamt</th>
<th className="text-center py-2 px-3 font-medium text-gray-600">Module</th>
</tr>
</thead>
<tbody>
{matrix.matrix.map(entry => (
<tr key={entry.role} className="border-b border-gray-100 hover:bg-gray-50">
<td className="py-2 pr-4 font-medium text-gray-900">{entry.role_name}</td>
{regulations.map(r => (
<td key={r} className="py-2 px-3 text-center">
<span className={`font-semibold ${getScoreColor(entry.regulations[r])}`}>
{entry.regulations[r].toFixed(0)}%
</span>
</td>
))}
<td className="py-2 px-3 text-center">
<span className={`font-bold ${getScoreColor(entry.overall_score)}`}>
{entry.overall_score.toFixed(0)}%
</span>
</td>
<td className="py-2 px-3 text-center text-gray-500">
{entry.completed_modules}/{entry.required_modules}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
// =============================================================================
// TAB: AUDIT TRAIL
// =============================================================================
function AuditTab({ gci }: { gci: GCIResult }) {
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-base font-semibold text-gray-900 mb-4">
Audit Trail - Berechnung GCI {gci.gci_score.toFixed(1)}
</h3>
<p className="text-sm text-gray-500 mb-4">
Jeder Schritt der GCI-Berechnung ist nachvollziehbar und prueffaehig dokumentiert.
</p>
<div className="space-y-2">
{gci.audit_trail.map((entry, i) => (
<div key={i} className="flex items-start gap-3 p-3 border border-gray-100 rounded-lg">
<div className={`flex-shrink-0 w-2 h-2 rounded-full mt-1.5 ${
entry.impact === 'positive' ? 'bg-green-500' :
entry.impact === 'negative' ? 'bg-red-500' :
'bg-gray-400'
}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-900">{entry.factor}</span>
<span className={`text-sm font-mono ${
entry.impact === 'positive' ? 'text-green-600' :
entry.impact === 'negative' ? 'text-red-600' :
'text-gray-600'
}`}>
{entry.value > 0 ? '+' : ''}{entry.value.toFixed(2)}
</span>
</div>
<p className="text-xs text-gray-500 mt-0.5">{entry.description}</p>
</div>
</div>
))}
</div>
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function GCIPage() {
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [gci, setGCI] = useState<GCIResult | null>(null)
const [breakdown, setBreakdown] = useState<GCIBreakdown | null>(null)
const [history, setHistory] = useState<GCIHistoryResponse | null>(null)
const [matrix, setMatrix] = useState<GCIMatrixResponse | null>(null)
const [nis2, setNIS2] = useState<NIS2Score | null>(null)
const [iso, setISO] = useState<ISOGapAnalysis | null>(null)
const [profiles, setProfiles] = useState<WeightProfile[]>([])
const [selectedProfile, setSelectedProfile] = useState('default')
const loadData = useCallback(async (profile?: string) => {
setLoading(true)
setError(null)
try {
const [gciRes, historyRes, profilesRes] = await Promise.all([
getGCIScore(profile),
getGCIHistory(),
getWeightProfiles(),
])
setGCI(gciRes)
setHistory(historyRes)
setProfiles(profilesRes.profiles || [])
} catch (err: any) {
setError(err.message || 'Fehler beim Laden der GCI-Daten')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
loadData(selectedProfile)
}, [selectedProfile, loadData])
// Lazy-load tab data
useEffect(() => {
if (activeTab === 'breakdown' && !breakdown && gci) {
getGCIBreakdown(selectedProfile).then(setBreakdown).catch(() => {})
}
if (activeTab === 'nis2' && !nis2) {
getNIS2Score().then(setNIS2).catch(() => {})
}
if (activeTab === 'iso' && !iso) {
getISOGapAnalysis().then(setISO).catch(() => {})
}
if (activeTab === 'matrix' && !matrix) {
getGCIMatrix().then(setMatrix).catch(() => {})
}
}, [activeTab, breakdown, nis2, iso, matrix, gci, selectedProfile])
const handleProfileChange = (profile: string) => {
setSelectedProfile(profile)
setBreakdown(null) // reset breakdown to reload
}
return (
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Gesamt-Compliance-Index (GCI)</h1>
<p className="text-sm text-gray-500 mt-1">
4-stufiges, mathematisch fundiertes Compliance-Scoring
</p>
</div>
<button
onClick={() => loadData(selectedProfile)}
disabled={loading}
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700 disabled:opacity-50 transition-colors"
>
{loading ? 'Lade...' : 'Aktualisieren'}
</button>
</div>
{/* Tabs */}
<TabNavigation tabs={TABS} activeTab={activeTab} onTabChange={setActiveTab} />
{/* Content */}
{error && <ErrorMessage message={error} onRetry={() => loadData(selectedProfile)} />}
{loading && !gci ? (
<LoadingSpinner />
) : gci ? (
<div className="pb-8">
{activeTab === 'overview' && (
<OverviewTab
gci={gci}
history={history}
profiles={profiles}
selectedProfile={selectedProfile}
onProfileChange={handleProfileChange}
/>
)}
{activeTab === 'breakdown' && (
<BreakdownTab breakdown={breakdown} loading={!breakdown} />
)}
{activeTab === 'nis2' && <NIS2Tab nis2={nis2} />}
{activeTab === 'iso' && <ISOTab iso={iso} />}
{activeTab === 'matrix' && <MatrixTab matrix={matrix} />}
{activeTab === 'audit' && <AuditTab gci={gci} />}
</div>
) : null}
</div>
)
}

View File

@@ -0,0 +1,89 @@
/**
* GCI API Proxy - Catch-all route
* Proxies all /api/sdk/v1/gci/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/gci`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (method === 'POST' || method === 'PUT') {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('GCI API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}

View File

@@ -561,6 +561,20 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
isActive={pathname === '/sdk/reporting'}
collapsed={collapsed}
/>
<AdditionalModuleItem
href="/sdk/gci"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M11 3.055A9.001 9.001 0 1020.945 13H11V3.055z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M20.488 9H15V3.512A9.025 9.025 0 0120.488 9z" />
</svg>
}
label="GCI Score"
isActive={pathname === '/sdk/gci'}
collapsed={collapsed}
/>
<AdditionalModuleItem
href="/sdk/industry-templates"
icon={

View File

@@ -0,0 +1,99 @@
/**
* GCI API Client
* Communicates with the Go backend via Next.js API proxy at /api/sdk/v1/gci/*
*/
import type {
GCIResult,
GCIBreakdown,
GCIHistoryResponse,
GCIMatrixResponse,
NIS2Score,
NIS2Role,
ISOGapAnalysis,
WeightProfile,
} from './types'
const BASE_URL = '/api/sdk/v1/gci'
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': typeof window !== 'undefined'
? (localStorage.getItem('bp-tenant-id') || 'default')
: 'default',
...options?.headers,
},
})
if (!res.ok) {
const error = await res.json().catch(() => ({ error: res.statusText }))
throw new Error(error.error || `API Error: ${res.status}`)
}
return res.json()
}
/** GCI Score abrufen */
export async function getGCIScore(profile?: string): Promise<GCIResult> {
const params = profile ? `?profile=${profile}` : ''
return apiFetch<GCIResult>(`/score${params}`)
}
/** Detailliertes 4-Level Breakdown abrufen */
export async function getGCIBreakdown(profile?: string): Promise<GCIBreakdown> {
const params = profile ? `?profile=${profile}` : ''
return apiFetch<GCIBreakdown>(`/score/breakdown${params}`)
}
/** GCI History abrufen */
export async function getGCIHistory(): Promise<GCIHistoryResponse> {
return apiFetch<GCIHistoryResponse>('/score/history')
}
/** Compliance Matrix abrufen */
export async function getGCIMatrix(): Promise<GCIMatrixResponse> {
return apiFetch<GCIMatrixResponse>('/matrix')
}
/** Audit Trail abrufen */
export async function getGCIAuditTrail(profile?: string): Promise<{ tenant_id: string; gci_score: number; audit_trail: any[] }> {
const params = profile ? `?profile=${profile}` : ''
return apiFetch(`/audit-trail${params}`)
}
/** Gewichtungsprofile abrufen */
export async function getWeightProfiles(): Promise<{ profiles: WeightProfile[] }> {
return apiFetch<{ profiles: WeightProfile[] }>('/profiles')
}
/** NIS2 Score abrufen */
export async function getNIS2Score(): Promise<NIS2Score> {
return apiFetch<NIS2Score>('/nis2/score')
}
/** NIS2 Rollen auflisten */
export async function getNIS2Roles(): Promise<{ roles: NIS2Role[]; total: number }> {
return apiFetch<{ roles: NIS2Role[]; total: number }>('/nis2/roles')
}
/** NIS2 Rolle zuweisen */
export async function assignNIS2Role(roleId: string, userId: string): Promise<any> {
return apiFetch('/nis2/roles/assign', {
method: 'POST',
body: JSON.stringify({ role_id: roleId, user_id: userId }),
})
}
/** ISO Gap-Analyse abrufen */
export async function getISOGapAnalysis(): Promise<ISOGapAnalysis> {
return apiFetch<ISOGapAnalysis>('/iso/gap-analysis')
}
/** ISO Mappings abrufen */
export async function getISOMappings(category?: string): Promise<any> {
const params = category ? `?category=${category}` : ''
return apiFetch(`/iso/mappings${params}`)
}

View File

@@ -0,0 +1,246 @@
/**
* GCI (Gesamt-Compliance-Index) Types
* TypeScript definitions for the 4-level compliance scoring model
*/
// =============================================================================
// MATURITY LEVELS
// =============================================================================
export type MaturityLevel = 'OPTIMIZED' | 'MANAGED' | 'DEFINED' | 'REACTIVE' | 'HIGH_RISK'
export const MATURITY_INFO: Record<MaturityLevel, { label: string; color: string; bgColor: string; borderColor: string; description: string }> = {
OPTIMIZED: { label: 'Optimiert', color: 'text-green-700', bgColor: 'bg-green-100', borderColor: 'border-green-300', description: 'Kontinuierliche Verbesserung, proaktive Compliance' },
MANAGED: { label: 'Gesteuert', color: 'text-blue-700', bgColor: 'bg-blue-100', borderColor: 'border-blue-300', description: 'Messbare Prozesse, regelmaessige Reviews' },
DEFINED: { label: 'Definiert', color: 'text-yellow-700', bgColor: 'bg-yellow-100', borderColor: 'border-yellow-300', description: 'Dokumentierte Prozesse, erste Strukturen' },
REACTIVE: { label: 'Reaktiv', color: 'text-orange-700', bgColor: 'bg-orange-100', borderColor: 'border-orange-300', description: 'Ad-hoc Massnahmen, wenig Struktur' },
HIGH_RISK: { label: 'Hohes Risiko', color: 'text-red-700', bgColor: 'bg-red-100', borderColor: 'border-red-300', description: 'Erheblicher Handlungsbedarf, Compliance-Luecken' },
}
// =============================================================================
// LEVEL 1: MODULE SCORE
// =============================================================================
export interface ModuleScore {
module_id: string
module_name: string
assigned: number
completed: number
raw_score: number
validity_factor: number
final_score: number
risk_weight: number
category: string
}
// =============================================================================
// LEVEL 2: RISK-WEIGHTED AREA SCORE
// =============================================================================
export interface RiskWeightedScore {
area_id: string
area_name: string
modules: ModuleScore[]
weighted_sum: number
total_weight: number
area_score: number
}
// =============================================================================
// LEVEL 3: REGULATION AREA SCORE
// =============================================================================
export interface RegulationAreaScore {
regulation_id: string
regulation_name: string
score: number
weight: number
weighted_score: number
module_count: number
completed_count: number
}
// =============================================================================
// LEVEL 4: GCI RESULT
// =============================================================================
export interface AuditEntry {
timestamp: string
factor: string
description: string
value: number
impact: 'positive' | 'negative' | 'neutral'
}
export interface GCIResult {
tenant_id: string
gci_score: number
maturity_level: MaturityLevel
maturity_label: string
calculated_at: string
profile: string
area_scores: RegulationAreaScore[]
criticality_multiplier: number
incident_adjustment: number
audit_trail: AuditEntry[]
}
export interface GCIBreakdown extends GCIResult {
level1_modules: ModuleScore[]
level2_areas: RiskWeightedScore[]
}
// =============================================================================
// GCI HISTORY
// =============================================================================
export interface GCISnapshot {
tenant_id: string
score: number
maturity_level: MaturityLevel
area_scores: Record<string, number>
calculated_at: string
}
export interface GCIHistoryResponse {
tenant_id: string
snapshots: GCISnapshot[]
total: number
}
// =============================================================================
// COMPLIANCE MATRIX
// =============================================================================
export interface ComplianceMatrixEntry {
role: string
role_name: string
regulations: Record<string, number>
overall_score: number
required_modules: number
completed_modules: number
}
export interface GCIMatrixResponse {
tenant_id: string
matrix: ComplianceMatrixEntry[]
}
// =============================================================================
// NIS2
// =============================================================================
export interface NIS2Role {
id: string
name: string
description: string
mandatory_modules: string[]
priority: number
}
export interface NIS2AreaScore {
area_id: string
area_name: string
weight: number
score: number
weighted_score: number
}
export interface NIS2RoleScore {
role_id: string
role_name: string
assigned_users: number
completion_rate: number
modules_completed: number
modules_required: number
}
export interface NIS2Score {
tenant_id: string
overall_score: number
maturity_level: string
areas: NIS2AreaScore[]
role_scores: NIS2RoleScore[]
calculated_at: string
}
// =============================================================================
// ISO 27001
// =============================================================================
export interface ISOControl {
id: string
name: string
description: string
category_id: string
category_name: string
control_type: string
is_critical: boolean
sdk_modules: string[]
}
export interface ISOGap {
control_id: string
control_name: string
category: string
status: string
priority: string
recommendation: string
}
export interface ISOCategorySummary {
category_id: string
category_name: string
total_controls: number
covered_full: number
covered_partial: number
not_covered: number
}
export interface ISOGapAnalysis {
tenant_id: string
total_controls: number
covered_full: number
covered_partial: number
not_covered: number
coverage_percent: number
category_summaries: ISOCategorySummary[]
gaps: ISOGap[]
calculated_at: string
}
// =============================================================================
// WEIGHT PROFILES
// =============================================================================
export interface WeightProfile {
id: string
name: string
description: string
weights: Record<string, number>
}
// =============================================================================
// HELPERS
// =============================================================================
export function getScoreColor(score: number): string {
if (score >= 80) return 'text-green-600'
if (score >= 60) return 'text-yellow-600'
if (score >= 40) return 'text-orange-600'
return 'text-red-600'
}
export function getScoreBgColor(score: number): string {
if (score >= 80) return 'bg-green-500'
if (score >= 60) return 'bg-yellow-500'
if (score >= 40) return 'bg-orange-500'
return 'bg-red-500'
}
export function getScoreRingColor(score: number): string {
if (score >= 80) return '#22c55e'
if (score >= 60) return '#eab308'
if (score >= 40) return '#f97316'
return '#ef4444'
}