merge: phases 1–5 refactor, CI hardening, docs (coolify → main)
Some checks failed
Build + Deploy / build-admin-compliance (push) Failing after 47s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 34s
Build + Deploy / build-developer-portal (push) Successful in 56s
Build + Deploy / build-tts (push) Successful in 26s
Build + Deploy / build-document-crawler (push) Successful in 15s
Build + Deploy / build-dsms-gateway (push) Successful in 13s
Build + Deploy / trigger-orca (push) Has been skipped
CI/CD / loc-budget (push) Successful in 22s
CI/CD / guardrail-integrity (push) Has been skipped
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been cancelled
CI/CD / test-go-ai-compliance (push) Has been cancelled
CI/CD / test-python-backend-compliance (push) Has been cancelled
CI/CD / test-python-document-crawler (push) Has been cancelled
CI/CD / test-python-dsms-gateway (push) Successful in 28s
CI/CD / sbom-scan (push) Has been cancelled
CI/CD / validate-canonical-controls (push) Successful in 20s
Some checks failed
Build + Deploy / build-admin-compliance (push) Failing after 47s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 34s
Build + Deploy / build-developer-portal (push) Successful in 56s
Build + Deploy / build-tts (push) Successful in 26s
Build + Deploy / build-document-crawler (push) Successful in 15s
Build + Deploy / build-dsms-gateway (push) Successful in 13s
Build + Deploy / trigger-orca (push) Has been skipped
CI/CD / loc-budget (push) Successful in 22s
CI/CD / guardrail-integrity (push) Has been skipped
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been cancelled
CI/CD / test-go-ai-compliance (push) Has been cancelled
CI/CD / test-python-backend-compliance (push) Has been cancelled
CI/CD / test-python-document-crawler (push) Has been cancelled
CI/CD / test-python-dsms-gateway (push) Successful in 28s
CI/CD / sbom-scan (push) Has been cancelled
CI/CD / validate-canonical-controls (push) Successful in 20s
Phase 1: backend-compliance — partial service-layer extraction Phase 2: ai-compliance-sdk — full hexagonal split; iace/ucca/training handlers and stores split into focused files; cmd/server/main.go → internal/app/ Phase 3: admin-compliance — types.ts, tom-generator loader, and major page components split; lib document generators extracted Phase 4: dsms-gateway, consent-sdk, developer-portal, breakpilot-compliance-sdk Phase 5 CI hardening: - loc-budget job now scans whole repo (blocking, no || true) - sbom-scan / grype blocking on high+ CVEs - ai-compliance-sdk/.golangci.yml: strict golangci-lint config - check-loc.sh: skip test_*.py and *.html; loc-exceptions.txt expanded - deleted stray routes.py.backup (2512 LOC) Docs: - root README.md with CI badge, service table, quick start, CI pipeline table - CONTRIBUTING.md: setup, pre-commit checklist, guardrail marker reference - CLAUDE.md: First-Time Setup & Claude Code Onboarding section - all 7 service READMEs updated (stale phase refs, current architecture) - AGENTS.go/python/typescript.md enhanced with linting, DI, barrel re-export - .gitignore: dist/, .turbo/, pnpm-lock.yaml added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
'use client'
|
||||
import React, { useState, useCallback, useEffect } from 'react'
|
||||
import type { ScopeProfilingAnswer } from '@/lib/sdk/compliance-scope-types'
|
||||
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers, getUnansweredRequiredQuestions } from '@/lib/sdk/compliance-scope-profiling'
|
||||
import type { ScopeProfilingAnswer, ScopeProfilingQuestion } from '@/lib/sdk/compliance-scope-types'
|
||||
import { SCOPE_QUESTION_BLOCKS, getBlockProgress, getTotalProgress, getAnswerValue, prefillFromCompanyProfile, getProfileInfoForBlock, getAutoFilledScoringAnswers, getUnansweredRequiredQuestions } from '@/lib/sdk/compliance-scope-profiling'
|
||||
import type { ScopeQuestionBlockId } from '@/lib/sdk/compliance-scope-types'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import { DatenkategorienBlock9 } from './DatenkategorienBlock'
|
||||
import { ScopeQuestionRenderer } from './ScopeQuestionRenderer'
|
||||
|
||||
interface ScopeWizardTabProps {
|
||||
answers: ScopeProfilingAnswer[]
|
||||
@@ -102,12 +101,107 @@ export function ScopeWizardTab({
|
||||
const toggleHelp = useCallback((questionId: string) => {
|
||||
setExpandedHelp(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(questionId)) next.delete(questionId)
|
||||
else next.add(questionId)
|
||||
if (next.has(questionId)) { next.delete(questionId) } else { next.add(questionId) }
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const isPrefilledFromProfile = useCallback((questionId: string) => {
|
||||
return prefilledIds.has(questionId)
|
||||
}, [prefilledIds])
|
||||
|
||||
const renderHelpText = (question: ScopeProfilingQuestion) => {
|
||||
if (!question.helpText) return null
|
||||
return (
|
||||
<>
|
||||
<button type="button" className="ml-2 text-blue-400 hover:text-blue-600 inline-flex items-center" onClick={(e) => { e.preventDefault(); toggleHelp(question.id) }}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
</button>
|
||||
{expandedHelp.has(question.id) && (
|
||||
<div className="flex items-start gap-2 mt-2 p-2.5 bg-blue-50 rounded-lg text-xs text-blue-700 leading-relaxed">
|
||||
<svg className="w-4 h-4 mt-0.5 flex-shrink-0 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span>{question.helpText}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPrefilledBadge = (questionId: string) => {
|
||||
if (!isPrefilledFromProfile(questionId)) return null
|
||||
return <span className="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">Aus Profil</span>
|
||||
}
|
||||
|
||||
const renderQuestion = (question: ScopeProfilingQuestion) => {
|
||||
const currentValue = getAnswerValue(answers, question.id)
|
||||
const label = (
|
||||
<div className="flex items-center flex-wrap gap-1">
|
||||
<span className="text-sm font-medium text-gray-900">{question.question}</span>
|
||||
{question.required && <span className="text-red-500 ml-1">*</span>}
|
||||
{renderPrefilledBadge(question.id)}
|
||||
{renderHelpText(question)}
|
||||
</div>
|
||||
)
|
||||
|
||||
switch (question.type) {
|
||||
case 'boolean':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between">{label}</div>
|
||||
<div className="flex gap-3">
|
||||
<button type="button" onClick={() => handleAnswerChange(question.id, true)} className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${currentValue === true ? 'border-purple-500 bg-purple-50 text-purple-700 font-medium' : 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'}`}>Ja</button>
|
||||
<button type="button" onClick={() => handleAnswerChange(question.id, false)} className={`flex-1 py-2 px-4 rounded-lg border-2 transition-all ${currentValue === false ? 'border-purple-500 bg-purple-50 text-purple-700 font-medium' : 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'}`}>Nein</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'single':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label}
|
||||
<div className="space-y-2">
|
||||
{question.options?.map((option) => (
|
||||
<button key={option.value} type="button" onClick={() => handleAnswerChange(question.id, option.value)} className={`w-full text-left py-3 px-4 rounded-lg border-2 transition-all ${currentValue === option.value ? 'border-purple-500 bg-purple-50 text-purple-700 font-medium' : 'border-gray-300 bg-white text-gray-700 hover:border-gray-400'}`}>{option.label}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'multi':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label}
|
||||
<div className="space-y-2">
|
||||
{question.options?.map((option) => {
|
||||
const selectedValues = Array.isArray(currentValue) ? currentValue as string[] : []
|
||||
const isChecked = selectedValues.includes(option.value)
|
||||
return (
|
||||
<label key={option.value} className={`flex items-center gap-3 py-3 px-4 rounded-lg border-2 cursor-pointer transition-all ${isChecked ? 'border-purple-500 bg-purple-50' : 'border-gray-300 bg-white hover:border-gray-400'}`}>
|
||||
<input type="checkbox" checked={isChecked} onChange={(e) => { const newValues = e.target.checked ? [...selectedValues, option.value] : selectedValues.filter(v => v !== option.value); handleAnswerChange(question.id, newValues) }} className="w-5 h-5 text-purple-600 border-gray-300 rounded focus:ring-purple-500" />
|
||||
<span className={isChecked ? 'text-purple-700 font-medium' : 'text-gray-700'}>{option.label}</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
case 'number':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label}
|
||||
<input type="number" value={currentValue != null ? String(currentValue) : ''} onChange={(e) => handleAnswerChange(question.id, parseInt(e.target.value, 10))} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Zahl eingeben" />
|
||||
</div>
|
||||
)
|
||||
case 'text':
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{label}
|
||||
<input type="text" value={currentValue != null ? String(currentValue) : ''} onChange={(e) => handleAnswerChange(question.id, e.target.value)} className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="Text eingeben" />
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 h-full">
|
||||
{/* Left Sidebar - Block Navigation */}
|
||||
@@ -126,25 +220,12 @@ export function ScopeWizardTab({
|
||||
const optionalDone = !hasRequired && hasAnyAnswer
|
||||
|
||||
return (
|
||||
<button
|
||||
key={block.id}
|
||||
type="button"
|
||||
onClick={() => setCurrentBlockIndex(idx)}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg transition-all ${
|
||||
isActive
|
||||
? 'bg-purple-50 border-2 border-purple-500'
|
||||
: 'bg-gray-50 border border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<button key={block.id} type="button" onClick={() => setCurrentBlockIndex(idx)} className={`w-full text-left px-3 py-2 rounded-lg transition-all ${isActive ? 'bg-purple-50 border-2 border-purple-500' : 'bg-gray-50 border border-gray-200 hover:border-gray-300'}`}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className={`text-sm font-medium ${isActive ? 'text-purple-700' : 'text-gray-700'}`}>
|
||||
{block.title}
|
||||
</span>
|
||||
<span className={`text-sm font-medium ${isActive ? 'text-purple-700' : 'text-gray-700'}`}>{block.title}</span>
|
||||
{allRequiredDone || optionalDone ? (
|
||||
<span className="flex items-center gap-1 text-xs font-semibold text-green-600">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" /></svg>
|
||||
{!hasRequired && <span>(optional)</span>}
|
||||
</span>
|
||||
) : !hasRequired ? (
|
||||
@@ -154,12 +235,7 @@ export function ScopeWizardTab({
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${
|
||||
allRequiredDone || optionalDone ? 'bg-green-500' : !hasRequired ? 'bg-gray-300' : 'bg-orange-400'
|
||||
}`}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
<div className={`h-full transition-all ${allRequiredDone || optionalDone ? 'bg-green-500' : !hasRequired ? 'bg-gray-300' : 'bg-orange-400'}`} style={{ width: `${progress}%` }} />
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
@@ -175,20 +251,15 @@ export function ScopeWizardTab({
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">Gesamtfortschritt</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-500">
|
||||
{completionStats.answered} / {completionStats.total} Fragen
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">{completionStats.answered} / {completionStats.total} Fragen</span>
|
||||
<span className="text-sm font-bold text-gray-900">{totalProgress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-500"
|
||||
style={{ width: `${totalProgress}%` }}
|
||||
/>
|
||||
<div className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-500" style={{ width: `${totalProgress}%` }} />
|
||||
</div>
|
||||
|
||||
{/* Clickable unanswered required questions summary */}
|
||||
{/* Unanswered required questions summary */}
|
||||
{(() => {
|
||||
const allUnanswered = getUnansweredRequiredQuestions(answers)
|
||||
if (allUnanswered.length === 0) return null
|
||||
@@ -206,11 +277,7 @@ export function ScopeWizardTab({
|
||||
{Array.from(byBlock.entries()).map(([blockId, info], i) => (
|
||||
<React.Fragment key={blockId}>
|
||||
{i > 0 && <span className="text-gray-300">·</span>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentBlockIndex(info.blockIndex)}
|
||||
className="text-orange-700 hover:text-orange-900 hover:underline font-medium"
|
||||
>
|
||||
<button type="button" onClick={() => setCurrentBlockIndex(info.blockIndex)} className="text-orange-700 hover:text-orange-900 hover:underline font-medium">
|
||||
{info.blockTitle} ({info.count})
|
||||
</button>
|
||||
</React.Fragment>
|
||||
@@ -228,17 +295,13 @@ export function ScopeWizardTab({
|
||||
<p className="text-gray-600">{currentBlock.description}</p>
|
||||
</div>
|
||||
{companyProfile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrefillFromProfile}
|
||||
className="px-4 py-2 text-sm bg-blue-50 text-blue-700 border border-blue-300 rounded-lg hover:bg-blue-100 transition-colors whitespace-nowrap"
|
||||
>
|
||||
<button type="button" onClick={handlePrefillFromProfile} className="px-4 py-2 text-sm bg-blue-50 text-blue-700 border border-blue-300 rounded-lg hover:bg-blue-100 transition-colors whitespace-nowrap">
|
||||
Aus Profil uebernehmen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* "Aus Profil" Info Box */}
|
||||
{/* Profile Info Box */}
|
||||
{companyProfile && (() => {
|
||||
const profileItems = getProfileInfoForBlock(companyProfile, currentBlock.id as ScopeQuestionBlockId)
|
||||
if (profileItems.length === 0) return null
|
||||
@@ -247,27 +310,16 @@ export function ScopeWizardTab({
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-blue-900 mb-2 flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
Aus Unternehmensprofil
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-blue-800">
|
||||
{profileItems.map(item => (
|
||||
<span key={item.label}>
|
||||
<span className="font-medium">{item.label}:</span> {item.value}
|
||||
</span>
|
||||
))}
|
||||
{profileItems.map(item => (<span key={item.label}><span className="font-medium">{item.label}:</span> {item.value}</span>))}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="/sdk/company-profile"
|
||||
className="text-sm text-blue-600 hover:text-blue-800 font-medium whitespace-nowrap flex items-center gap-1"
|
||||
>
|
||||
<a href="/sdk/company-profile" className="text-sm text-blue-600 hover:text-blue-800 font-medium whitespace-nowrap flex items-center gap-1">
|
||||
Profil bearbeiten
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -281,9 +333,7 @@ export function ScopeWizardTab({
|
||||
) : (
|
||||
currentBlock.questions.map((question) => {
|
||||
const isAnswered = answers.some(a => a.questionId === question.id)
|
||||
const borderClass = question.required
|
||||
? isAnswered ? 'border-l-4 border-l-green-400 pl-4' : 'border-l-4 border-l-orange-400 pl-4'
|
||||
: ''
|
||||
const borderClass = question.required ? (isAnswered ? 'border-l-4 border-l-green-400 pl-4' : 'border-l-4 border-l-orange-400 pl-4') : ''
|
||||
return (
|
||||
<div key={question.id} className={`border-b border-gray-100 pb-6 last:border-b-0 last:pb-0 ${borderClass}`}>
|
||||
<ScopeQuestionRenderer
|
||||
@@ -303,32 +353,16 @@ export function ScopeWizardTab({
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex items-center justify-between bg-white rounded-xl border border-gray-200 p-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
disabled={currentBlockIndex === 0}
|
||||
className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<button type="button" onClick={handleBack} disabled={currentBlockIndex === 0} className="px-6 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Zurueck
|
||||
</button>
|
||||
<span className="text-sm text-gray-600">
|
||||
Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600">Block {currentBlockIndex + 1} von {SCOPE_QUESTION_BLOCKS.length}</span>
|
||||
{currentBlockIndex === SCOPE_QUESTION_BLOCKS.length - 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEvaluate}
|
||||
disabled={!canEvaluate || isEvaluating}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<button type="button" onClick={onEvaluate} disabled={!canEvaluate || isEvaluating} className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{isEvaluating ? 'Evaluiere...' : 'Auswertung starten'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium"
|
||||
>
|
||||
<button type="button" onClick={handleNext} className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors font-medium">
|
||||
Weiter
|
||||
</button>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user