926dc02a09
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>
242 lines
9.9 KiB
TypeScript
242 lines
9.9 KiB
TypeScript
import Link from 'next/link'
|
||
import {
|
||
type UseCaseRow,
|
||
type CorpusOverview,
|
||
licenseTierBadgeClass,
|
||
commercialBadgeClass,
|
||
groupUseCases,
|
||
} from './_helpers'
|
||
|
||
const BACKEND_URL =
|
||
process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
async function getData(): Promise<{
|
||
useCases: UseCaseRow[]
|
||
corpus: CorpusOverview | null
|
||
}> {
|
||
try {
|
||
const [ucRes, corpusRes] = await Promise.all([
|
||
fetch(`${BACKEND_URL}/api/compliance/v1/controls/use-cases`, {
|
||
cache: 'no-store',
|
||
}),
|
||
fetch(`${BACKEND_URL}/api/compliance/v1/controls/corpus`, {
|
||
cache: 'no-store',
|
||
}),
|
||
])
|
||
return {
|
||
useCases: ucRes.ok ? await ucRes.json() : [],
|
||
corpus: corpusRes.ok ? await corpusRes.json() : null,
|
||
}
|
||
} catch {
|
||
return { useCases: [], corpus: null }
|
||
}
|
||
}
|
||
|
||
function Stat({ label, value }: { label: string; value: string | number }) {
|
||
return (
|
||
<div className="rounded-lg border border-gray-200 bg-white px-4 py-3">
|
||
<div className="text-2xl font-semibold text-gray-900">{value}</div>
|
||
<div className="text-xs text-gray-500">{label}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default async function CoveragePage() {
|
||
const { useCases, corpus } = await getData()
|
||
const groups = groupUseCases(useCases)
|
||
const totalRelevant = useCases.reduce((s, u) => s + u.atom_relevant, 0)
|
||
const totalAtoms = useCases.reduce((s, u) => s + u.atom_total, 0)
|
||
const totalReview = totalAtoms - totalRelevant
|
||
|
||
return (
|
||
<div className="mx-auto max-w-7xl space-y-10 p-6">
|
||
<header className="space-y-1">
|
||
<h1 className="text-2xl font-bold text-gray-900">
|
||
Compliance-Abdeckung
|
||
</h1>
|
||
<p className="text-sm text-gray-600">
|
||
Alle ableitbaren Use Cases und alle Quell-Dokumente im Korpus inkl.
|
||
Lizenz — damit kein Thema und keine Quelle vergessen wird.
|
||
</p>
|
||
</header>
|
||
|
||
<section className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||
<Stat label="Use Cases" value={useCases.length} />
|
||
<Stat label="Kern-Pflichten" value={totalRelevant.toLocaleString('de-DE')} />
|
||
<Stat label="zur Prüfung" value={totalReview.toLocaleString('de-DE')} />
|
||
<Stat label="Quell-Dokumente" value={corpus?.totals.documents ?? 0} />
|
||
</section>
|
||
|
||
{corpus?.license_summary?.length ? (
|
||
<section className="space-y-2">
|
||
<h2 className="text-lg font-semibold text-gray-900">Lizenz-Verteilung</h2>
|
||
<div className="flex flex-wrap gap-3">
|
||
{corpus.license_summary.map((l) => (
|
||
<div
|
||
key={String(l.license_rule)}
|
||
className="flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2"
|
||
>
|
||
<span
|
||
className={`rounded px-2 py-0.5 text-xs font-medium ${licenseTierBadgeClass(l.license_rule)}`}
|
||
>
|
||
Tier {l.license_rule ?? '?'}
|
||
</span>
|
||
<span className="text-sm text-gray-700">{l.label}</span>
|
||
<span className="text-sm font-semibold text-gray-900">
|
||
{l.atom_count.toLocaleString('de-DE')}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
) : null}
|
||
|
||
<section className="space-y-4">
|
||
<h2 className="text-lg font-semibold text-gray-900">Use Cases</h2>
|
||
{groups.map((g) => (
|
||
<div key={g.group} className="space-y-1">
|
||
<h3 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
|
||
{g.label} ({g.rows.length})
|
||
</h3>
|
||
<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">Use Case</th>
|
||
<th className="px-4 py-2">Key</th>
|
||
<th className="px-4 py-2 text-right">Kern</th>
|
||
<th className="px-4 py-2 text-right">zur Prüfung</th>
|
||
<th className="px-4 py-2 text-right">gesamt</th>
|
||
<th className="px-4 py-2">Quellen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100 bg-white">
|
||
{g.rows.map((u) => (
|
||
<tr key={u.key} className={u.atom_total === 0 ? 'text-gray-400' : ''}>
|
||
<td className="px-4 py-2 font-medium">
|
||
<Link
|
||
href={`/sdk/coverage/${u.key}`}
|
||
className="text-purple-700 hover:underline"
|
||
>
|
||
{u.label}
|
||
</Link>
|
||
</td>
|
||
<td className="px-4 py-2 font-mono text-xs text-gray-500">{u.key}</td>
|
||
<td className="px-4 py-2 text-right font-semibold">
|
||
{u.atom_relevant.toLocaleString('de-DE')}
|
||
</td>
|
||
<td className="px-4 py-2 text-right text-amber-700">
|
||
{(u.atom_total - u.atom_relevant).toLocaleString('de-DE')}
|
||
</td>
|
||
<td className="px-4 py-2 text-right text-gray-500">
|
||
{u.atom_total.toLocaleString('de-DE')}
|
||
</td>
|
||
<td className="px-4 py-2 text-xs text-gray-500">
|
||
{u.regulations.slice(0, 4).join(', ')}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</section>
|
||
|
||
<section className="space-y-2">
|
||
<h2 className="text-lg font-semibold text-gray-900">
|
||
Korpus-Dokumente ({corpus?.documents.length ?? 0})
|
||
</h2>
|
||
<p className="text-xs text-gray-500">
|
||
Quell-Regulierung × Lizenz-Tier × Anzahl Pflichten × gemappter Use Case.
|
||
</p>
|
||
<div className="max-h-[28rem] overflow-auto rounded-lg border border-gray-200">
|
||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||
<thead className="sticky top-0 bg-gray-50 text-left text-xs uppercase text-gray-500">
|
||
<tr>
|
||
<th className="px-4 py-2">Dokument / Quelle</th>
|
||
<th className="px-4 py-2">Lizenz</th>
|
||
<th className="px-4 py-2 text-right">Pflichten</th>
|
||
<th className="px-4 py-2">Use Case</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100 bg-white">
|
||
{(corpus?.documents ?? []).map((d) => (
|
||
<tr key={d.source_regulation}>
|
||
<td className="px-4 py-2 text-gray-900">{d.source_regulation}</td>
|
||
<td className="px-4 py-2">
|
||
<span
|
||
className={`rounded px-2 py-0.5 text-xs font-medium ${licenseTierBadgeClass(d.license_rule)}`}
|
||
title={d.license_tier}
|
||
>
|
||
Tier {d.license_rule ?? '?'}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-2 text-right">{d.atom_count.toLocaleString('de-DE')}</td>
|
||
<td className="px-4 py-2 font-mono text-xs text-gray-600">
|
||
{d.use_case ?? <span className="text-amber-600">— ungemappt</span>}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{corpus?.license_catalog?.length ? (
|
||
<section className="space-y-2">
|
||
<h2 className="text-lg font-semibold text-gray-900">
|
||
Lizenz-Katalog ({corpus.license_catalog.length} kuratierte Quellen)
|
||
</h2>
|
||
<p className="text-xs text-gray-500">
|
||
Detaillierte Nutzungsrechte je kuratierter Quelle (kommerzielle
|
||
Nutzung, Auslieferung im Produkt).
|
||
</p>
|
||
<div className="overflow-auto 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">Quelle</th>
|
||
<th className="px-4 py-2">Herausgeber</th>
|
||
<th className="px-4 py-2">Lizenz</th>
|
||
<th className="px-4 py-2">kommerziell</th>
|
||
<th className="px-4 py-2">im Produkt</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100 bg-white">
|
||
{corpus.license_catalog.map((c) => (
|
||
<tr key={c.source_id}>
|
||
<td className="px-4 py-2 text-gray-900">
|
||
{c.terms_url ? (
|
||
<a href={c.terms_url} target="_blank" rel="noreferrer" className="hover:underline">
|
||
{c.title}
|
||
</a>
|
||
) : (
|
||
c.title
|
||
)}
|
||
</td>
|
||
<td className="px-4 py-2 text-gray-600">{c.publisher ?? '—'}</td>
|
||
<td className="px-4 py-2 text-gray-600">{c.license_name ?? c.license_id ?? '—'}</td>
|
||
<td className="px-4 py-2">
|
||
<span
|
||
className={`rounded px-2 py-0.5 text-xs font-medium ${commercialBadgeClass(c.commercial_use)}`}
|
||
>
|
||
{c.commercial_use ?? 'unbekannt'}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-2 text-gray-600">
|
||
{c.ship_in_product ? 'ja' : 'nein'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
) : null}
|
||
</div>
|
||
)
|
||
}
|