feat(ui): Master Controls Browser — 13.5K MCs with member drill-down
- New page /sdk/master-controls with sortable, searchable MC list - Click MC → expandable detail panel with atomic controls - Shows L1 token, L2 subtopic, phase, severity, regulation source - API proxy via pg directly to compliance.master_controls - Sidebar entry added Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL || 'postgresql://breakpilot:breakpilot123@bp-core-postgres:5432/breakpilot_db',
|
||||
})
|
||||
|
||||
/**
|
||||
* GET /api/sdk/v1/master-controls?action=list|detail|members
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const action = searchParams.get('action') || 'list'
|
||||
|
||||
if (action === 'list') {
|
||||
return handleList(searchParams)
|
||||
}
|
||||
if (action === 'detail') {
|
||||
return handleDetail(searchParams)
|
||||
}
|
||||
if (action === 'members') {
|
||||
return handleMembers(searchParams)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: String(e) }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
async function handleList(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 offset = parseInt(params.get('offset') || '0')
|
||||
|
||||
const validSort = ['total_controls', 'canonical_name', 'created_at'].includes(sortBy) ? sortBy : 'total_controls'
|
||||
const validOrder = order === 'ASC' ? 'ASC' : 'DESC'
|
||||
|
||||
let where = 'WHERE 1=1'
|
||||
const args: unknown[] = []
|
||||
let idx = 1
|
||||
|
||||
if (search) {
|
||||
where += ` AND mc.canonical_name ILIKE $${idx}`
|
||||
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)
|
||||
|
||||
args.push(limit, offset)
|
||||
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
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
total,
|
||||
limit,
|
||||
offset,
|
||||
master_controls: res.rows,
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
if (res.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ master_control: 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]
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
mc_id: mcId,
|
||||
members: res.rows,
|
||||
count: res.rows.length,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user