feat(mc-browser): reuse Control Library UI for Master Controls

- MC page.tsx imports ControlListView + useControlLibraryState directly
- useControlLibraryState accepts optional backendUrl override
- MC API route returns data in canonical control format
- Same filters, pagination, sorting, click-to-detail as Control Library

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-11 20:02:31 +02:00
parent 6af9353bad
commit b8770e1b9c
3 changed files with 263 additions and 344 deletions
@@ -6,41 +6,46 @@ const pool = new Pool({
})
/**
* GET /api/sdk/v1/master-controls?action=list|detail|members
* MC API that returns data in the same format as the canonical controls
* endpoint. This allows the MC page to reuse ControlListView components.
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action') || 'list'
const endpoint = searchParams.get('endpoint') || 'controls'
if (action === 'list') {
return handleList(searchParams)
}
if (action === 'detail') {
return handleDetail(searchParams)
}
if (action === 'members') {
return handleMembers(searchParams)
}
switch (endpoint) {
case 'frameworks':
return NextResponse.json([])
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
case 'controls':
return handleControls(searchParams)
case 'controls-count':
return handleCount(searchParams)
case 'controls-meta':
return handleMeta(searchParams)
case 'control':
return handleDetail(searchParams)
default:
return NextResponse.json({ error: 'unknown' }, { status: 400 })
}
} catch (e) {
return NextResponse.json({ error: String(e) }, { status: 500 })
}
}
async function handleList(params: URLSearchParams) {
async function handleControls(params: URLSearchParams) {
const search = params.get('search') || ''
const minControls = parseInt(params.get('min_controls') || '0')
const sortBy = params.get('sort') || 'total_controls'
const order = params.get('order') || 'DESC'
const limit = Math.min(parseInt(params.get('limit') || '100'), 500)
const limit = Math.min(parseInt(params.get('limit') || '50'), 200)
const offset = parseInt(params.get('offset') || '0')
const sort = params.get('sort') || 'control_id'
const order = params.get('order') === 'desc' ? 'DESC' : 'ASC'
const validSort = ['total_controls', 'canonical_name', 'created_at'].includes(sortBy) ? sortBy : 'total_controls'
const validOrder = order === 'ASC' ? 'ASC' : 'DESC'
let where = 'WHERE 1=1'
let where = "WHERE 1=1"
const args: unknown[] = []
let idx = 1
@@ -49,81 +54,150 @@ async function handleList(params: URLSearchParams) {
args.push(`%${search}%`)
idx++
}
if (minControls > 0) {
where += ` AND mc.total_controls >= $${idx}`
args.push(minControls)
idx++
}
const countRes = await pool.query(
`SELECT count(*) FROM compliance.master_controls mc ${where}`, args
)
const total = parseInt(countRes.rows[0].count)
const sortCol = sort === 'control_id' ? 'mc.master_control_id' :
sort === 'created_at' ? 'mc.created_at' : 'mc.master_control_id'
args.push(limit, offset)
const res = await pool.query(`
SELECT mc.master_control_id as control_id,
mc.canonical_name as title,
'Master Control mit ' || mc.total_controls || ' Atomic Controls' as objective,
CASE WHEN mc.total_controls > 100 THEN 'high'
WHEN mc.total_controls > 20 THEN 'medium'
ELSE 'low' END as severity,
'master_control' as category,
mc.total_controls,
mc.phases_covered,
mc.id,
mc.created_at
FROM compliance.master_controls mc
${where}
ORDER BY ${sortCol} ${order}
LIMIT $${idx} OFFSET $${idx + 1}
`, args)
// Map to canonical control format
const controls = res.rows.map(r => ({
id: r.id,
control_id: r.control_id,
title: r.title,
objective: r.objective,
severity: r.severity,
category: r.category,
release_state: 'active',
source_citation: null,
verification_method: null,
evidence_type: null,
target_audience: [],
requirements: [],
test_procedure: [],
evidence: [],
open_anchors: [],
total_controls: r.total_controls,
phases_covered: r.phases_covered,
created_at: r.created_at,
}))
return NextResponse.json(controls)
}
async function handleCount(params: URLSearchParams) {
const search = params.get('search') || ''
let where = "WHERE 1=1"
const args: unknown[] = []
if (search) {
where += ` AND mc.canonical_name ILIKE $1`
args.push(`%${search}%`)
}
const res = await pool.query(
`SELECT mc.id, mc.master_control_id, mc.canonical_name,
mc.total_controls, mc.phases_covered, mc.phase_control_count
FROM compliance.master_controls mc
${where}
ORDER BY mc.${validSort} ${validOrder}
LIMIT $${idx} OFFSET $${idx + 1}`,
args
`SELECT count(*) FROM compliance.master_controls mc ${where}`, args
)
return NextResponse.json({ total: parseInt(res.rows[0].count) })
}
async function handleMeta(params: URLSearchParams) {
const res = await pool.query(`
SELECT count(*) as total,
count(CASE WHEN total_controls > 100 THEN 1 END) as high_count,
count(CASE WHEN total_controls BETWEEN 20 AND 100 THEN 1 END) as medium_count,
count(CASE WHEN total_controls < 20 THEN 1 END) as low_count
FROM compliance.master_controls
`)
const r = res.rows[0]
// Get top L1 tokens as "domains"
const domainRes = await pool.query(`
SELECT split_part(canonical_name, '_', 1) as domain, count(*) as count
FROM compliance.master_controls
GROUP BY 1 ORDER BY 2 DESC LIMIT 30
`)
return NextResponse.json({
total,
limit,
offset,
master_controls: res.rows,
total: parseInt(r.total),
severity_counts: {
high: parseInt(r.high_count),
medium: parseInt(r.medium_count),
low: parseInt(r.low_count),
},
domains: domainRes.rows.map(d => ({ domain: d.domain, count: parseInt(d.count) })),
sources: [],
no_source_count: 0,
release_state_counts: { active: parseInt(r.total) },
verification_method_counts: {},
category_counts: {},
evidence_type_counts: {},
})
}
async function handleDetail(params: URLSearchParams) {
const id = params.get('id')
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 })
const res = await pool.query(
`SELECT mc.id, mc.master_control_id, mc.canonical_name,
mc.total_controls, mc.phases_covered, mc.phase_control_count
FROM compliance.master_controls mc
WHERE mc.master_control_id = $1 OR mc.id::text = $1`,
[id]
)
const id = params.get('id') || ''
const res = await pool.query(`
SELECT mc.id, mc.master_control_id as control_id, mc.canonical_name as title,
'Master Control mit ' || mc.total_controls || ' Atomic Controls' as objective,
mc.total_controls, mc.phases_covered, mc.phase_control_count, mc.created_at
FROM compliance.master_controls mc
WHERE mc.master_control_id = $1 OR mc.id::text = $1
`, [id])
if (res.rows.length === 0) {
return NextResponse.json({ error: 'not found' }, { status: 404 })
}
return NextResponse.json({ master_control: res.rows[0] })
}
const mc = res.rows[0]
async function handleMembers(params: URLSearchParams) {
const mcId = params.get('mc_id')
if (!mcId) return NextResponse.json({ error: 'mc_id required' }, { status: 400 })
const limit = Math.min(parseInt(params.get('limit') || '50'), 200)
const offset = parseInt(params.get('offset') || '0')
const res = await pool.query(
`SELECT cc.control_id, cc.title, cc.objective, cc.severity,
mcm.phase, mcm.action,
COALESCE(pc.source_citation::jsonb->>'source', '') as regulation_source,
COALESCE(pc.source_citation::jsonb->>'article', '') as regulation_article
FROM compliance.master_control_members mcm
JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid
LEFT JOIN compliance.canonical_controls pc ON pc.id = cc.parent_control_uuid
WHERE mcm.master_control_uuid = (
SELECT id FROM compliance.master_controls WHERE master_control_id = $1 OR id::text = $1 LIMIT 1
)
ORDER BY mcm.phase, cc.control_id
LIMIT $2 OFFSET $3`,
[mcId, limit, offset]
)
// Load members
const membersRes = await pool.query(`
SELECT cc.control_id, cc.title, cc.severity, mcm.phase, mcm.action
FROM compliance.master_control_members mcm
JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid
WHERE mcm.master_control_uuid = $1
ORDER BY mcm.phase, cc.control_id
LIMIT 100
`, [mc.id])
return NextResponse.json({
mc_id: mcId,
members: res.rows,
count: res.rows.length,
id: mc.id,
control_id: mc.control_id,
title: mc.title,
objective: mc.objective,
severity: mc.total_controls > 100 ? 'high' : mc.total_controls > 20 ? 'medium' : 'low',
category: 'master_control',
release_state: 'active',
total_controls: mc.total_controls,
phases_covered: mc.phases_covered,
phase_control_count: mc.phase_control_count,
members: membersRes.rows,
requirements: membersRes.rows.map((m: { control_id: string; title: string; phase: string }) =>
`[${m.phase}] ${m.control_id}: ${m.title}`
),
test_procedure: [],
evidence: [],
open_anchors: [],
target_audience: [],
source_citation: null,
created_at: mc.created_at,
})
}