From 1866bb11ae7ca3cddfa3641443731c7568d97c13 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 12 May 2026 00:24:16 +0200 Subject: [PATCH] feat(mc-browser): MC Detail with member controls + phase filter Replace ControlDetail (empty for MCs) with MCDetail panel showing: - MC name, ID, total controls count - Phase badges as clickable filters - Member controls list with severity, phase, action, regulation source - Filter by lifecycle phase (definition, implementation, testing, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/sdk/master-controls/page.tsx | 181 ++++++++++++++---- 1 file changed, 142 insertions(+), 39 deletions(-) diff --git a/admin-compliance/app/sdk/master-controls/page.tsx b/admin-compliance/app/sdk/master-controls/page.tsx index 9de2d64..84bdf9c 100644 --- a/admin-compliance/app/sdk/master-controls/page.tsx +++ b/admin-compliance/app/sdk/master-controls/page.tsx @@ -1,9 +1,8 @@ 'use client' -import { ControlDetail } from '../control-library/components/ControlDetail' +import React, { useState, useEffect } from 'react' import { ControlListView } from '../control-library/components/ControlListView' import { useControlLibraryState } from '../control-library/components/useControlLibraryState' -import { BACKEND_URL } from '../control-library/components/helpers' /** * Master Controls page — reuses the Control Library UI exactly, @@ -35,46 +34,12 @@ export default function MasterControlsPage() { ) } - // DETAIL mode — add fallback fields that ControlDetail expects + // DETAIL mode — show MC members if (state.mode === 'detail' && state.selectedControl) { - const c = state.selectedControl - const safeCtrl = { - ...c, - scope: c.scope || { platforms: [], components: [], data_classes: [] }, - target_audience: c.target_audience || [], - requirements: c.requirements || [], - test_procedure: c.test_procedure || [], - evidence: c.evidence || [], - open_anchors: c.open_anchors || [], - tags: c.tags || [], - risk_score: c.risk_score ?? null, - implementation_effort: c.implementation_effort ?? null, - generation_metadata: c.generation_metadata || null, - source_citation: c.source_citation || null, - source_original_text: c.source_original_text || '', - verification_method: c.verification_method || null, - evidence_type: c.evidence_type || null, - release_state: c.release_state || 'active', - category: c.category || 'master_control', - severity: c.severity || 'medium', - parent_control_uuid: c.parent_control_uuid || null, - similar_controls: c.similar_controls || [], - } return ( - { state.setMode('list'); state.setSelectedControl(null) }} - onEdit={() => {}} - onDelete={async () => {}} - onReview={async () => {}} - onRefresh={state.fullReload} - onCompare={() => {}} - onNavigateToControl={async (controlId: string) => { - try { - const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${controlId}`) - if (res.ok) { state.setSelectedControl(await res.json()); state.setMode('detail') } - } catch { /* ignore */ } - }} /> ) } @@ -131,3 +96,141 @@ export default function MasterControlsPage() { /> ) } + +// ── MC Detail Panel ───────────────────────────────────────────── + +interface Member { + control_id: string + title: string + severity: string + phase: string + action: string + regulation_source?: string + regulation_article?: string +} + +const SEV = { + critical: 'bg-red-100 text-red-800', + high: 'bg-orange-100 text-orange-800', + medium: 'bg-yellow-100 text-yellow-800', + low: 'bg-blue-100 text-blue-800', +} as Record + +function MCDetail({ mc, onBack }: { mc: Record; onBack: () => void }) { + const [members, setMembers] = useState([]) + const [loading, setLoading] = useState(true) + const [phaseFilter, setPhaseFilter] = useState('') + + const mcId = (mc.control_id || mc.master_control_id || '') as string + const mcName = (mc.title || mc.canonical_name || '') as string + const totalControls = (mc.total_controls || 0) as number + const phases = (mc.phases_covered || []) as string[] + + useEffect(() => { + setLoading(true) + fetch(`/api/sdk/v1/master-controls?endpoint=control&id=${mcId}`) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data?.members) setMembers(data.members) + else if (data?.requirements) { + // Fallback: parse requirements strings + setMembers((data.requirements as string[]).map((req: string) => { + const match = req.match(/^\[(\w+)\]\s+(\S+):\s+(.+)$/) + return match + ? { control_id: match[2], title: match[3], phase: match[1], action: '', severity: '' } + : { control_id: '', title: req, phase: '', action: '', severity: '' } + })) + } + }) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [mcId]) + + const filtered = phaseFilter ? members.filter(m => m.phase === phaseFilter) : members + const uniquePhases = [...new Set(members.map(m => m.phase).filter(Boolean))] + const phaseGroups = uniquePhases.reduce((acc, p) => { + acc[p] = members.filter(m => m.phase === p).length + return acc + }, {} as Record) + + return ( +
+ {/* Header */} + + +
+

{mcName}

+

{mcId} — {totalControls} Atomic Controls

+ + {/* Phase badges */} +
+ {uniquePhases.map(p => ( + + ))} + {phaseFilter && ( + + )} +
+
+ + {/* Members */} +
+
+ {filtered.length} von {members.length} Controls{phaseFilter ? ` (Phase: ${phaseFilter})` : ''} +
+ + {loading ? ( +
+
+
+ ) : ( +
+ {filtered.map((m, i) => ( +
+
+ {m.control_id} + {m.severity && ( + + {m.severity} + + )} + {m.phase && ( + + {m.phase} + + )} + {m.action && ( + {m.action} + )} +
+

{m.title}

+ {m.regulation_source && ( +

+ {m.regulation_source} {m.regulation_article} +

+ )} +
+ ))} + {filtered.length === 0 && !loading && ( +
Keine Controls gefunden
+ )} +
+ )} +
+
+ ) +}