feat(use-case-controls): relevant als Stufe statt Hard-Filter + Provenance
CI / test-python-backend (push) Successful in 30s
CI / test-python-document-crawler (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / test-python-dsms-gateway (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) Successful in 12s
CI / validate-canonical-controls (push) Successful in 12s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / detect-changes (push) Successful in 15s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 30s
CI / test-python-document-crawler (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / test-python-dsms-gateway (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) Successful in 12s
CI / validate-canonical-controls (push) Successful in 12s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / detect-changes (push) Successful in 15s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / nodejs-build (push) Successful in 3m9s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
Der harte relevant=true-Filter versteckte ~25% des Korpus (40.926 Atome), ~70% davon echte Pflichten (500er-Validierung). relevant wird zur Stufe: - Service: tier-Param (core=Default schuetzt Agent/CRA; all=alles inkl. review), ORDER BY relevant DESC; pro Control relevant/tier/source_type (own_library bei license_rule=3, sonst derived) + source_regulation/article; core_count/review_count. Pure Helper tier_label + source_type (+ Tests). - Route: optionaler tier-Query (default core) — contract-safe (additiv). - Frontend: Coverage-Drill-down /sdk/coverage/[useCase] — Kern-Pflichten vs. "zur fachlichen Pruefung", je mit Herkunfts-Badge; Uebersicht zeigt Delta. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
type ControlsResponse,
|
||||
type ControlItem,
|
||||
provenanceLabel,
|
||||
provenanceBadgeClass,
|
||||
severityBadgeClass,
|
||||
splitByTier,
|
||||
} from '../_helpers'
|
||||
|
||||
const BACKEND_URL =
|
||||
process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
async function getControls(useCase: string): Promise<ControlsResponse | null> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${BACKEND_URL}/api/compliance/v1/controls/use-cases/${encodeURIComponent(
|
||||
useCase,
|
||||
)}/controls?tier=all&limit=200`,
|
||||
{ cache: 'no-store' },
|
||||
)
|
||||
return res.ok ? ((await res.json()) as ControlsResponse) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function ControlsTable({ rows }: { rows: ControlItem[] }) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200">
|
||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
|
||||
<tr>
|
||||
<th className="px-4 py-2">Prüfaspekt</th>
|
||||
<th className="px-4 py-2">Sub-Thema</th>
|
||||
<th className="px-4 py-2">Schwere</th>
|
||||
<th className="px-4 py-2">Herkunft</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{rows.map((c) => (
|
||||
<tr key={c.id}>
|
||||
<td className="px-4 py-2 text-gray-900">{c.title}</td>
|
||||
<td className="px-4 py-2 text-xs text-gray-500">
|
||||
{c.sub_topic || '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{c.severity ? (
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs font-medium ${severityBadgeClass(
|
||||
c.severity,
|
||||
)}`}
|
||||
>
|
||||
{c.severity}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs font-medium ${provenanceBadgeClass(
|
||||
c.source_type,
|
||||
)}`}
|
||||
title={c.source_article || undefined}
|
||||
>
|
||||
{provenanceLabel(c)}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function UseCaseControlsPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ useCase: string }>
|
||||
}) {
|
||||
const { useCase } = await params
|
||||
const data = await getControls(useCase)
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl space-y-4 p-6">
|
||||
<Link
|
||||
href="/sdk/coverage"
|
||||
className="text-sm text-purple-600 hover:underline"
|
||||
>
|
||||
← Abdeckung
|
||||
</Link>
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-6 text-amber-800">
|
||||
Keine atom-grain-Daten für „{useCase}" gefunden.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { core, review } = splitByTier(data.controls)
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl space-y-8 p-6">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||
<Link href="/sdk/coverage" className="hover:text-purple-600">
|
||||
Abdeckung
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-gray-900">{data.label}</span>
|
||||
</div>
|
||||
|
||||
<header className="space-y-1">
|
||||
<h1 className="text-2xl font-bold text-gray-900">{data.label}</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
<span className="font-semibold text-gray-900">
|
||||
{data.core_count.toLocaleString('de-DE')}
|
||||
</span>{' '}
|
||||
Kern-Pflichten ·{' '}
|
||||
<span className="font-semibold text-gray-900">
|
||||
{data.review_count.toLocaleString('de-DE')}
|
||||
</span>{' '}
|
||||
zur fachlichen Prüfung
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Kern-Pflichten ({core.length})
|
||||
</h2>
|
||||
{core.length ? (
|
||||
<ControlsTable rows={core} />
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">—</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="space-y-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Zur fachlichen Prüfung ({review.length})
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500">
|
||||
Breiter gefasste Aspekte — bewusst eher zu viel als zu wenig. Fachlich
|
||||
prüfen; nicht zutreffende lassen sich (künftig) als unanwendbar
|
||||
markieren.
|
||||
</p>
|
||||
</div>
|
||||
{review.length ? (
|
||||
<ControlsTable rows={review} />
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">—</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{data.total > data.controls.length ? (
|
||||
<p className="text-xs text-gray-400">
|
||||
Angezeigt: erste {data.controls.length.toLocaleString('de-DE')} von{' '}
|
||||
{data.total.toLocaleString('de-DE')} — Sub-Thema-Filter folgt.
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user