e536247c20
Wire the 195 Clean-Room QUAIDAL controls (from breakpilot-core migration 011)
into the compliance SaaS UI.
Backend:
- GET /api/v1/quaidal/stats - counts by kind + source provenance
- GET /api/v1/quaidal/controls - list, optional kind= filter
- GET /api/v1/quaidal/controls/{id} - single derived control
- GET /api/v1/quaidal/criteria - 10 QKB criteria
- GET /api/v1/quaidal/criteria/{id} - QKB with QB/MA/QM tree
Frontend:
- /sdk/quality: new "Trainingsdaten-Qualität (BSI QUAIDAL)" tab with
10 QKB cards and a drill-down modal showing the full QB→MA→QM tree
plus original BSI source link and license note.
- /sdk/ai-act: Art. 10 tile on each high-risk/unacceptable result,
linking to /sdk/quality?category=data_quality.
Pattern matches existing IACE module DIN-reference handling:
own wording, source section + URL preserved for due diligence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
153 lines
6.2 KiB
TypeScript
153 lines
6.2 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
import { fetchCriterionTree, type QuaidalControl, type QuaidalCriterionTree } from '../_hooks/useQuaidalData'
|
||
|
||
interface Props {
|
||
sectionId: string
|
||
onClose: () => void
|
||
}
|
||
|
||
function ControlBlock({ ctrl, badgeColor }: { ctrl: QuaidalControl; badgeColor: string }) {
|
||
return (
|
||
<div className="border border-gray-200 rounded-lg p-4 bg-white">
|
||
<div className="flex items-start justify-between gap-3 mb-2">
|
||
<h4 className="font-semibold text-gray-900">{ctrl.canonical_name}</h4>
|
||
<span className={`px-2 py-0.5 text-xs rounded-full ${badgeColor} shrink-0`}>{ctrl.source.section}</span>
|
||
</div>
|
||
<p className="text-sm text-gray-600 mb-3 whitespace-pre-line">{ctrl.description}</p>
|
||
{ctrl.source.url && (
|
||
<a
|
||
href={ctrl.source.url}
|
||
target="_blank"
|
||
rel="noreferrer noopener"
|
||
className="text-xs text-purple-600 hover:text-purple-800 underline"
|
||
>
|
||
BSI-Quelle ansehen ({ctrl.source.framework})
|
||
</a>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function QuaidalCriterionDetail({ sectionId, onClose }: Props) {
|
||
const [tree, setTree] = useState<QuaidalCriterionTree | null>(null)
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
useEffect(() => {
|
||
let active = true
|
||
setLoading(true)
|
||
fetchCriterionTree(sectionId).then(t => {
|
||
if (active) {
|
||
setTree(t)
|
||
setLoading(false)
|
||
}
|
||
})
|
||
return () => { active = false }
|
||
}, [sectionId])
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||
<div className="bg-white rounded-2xl shadow-xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
|
||
<div>
|
||
<div className="text-xs text-gray-500 uppercase tracking-wide">QUAIDAL Kriterium</div>
|
||
<h2 className="text-xl font-bold text-gray-900">
|
||
{tree?.criterion.canonical_name || sectionId}
|
||
</h2>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="w-8 h-8 rounded-full hover:bg-gray-100 flex items-center justify-center text-gray-500"
|
||
aria-label="Schliessen"
|
||
>×</button>
|
||
</div>
|
||
|
||
<div className="overflow-y-auto p-6 space-y-6">
|
||
{loading && <div className="text-center text-gray-400 py-12">Lade...</div>}
|
||
|
||
{tree && (
|
||
<>
|
||
<div>
|
||
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
||
Anforderung (eigene Formulierung)
|
||
</h3>
|
||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
|
||
<p className="text-gray-800 whitespace-pre-line">{tree.criterion.description}</p>
|
||
</div>
|
||
<div className="mt-3 flex flex-wrap items-center gap-3 text-xs text-gray-500">
|
||
<span>Regulierung: <span className="font-medium text-gray-700">{tree.criterion.regulation_anchor || '—'}</span></span>
|
||
<span>Quelle: <span className="font-medium text-gray-700">{tree.criterion.source.framework} {tree.criterion.source.section}</span></span>
|
||
{tree.criterion.source.url && (
|
||
<a href={tree.criterion.source.url} target="_blank" rel="noreferrer noopener" className="text-purple-600 hover:text-purple-800 underline">
|
||
Originalquelle
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{tree.criterion.external_refs.length > 0 && (
|
||
<div>
|
||
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-2">
|
||
Externe Referenzen (nicht ingestiert, nur Verweis)
|
||
</h3>
|
||
<div className="flex flex-wrap gap-2">
|
||
{tree.criterion.external_refs.map((ref, i) => (
|
||
<span key={i} className="px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded">
|
||
{ref.framework}{ref.citation ? ` — ${ref.citation}` : ''}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{tree.building_blocks.length > 0 && (
|
||
<div>
|
||
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||
Bausteine ({tree.building_blocks.length})
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{tree.building_blocks.map(qb => (
|
||
<ControlBlock key={qb.derived_id} ctrl={qb} badgeColor="bg-blue-100 text-blue-700" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{tree.measures.length > 0 && (
|
||
<div>
|
||
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||
Maßnahmen ({tree.measures.length})
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{tree.measures.map(m => (
|
||
<ControlBlock key={m.derived_id} ctrl={m} badgeColor="bg-green-100 text-green-700" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{tree.metrics.length > 0 && (
|
||
<div>
|
||
<h3 className="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">
|
||
Metriken & Methoden ({tree.metrics.length})
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{tree.metrics.map(qm => (
|
||
<ControlBlock key={qm.derived_id} ctrl={qm} badgeColor="bg-amber-100 text-amber-700" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="px-6 py-3 border-t border-gray-200 bg-gray-50 text-xs text-gray-500">
|
||
Eigene Clean-Room-Ableitung von BSI QUAIDAL. Quellverweis und Lizenz-Note pro Eintrag.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|