feat(use-case-controls): Adressat-Achse — out-of-scope advisory + additiver GOV-Tag

2-Pass-Haiku-Klassifikation (konservativ + Re-Confirm jeder Nicht-unternehmen-
Einstufung) der Review-Tier-Atome: wer muss die Pflicht erfuellen?

- Migration 155: atom_classification.addressee (unternehmen/oeffentliche_stelle/
  aufsichtsbefugnis/staat_eu/dritter/meta), additiv, kein CHECK. [migration-approved]
- Service: addressee + applicable + is_gov pro Control; include_out_of_scope-Param
  (Default false -> out-of-scope advisory ausgeblendet, NIE geloescht); out_of_scope_count.
  Pure Helper addressee_applicable/addressee_is_gov (+ Tests).
- Route: optionaler include_out_of_scope-Query (contract-safe, additiv).
- Frontend: GOV-Chip (additiv) + "kein Kunden-Pruefaspekt"-Chip + 1-Klick-Toggle
  zum Einblenden der out-of-scope-Atome.

Daten: 40.859 Adressat-Tags auf macmini geladen (81% applicable, 19% advisory,
3.146 GOV). Konservativ: NULL/Unklar = applicable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-16 06:58:37 +02:00
parent f6fe592164
commit 0a6e57ac02
7 changed files with 181 additions and 12 deletions
@@ -6,6 +6,7 @@ import {
provenanceBadgeClass,
severityBadgeClass,
splitByTier,
addresseeLabel,
} from '../_helpers'
const BACKEND_URL =
@@ -13,12 +14,15 @@ const BACKEND_URL =
export const dynamic = 'force-dynamic'
async function getControls(useCase: string): Promise<ControlsResponse | null> {
async function getControls(
useCase: string,
oos: boolean,
): Promise<ControlsResponse | null> {
try {
const res = await fetch(
`${BACKEND_URL}/api/compliance/v1/controls/use-cases/${encodeURIComponent(
useCase,
)}/controls?tier=all&limit=200`,
)}/controls?tier=all&limit=200&include_out_of_scope=${oos}`,
{ cache: 'no-store' },
)
return res.ok ? ((await res.json()) as ControlsResponse) : null
@@ -42,7 +46,23 @@ function ControlsTable({ rows }: { rows: ControlItem[] }) {
<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-gray-900">
<div>{c.title}</div>
{c.is_gov || !c.applicable ? (
<div className="mt-1 flex flex-wrap gap-1">
{c.is_gov ? (
<span className="rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-medium text-emerald-800">
GOV · Öffentliche Stelle
</span>
) : null}
{!c.applicable ? (
<span className="rounded bg-amber-100 px-1.5 py-0.5 text-[10px] font-medium text-amber-800">
kein Kunden-Prüfaspekt · {addresseeLabel(c.addressee)}
</span>
) : null}
</div>
) : null}
</td>
<td className="px-4 py-2 text-xs text-gray-500">
{c.sub_topic || '—'}
</td>
@@ -77,11 +97,15 @@ function ControlsTable({ rows }: { rows: ControlItem[] }) {
export default async function UseCaseControlsPage({
params,
searchParams,
}: {
params: Promise<{ useCase: string }>
searchParams: Promise<{ [k: string]: string | string[] | undefined }>
}) {
const { useCase } = await params
const data = await getControls(useCase)
const sp = await searchParams
const oos = sp.oos === '1'
const data = await getControls(useCase, oos)
if (!data) {
return (
@@ -123,6 +147,25 @@ export default async function UseCaseControlsPage({
</span>{' '}
zur fachlichen Prüfung
</p>
{data.out_of_scope_count > 0 ? (
<p className="text-xs">
{data.include_out_of_scope ? (
<Link
href={`/sdk/coverage/${useCase}`}
className="text-purple-600 hover:underline"
>
← Nur Kunden-Prüfaspekte ({data.out_of_scope_count.toLocaleString('de-DE')} Behörde/Mitgliedstaat/Dritter ausblenden)
</Link>
) : (
<Link
href={`/sdk/coverage/${useCase}?oos=1`}
className="text-amber-700 hover:underline"
>
+ {data.out_of_scope_count.toLocaleString('de-DE')} ausgeblendet (kein Kunden-Prüfaspekt: Behörde/Mitgliedstaat/Dritter) — einblenden
</Link>
)}
</p>
) : null}
</header>
<section className="space-y-2">