feat(use-case-compiler): MC-based compliance questionnaires with scoring
Build + Deploy / build-admin-compliance (push) Successful in 2m46s
Build + Deploy / build-backend-compliance (push) Successful in 26s
Build + Deploy / build-ai-sdk (push) Successful in 52s
Build + Deploy / build-developer-portal (push) Successful in 22s
Build + Deploy / build-tts (push) Successful in 16s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
Build + Deploy / build-dsms-node (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m16s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 1m0s
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 16s
Build + Deploy / trigger-orca (push) Successful in 2m36s
Build + Deploy / build-admin-compliance (push) Successful in 2m46s
Build + Deploy / build-backend-compliance (push) Successful in 26s
Build + Deploy / build-ai-sdk (push) Successful in 52s
Build + Deploy / build-developer-portal (push) Successful in 22s
Build + Deploy / build-tts (push) Successful in 16s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
Build + Deploy / build-dsms-node (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m16s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 1m0s
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 29s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 16s
Build + Deploy / trigger-orca (push) Successful in 2m36s
Implements the Use-Case Compiler that turns Master Controls into interactive compliance audits. 5 templates (Vendor Check, SAST/DAST, DSGVO, NIS2, CRA), deterministic + LLM question generation, scoring engine with regulation/severity breakdown, and gap detection. - Backend: 9 API endpoints, 22 unit tests (all pass) - Frontend: Template selector, questionnaire, result dashboard - Migration 027: usecase_audits + usecase_answers tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useMemo } from 'react'
|
||||
|
||||
interface Question {
|
||||
id: string
|
||||
mc_id: string
|
||||
mc_name: string
|
||||
question: string
|
||||
question_type: string
|
||||
evidence_required: boolean
|
||||
pass_criteria: string[]
|
||||
fail_criteria: string[]
|
||||
severity: string
|
||||
regulation: string
|
||||
depends_on?: string
|
||||
}
|
||||
|
||||
interface Answer {
|
||||
question_id: string
|
||||
value: unknown
|
||||
comment: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
questions: Question[]
|
||||
answers: Answer[]
|
||||
onAnswer: (questionId: string, value: unknown, comment: string) => void
|
||||
onSkip: (questionId: string) => void
|
||||
}
|
||||
|
||||
const SEVERITY_COLORS: Record<string, string> = {
|
||||
HIGH: 'bg-red-100 text-red-700 border-red-200',
|
||||
MEDIUM: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
LOW: 'bg-green-100 text-green-700 border-green-200',
|
||||
}
|
||||
|
||||
export function QuestionnaireView({ questions, answers, onAnswer, onSkip }: Props) {
|
||||
const [comments, setComments] = useState<Record<string, string>>({})
|
||||
const [showCriteria, setShowCriteria] = useState<string | null>(null)
|
||||
|
||||
const answerMap = useMemo(() => {
|
||||
const m: Record<string, Answer> = {}
|
||||
for (const a of answers) { m[a.question_id] = a }
|
||||
return m
|
||||
}, [answers])
|
||||
|
||||
// Filter visible questions (dependency check)
|
||||
const visibleQuestions = useMemo(() => {
|
||||
return questions.filter(q => {
|
||||
if (!q.depends_on) return true
|
||||
const depAnswer = answerMap[q.depends_on]
|
||||
if (!depAnswer) return false
|
||||
return depAnswer.value === true || depAnswer.value === 'yes' || depAnswer.value === 'ja'
|
||||
})
|
||||
}, [questions, answerMap])
|
||||
|
||||
// Group by regulation
|
||||
const grouped = useMemo(() => {
|
||||
const groups: Record<string, Question[]> = {}
|
||||
for (const q of visibleQuestions) {
|
||||
const key = q.regulation || 'Allgemein'
|
||||
if (!groups[key]) groups[key] = []
|
||||
groups[key].push(q)
|
||||
}
|
||||
return groups
|
||||
}, [visibleQuestions])
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{Object.entries(grouped).map(([reg, qs]) => (
|
||||
<div key={reg}>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<span className="px-2.5 py-0.5 bg-blue-100 text-blue-700 rounded text-sm font-medium">
|
||||
{reg}
|
||||
</span>
|
||||
<span className="text-sm text-gray-400 font-normal">
|
||||
{qs.length} Fragen
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{qs.map((q, idx) => {
|
||||
const existing = answerMap[q.id]
|
||||
const isAnswered = !!existing && existing.status !== 'skipped'
|
||||
const isSkipped = existing?.status === 'skipped'
|
||||
const isPassed = existing?.value === true || existing?.value === 'yes'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={q.id}
|
||||
className={`bg-white rounded-xl border p-5 transition-all ${
|
||||
isAnswered
|
||||
? isPassed
|
||||
? 'border-green-200 bg-green-50/30'
|
||||
: 'border-red-200 bg-red-50/30'
|
||||
: isSkipped
|
||||
? 'border-gray-200 bg-gray-50/50 opacity-60'
|
||||
: 'border-gray-200 hover:border-blue-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xs text-gray-400 font-mono mt-1 shrink-0">
|
||||
{q.id}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2 mb-2">
|
||||
<p className="text-sm font-medium text-gray-900">{q.question}</p>
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium border ${
|
||||
SEVERITY_COLORS[q.severity] || SEVERITY_COLORS.MEDIUM
|
||||
}`}>
|
||||
{q.severity}
|
||||
</span>
|
||||
{q.evidence_required && (
|
||||
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">
|
||||
Nachweis
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{q.mc_name && (
|
||||
<p className="text-xs text-gray-400 mb-2">
|
||||
MC: {q.mc_name} {q.mc_id && `(${q.mc_id})`}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Criteria toggle */}
|
||||
<button
|
||||
onClick={() => setShowCriteria(showCriteria === q.id ? null : q.id)}
|
||||
className="text-xs text-blue-500 hover:text-blue-700 mb-2"
|
||||
>
|
||||
{showCriteria === q.id ? 'Kriterien ausblenden' : 'Bewertungskriterien anzeigen'}
|
||||
</button>
|
||||
|
||||
{showCriteria === q.id && (
|
||||
<div className="grid grid-cols-2 gap-3 mb-3 text-xs">
|
||||
{q.pass_criteria?.length > 0 && (
|
||||
<div className="bg-green-50 border border-green-100 rounded p-2">
|
||||
<div className="font-medium text-green-700 mb-1">Pass</div>
|
||||
{q.pass_criteria.map((c, i) => (
|
||||
<div key={i} className="text-green-600">{c}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{q.fail_criteria?.length > 0 && (
|
||||
<div className="bg-red-50 border border-red-100 rounded p-2">
|
||||
<div className="font-medium text-red-700 mb-1">Fail</div>
|
||||
{q.fail_criteria.map((c, i) => (
|
||||
<div key={i} className="text-red-600">{c}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Answer controls */}
|
||||
{!isAnswered && !isSkipped && (
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<button
|
||||
onClick={() => onAnswer(q.id, true, comments[q.id] || '')}
|
||||
className="px-4 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm"
|
||||
>
|
||||
Ja
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAnswer(q.id, false, comments[q.id] || '')}
|
||||
className="px-4 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm"
|
||||
>
|
||||
Nein
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSkip(q.id)}
|
||||
className="px-3 py-1.5 text-gray-400 hover:text-gray-600 text-sm"
|
||||
>
|
||||
Ueberspringen
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Kommentar (optional)"
|
||||
value={comments[q.id] || ''}
|
||||
onChange={e => setComments(prev => ({ ...prev, [q.id]: e.target.value }))}
|
||||
className="flex-1 px-3 py-1.5 border border-gray-200 rounded-lg text-sm focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Already answered */}
|
||||
{isAnswered && (
|
||||
<div className="flex items-center gap-2 mt-2 text-sm">
|
||||
<span className={isPassed ? 'text-green-600 font-medium' : 'text-red-600 font-medium'}>
|
||||
{isPassed ? 'Ja' : 'Nein'}
|
||||
</span>
|
||||
{existing.comment && (
|
||||
<span className="text-gray-400">— {existing.comment}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSkipped && (
|
||||
<div className="text-sm text-gray-400 mt-2">Uebersprungen</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user