Use-Case-Mapping-Filter für Master Controls + Mapper-Präzisionsfix
CI / detect-changes (push) Successful in 14s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 7s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Failing after 15s
CI / go-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m23s
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 34s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

Phase 2: Live-Filter an /sdk/master-controls (Use Case, Quell-Regulierung,
Verifikations-Methode, Coverage, Primärzweck-Toggle, category via Member-EXISTS).
API mit EXISTS-Filtern + gecachten Meta-Counts in master-controls/route.ts.

Phase A: neue UseCase telekommunikation + Fix der Impressum-Fehlrouten im
Register (TKG/AT-TKG->telekommunikation, telemedien->dse, GewO->handelsrecht);
echte Impressum-Quellen (TMG/Mediengesetz) bleiben impressum. Deterministischer
Seed aus source_regulation; Tests grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-09 23:19:56 +02:00
parent c4d9b1426f
commit 372e1fe9e9
10 changed files with 434 additions and 45 deletions
@@ -12,6 +12,7 @@ import {
VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS,
} from './helpers'
import { ControlsMeta } from './useControlLibraryState'
import { useCaseLabel, mcVerificationLabel } from './mcMappingLabels'
import { GeneratorModal } from './GeneratorModal'
interface ControlListViewProps {
@@ -34,6 +35,10 @@ interface ControlListViewProps {
domainFilter: string
stateFilter: string
verificationFilter: string
useCaseFilter: string
primaryOnly: boolean
regulationFilter: string
mappedFilter: string
categoryFilter: string
evidenceTypeFilter: string
audienceFilter: string
@@ -46,6 +51,10 @@ interface ControlListViewProps {
setDomainFilter: (v: string) => void
setStateFilter: (v: string) => void
setVerificationFilter: (v: string) => void
setUseCaseFilter: (v: string) => void
setPrimaryOnly: (v: boolean) => void
setRegulationFilter: (v: string) => void
setMappedFilter: (v: string) => void
setCategoryFilter: (v: string) => void
setEvidenceTypeFilter: (v: string) => void
setAudienceFilter: (v: string) => void
@@ -71,10 +80,12 @@ export function ControlListView({
reviewCount, bulkProcessing, showStats, processedStats,
showGenerator, currentPage, totalPages, sortBy,
searchQuery, severityFilter, domainFilter, stateFilter,
verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter,
verificationFilter, useCaseFilter, primaryOnly, regulationFilter, mappedFilter,
categoryFilter, evidenceTypeFilter, audienceFilter,
sourceFilter, typeFilter, hideDuplicates,
setSearchQuery, setSeverityFilter, setDomainFilter, setStateFilter,
setVerificationFilter, setCategoryFilter, setEvidenceTypeFilter, setAudienceFilter,
setVerificationFilter, setUseCaseFilter, setPrimaryOnly, setRegulationFilter, setMappedFilter,
setCategoryFilter, setEvidenceTypeFilter, setAudienceFilter,
setSourceFilter, setTypeFilter, setHideDuplicates, setSortBy,
setShowStats, setShowGenerator, setCurrentPage,
onSelectControl, onCreateMode, onEnterReview, onBulkReject, onRefresh, onLoadStats, onFullReload,
@@ -176,18 +187,60 @@ export function ControlListView({
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
Duplikate ausblenden
</label>
{meta?.use_case_counts && (
<select value={useCaseFilter} onChange={e => setUseCaseFilter(e.target.value)}
className="text-sm border border-purple-300 bg-purple-50 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500 max-w-[260px]">
<option value="">Use Case (alle)</option>
{Object.entries(meta.use_case_counts).sort((a, b) => b[1] - a[1]).map(([k, c]) => (
<option key={k} value={k}>{useCaseLabel(k)} ({c})</option>
))}
</select>
)}
{meta?.use_case_counts && useCaseFilter && (
<label className="flex items-center gap-1.5 text-xs text-gray-600 cursor-pointer whitespace-nowrap"
title="Nur Master Controls, deren Primärzweck dieser Use Case ist (blendet über-geclusterte Mehrfachzwecke aus)">
<input type="checkbox" checked={primaryOnly} onChange={e => setPrimaryOnly(e.target.checked)}
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500" />
nur Primärzweck
</label>
)}
{meta?.regulations && meta.regulations.length > 0 && (
<select value={regulationFilter} onChange={e => setRegulationFilter(e.target.value)}
className="text-sm border border-blue-300 bg-blue-50 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500 max-w-[260px]">
<option value="">Regulierung (alle)</option>
{meta.regulations.map(rg => (
<option key={rg.source_regulation} value={rg.source_regulation}>{rg.source_regulation} ({rg.count})</option>
))}
</select>
)}
<select value={verificationFilter} onChange={e => setVerificationFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">Nachweis</option>
{Object.entries(VERIFICATION_METHODS).map(([k, v]) => (
<option key={k} value={k}>{v.label}{meta?.verification_method_counts?.[k] ? ` (${meta.verification_method_counts[k]})` : ''}</option>
))}
{Object.keys(meta?.verification_method_counts || {})
.filter(k => k !== '__none__' && !(k in VERIFICATION_METHODS))
.map(k => (
<option key={k} value={k}>{mcVerificationLabel(k)} ({meta!.verification_method_counts![k]})</option>
))}
{meta?.verification_method_counts?.['__none__'] ? <option value="__none__">Ohne Nachweis ({meta.verification_method_counts['__none__']})</option> : null}
</select>
{meta?.mapped_total != null && (
<select value={mappedFilter} onChange={e => setMappedFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">Coverage: alle</option>
<option value="mapped">Zugeordnet ({meta.mapped_total})</option>
<option value="unmapped">Offen ({meta.unmapped_count ?? 0})</option>
</select>
)}
<select value={categoryFilter} onChange={e => setCategoryFilter(e.target.value)}
className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500">
<option value="">Kategorie</option>
{CATEGORY_OPTIONS.map(c => <option key={c.value} value={c.value}>{c.label}{meta?.category_counts?.[c.value] ? ` (${meta.category_counts[c.value]})` : ''}</option>)}
{Object.keys(meta?.category_counts || {})
.filter(k => k !== '__none__' && !CATEGORY_OPTIONS.some(c => c.value === k))
.map(k => <option key={k} value={k}>{k} ({meta!.category_counts![k]})</option>)}
{meta?.category_counts?.['__none__'] ? <option value="__none__">Ohne Kategorie ({meta.category_counts['__none__']})</option> : null}
</select>
<select value={evidenceTypeFilter} onChange={e => setEvidenceTypeFilter(e.target.value)}
@@ -0,0 +1,65 @@
// Display labels for the master-control mapping dimensions (use case +
// verification method). Keys mirror the backend use_case_registry; an unknown
// key humanizes gracefully so a newly-seeded use case still renders.
export const USE_CASE_LABELS: Record<string, string> = {
impressum: 'Impressum',
telekommunikation: 'Telekommunikation (TKG)',
dse: 'Datenschutzerklärung',
agb: 'AGB',
cookie_banner: 'Cookie-Banner',
widerruf: 'Widerruf',
dsr: 'Betroffenenrechte (DSR)',
loeschkonzept: 'Löschkonzept',
avv: 'Auftragsverarbeitung (AVV)',
dsfa: 'DSFA',
code_security: 'Code Security',
network_security: 'Network Security',
cra: 'Cyber Resilience Act',
isms: 'ISMS',
tisax: 'TISAX',
kritis: 'KRITIS',
dora: 'DORA',
ai_act: 'AI Act',
mica: 'MiCA',
mdr: 'Medizinprodukte (MDR)',
maschinen: 'Maschinenverordnung',
batterie: 'Batterieverordnung',
ehds: 'EHDS',
produktsicherheit: 'Produktsicherheit',
dsa: 'Digital Services Act',
dma: 'Digital Markets Act',
data_governance: 'Data Governance Act',
zahlungsdienste: 'Zahlungsdienste (PSD2)',
geldwaesche: 'Geldwäsche (GwG)',
lieferkette: 'Lieferkettengesetz',
whistleblowing: 'Whistleblowing',
barrierefreiheit: 'Barrierefreiheit (BFSG)',
verbraucherschutz: 'Verbraucherschutz',
urheberrecht: 'Urheberrecht',
wettbewerbsrecht: 'Wettbewerbsrecht',
gleichbehandlung: 'Gleichbehandlung (AGG)',
steuerrecht: 'Steuerrecht',
handelsrecht: 'Handelsrecht',
}
export const MC_VERIFICATION_LABELS: Record<string, string> = {
document: 'Dokument',
source_code: 'Source Code',
network: 'Netzwerk/Infra',
it_process: 'IT-Prozess',
hybrid: 'Hybrid',
manual: 'Manuell',
}
function humanize(key: string): string {
return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
}
export function useCaseLabel(key: string): string {
return USE_CASE_LABELS[key] || humanize(key)
}
export function mcVerificationLabel(key: string): string {
return MC_VERIFICATION_LABELS[key] || humanize(key)
}
@@ -14,6 +14,11 @@ export interface ControlsMeta {
category_counts?: Record<string, number>
evidence_type_counts?: Record<string, number>
release_state_counts?: Record<string, number>
// Master-control mapping dimensions (only returned by the MC endpoint)
use_case_counts?: Record<string, number>
regulations?: Array<{ source_regulation: string; count: number }>
mapped_total?: number
unmapped_count?: number
}
const PAGE_SIZE = 50
@@ -35,6 +40,10 @@ export function useControlLibraryState(backendUrlOverride?: string) {
const [domainFilter, setDomainFilter] = useState<string>('')
const [stateFilter, setStateFilter] = useState<string>('')
const [verificationFilter, setVerificationFilter] = useState<string>('')
const [useCaseFilter, setUseCaseFilter] = useState<string>('')
const [primaryOnly, setPrimaryOnly] = useState<boolean>(false)
const [regulationFilter, setRegulationFilter] = useState<string>('')
const [mappedFilter, setMappedFilter] = useState<string>('')
const [categoryFilter, setCategoryFilter] = useState<string>('')
const [evidenceTypeFilter, setEvidenceTypeFilter] = useState<string>('')
const [audienceFilter, setAudienceFilter] = useState<string>('')
@@ -88,6 +97,10 @@ export function useControlLibraryState(backendUrlOverride?: string) {
if (domainFilter) p.set('domain', domainFilter)
if (stateFilter) p.set('release_state', stateFilter)
if (verificationFilter) p.set('verification_method', verificationFilter)
if (useCaseFilter) p.set('use_case', useCaseFilter)
if (primaryOnly) p.set('primary', '1')
if (regulationFilter) p.set('source_regulation', regulationFilter)
if (mappedFilter) p.set('mapped', mappedFilter)
if (categoryFilter) p.set('category', categoryFilter)
if (evidenceTypeFilter) p.set('evidence_type', evidenceTypeFilter)
if (audienceFilter) p.set('target_audience', audienceFilter)
@@ -97,7 +110,7 @@ export function useControlLibraryState(backendUrlOverride?: string) {
if (debouncedSearch) p.set('search', debouncedSearch)
if (extra) for (const [k, v] of Object.entries(extra)) p.set(k, v)
return p.toString()
}, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch])
}, [severityFilter, domainFilter, stateFilter, verificationFilter, useCaseFilter, primaryOnly, regulationFilter, mappedFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch])
const loadFrameworks = useCallback(async () => {
try {
@@ -156,7 +169,7 @@ export function useControlLibraryState(backendUrlOverride?: string) {
useEffect(() => { loadFrameworks(); loadReviewCount() }, [loadFrameworks, loadReviewCount])
useEffect(() => { loadMeta() }, [loadMeta])
useEffect(() => { loadControls() }, [loadControls])
useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy])
useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, useCaseFilter, primaryOnly, regulationFilter, mappedFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy])
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE))
@@ -212,6 +225,10 @@ export function useControlLibraryState(backendUrlOverride?: string) {
domainFilter, setDomainFilter,
stateFilter, setStateFilter,
verificationFilter, setVerificationFilter,
useCaseFilter, setUseCaseFilter,
primaryOnly, setPrimaryOnly,
regulationFilter, setRegulationFilter,
mappedFilter, setMappedFilter,
categoryFilter, setCategoryFilter,
evidenceTypeFilter, setEvidenceTypeFilter,
audienceFilter, setAudienceFilter,