diff --git a/admin-compliance/app/api/sdk/v1/canonical/route.ts b/admin-compliance/app/api/sdk/v1/canonical/route.ts index 5b1b608..06c87c7 100644 --- a/admin-compliance/app/api/sdk/v1/canonical/route.ts +++ b/admin-compliance/app/api/sdk/v1/canonical/route.ts @@ -26,7 +26,7 @@ export async function GET(request: NextRequest) { case 'controls': { const controlParams = new URLSearchParams() - const passthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', + const passthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type', 'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates', 'sort', 'order', 'limit', 'offset'] for (const key of passthrough) { const val = searchParams.get(key) @@ -39,7 +39,7 @@ export async function GET(request: NextRequest) { case 'controls-count': { const countParams = new URLSearchParams() - const countPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', + const countPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', 'evidence_type', 'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates'] for (const key of countPassthrough) { const val = searchParams.get(key) diff --git a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx index d4a047f..b0386c6 100644 --- a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx +++ b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx @@ -8,10 +8,10 @@ import { } from 'lucide-react' import { CanonicalControl, EFFORT_LABELS, BACKEND_URL, - SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, + SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, ObligationTypeBadge, GenerationStrategyBadge, ExtractionMethodBadge, RegulationCountBadge, - VERIFICATION_METHODS, CATEGORY_OPTIONS, + VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS, ObligationInfo, DocumentReference, MergedDuplicate, RegulationSummary, } from './helpers' @@ -185,6 +185,7 @@ export function ControlDetail({ + diff --git a/admin-compliance/app/sdk/control-library/components/helpers.tsx b/admin-compliance/app/sdk/control-library/components/helpers.tsx index cc460c3..dd50c58 100644 --- a/admin-compliance/app/sdk/control-library/components/helpers.tsx +++ b/admin-compliance/app/sdk/control-library/components/helpers.tsx @@ -44,6 +44,7 @@ export interface CanonicalControl { customer_visible?: boolean verification_method: string | null category: string | null + evidence_type: string | null target_audience: string | string[] | null generation_metadata?: Record | null generation_strategy?: string | null @@ -102,6 +103,7 @@ export const EMPTY_CONTROL = { tags: [] as string[], verification_method: null as string | null, category: null as string | null, + evidence_type: null as string | null, target_audience: null as string | null, } @@ -145,6 +147,18 @@ export const CATEGORY_OPTIONS = [ { value: 'identity', label: 'Identitaetsmanagement' }, ] +export const EVIDENCE_TYPE_CONFIG: Record = { + code: { bg: 'bg-sky-100 text-sky-700', label: 'Code' }, + process: { bg: 'bg-amber-100 text-amber-700', label: 'Prozess' }, + hybrid: { bg: 'bg-violet-100 text-violet-700', label: 'Hybrid' }, +} + +export const EVIDENCE_TYPE_OPTIONS = [ + { value: 'code', label: 'Code — Technisch (Source Code, IaC, CI/CD)' }, + { value: 'process', label: 'Prozess — Organisatorisch (Dokumente, Policies)' }, + { value: 'hybrid', label: 'Hybrid — Code + Prozess' }, +] + export const TARGET_AUDIENCE_OPTIONS: Record = { // Legacy English keys enterprise: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' }, @@ -244,6 +258,13 @@ export function CategoryBadge({ category }: { category: string | null }) { ) } +export function EvidenceTypeBadge({ type }: { type: string | null }) { + if (!type) return null + const config = EVIDENCE_TYPE_CONFIG[type] + if (!config) return null + return {config.label} +} + export function TargetAudienceBadge({ audience }: { audience: string | string[] | null }) { if (!audience) return null diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 07549c9..3fe398d 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -8,9 +8,9 @@ import { } from 'lucide-react' import { CanonicalControl, Framework, BACKEND_URL, EMPTY_CONTROL, - SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, + SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, GenerationStrategyBadge, ObligationTypeBadge, - VERIFICATION_METHODS, CATEGORY_OPTIONS, TARGET_AUDIENCE_OPTIONS, + VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS, TARGET_AUDIENCE_OPTIONS, } from './components/helpers' import { ControlForm } from './components/ControlForm' import { ControlDetail } from './components/ControlDetail' @@ -51,6 +51,7 @@ export default function ControlLibraryPage() { const [stateFilter, setStateFilter] = useState('') const [verificationFilter, setVerificationFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') + const [evidenceTypeFilter, setEvidenceTypeFilter] = useState('') const [audienceFilter, setAudienceFilter] = useState('') const [sourceFilter, setSourceFilter] = useState('') const [typeFilter, setTypeFilter] = useState('') @@ -94,6 +95,7 @@ export default function ControlLibraryPage() { if (stateFilter) p.set('release_state', stateFilter) if (verificationFilter) p.set('verification_method', verificationFilter) if (categoryFilter) p.set('category', categoryFilter) + if (evidenceTypeFilter) p.set('evidence_type', evidenceTypeFilter) if (audienceFilter) p.set('target_audience', audienceFilter) if (sourceFilter) p.set('source', sourceFilter) if (typeFilter) p.set('control_type', typeFilter) @@ -101,7 +103,7 @@ export default function ControlLibraryPage() { 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, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) + }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) // Load metadata (domains, sources — once + on refresh) const loadMeta = useCallback(async () => { @@ -169,7 +171,7 @@ export default function ControlLibraryPage() { useEffect(() => { loadControls() }, [loadControls]) // Reset page when filters change - useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy]) + useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy]) // Pagination const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) @@ -654,6 +656,16 @@ export default function ControlLibraryPage() { ))} +