dfac940272
Backend
- backend-compliance/compliance/api/licenses_routes.py: three endpoints
built on the now-complete license_rule classification
- GET /api/compliance/licenses/overview
global aggregation by rule + per-source breakdown (Stufe 1)
- POST /api/compliance/licenses/aggregate
per-control-set aggregation for PDF footer (Stufe 2) and
tech-file appendix (Stufe 4) — consumed later
- GET /api/compliance/licenses/source-info/{control_uuid}
single-control lookup for the inline source badge (Stufe 3)
- registered in api/__init__.py via the existing safe-import loader
Frontend
- app/sdk/licenses/page.tsx (Stufe 1): the /sdk/licenses overview page.
Renders rule legend cards + per-rule source tables. Drives the
/licenses footer link and gives auditors a one-page view of what
licence classes the platform is operating under.
- components/sdk/SourceBadge.tsx (Stufe 3): reusable React component.
Small R1/R2/R3 pill with click-expand tooltip showing source
regulation + attribution string + render-full-text policy. Will be
embedded into IACE hazards/mitigations, VVT items, DSFA controls in
follow-up commits.
Two stages of the four-stage renderer are now ready. Stufe 2 (PDF
auto-footer) + Stufe 4 (tech-file appendix) follow once the existing
PDF generators are extended to call /licenses/aggregate.
161 lines
6.0 KiB
TypeScript
161 lines
6.0 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useState } from 'react'
|
||
|
||
// Stufe 1 of the Attribution Renderer (Task #23): the global
|
||
// "Quellen & Lizenzen" overview. Aggregates all 314k canonical_controls
|
||
// by their license_rule and shows the source regulations behind each
|
||
// bucket. Drives the footer link and gives auditors a one-page view of
|
||
// what licence classes the platform is operating under.
|
||
|
||
type SourceCount = {
|
||
regulation_id: string
|
||
regulation_name_de: string | null
|
||
license_rule: number
|
||
license_type: string | null
|
||
attribution: string | null
|
||
jurisdiction: string | null
|
||
source_type: string | null
|
||
n_controls: number
|
||
}
|
||
|
||
type RuleBucket = {
|
||
rule: number
|
||
label_de: string
|
||
label_en: string
|
||
attribution_required: boolean
|
||
render_full_text: boolean
|
||
total_controls: number
|
||
distinct_sources: number
|
||
sources: SourceCount[]
|
||
}
|
||
|
||
type Overview = {
|
||
total_controls: number
|
||
buckets: RuleBucket[]
|
||
}
|
||
|
||
const RULE_COLOR: Record<number, string> = {
|
||
1: 'border-emerald-200 bg-emerald-50',
|
||
2: 'border-amber-200 bg-amber-50',
|
||
3: 'border-slate-200 bg-slate-50',
|
||
}
|
||
|
||
const RULE_BADGE: Record<number, string> = {
|
||
1: 'bg-emerald-600 text-white',
|
||
2: 'bg-amber-600 text-white',
|
||
3: 'bg-slate-600 text-white',
|
||
}
|
||
|
||
export default function LicensesPage() {
|
||
const [data, setData] = useState<Overview | null>(null)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
useEffect(() => {
|
||
fetch('/api/sdk/v1/compliance/licenses/overview')
|
||
.then((r) => (r.ok ? r.json() : Promise.reject(`HTTP ${r.status}`)))
|
||
.then(setData)
|
||
.catch((e) => setError(String(e)))
|
||
}, [])
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="p-6">
|
||
<h1 className="text-xl font-semibold mb-2">Quellen & Lizenzen</h1>
|
||
<p className="text-red-600">Fehler beim Laden: {error}</p>
|
||
</div>
|
||
)
|
||
}
|
||
if (!data) {
|
||
return (
|
||
<div className="p-6">
|
||
<h1 className="text-xl font-semibold">Quellen & Lizenzen</h1>
|
||
<p className="text-slate-500 mt-2">Lade …</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="p-6 max-w-7xl">
|
||
<header className="mb-6">
|
||
<h1 className="text-2xl font-semibold">Quellen & Lizenzen</h1>
|
||
<p className="text-sm text-slate-600 mt-1">
|
||
Diese Plattform stützt sich auf {data.total_controls.toLocaleString('de-DE')}{' '}
|
||
klassifizierte Compliance-Controls aus den unten genannten Quellen.
|
||
Jeder Control trägt eine deterministische Lizenzregel (R1–R3), die das
|
||
Render-Verhalten in Berichten und im Frontend steuert.
|
||
</p>
|
||
</header>
|
||
|
||
<section className="mb-8">
|
||
<h2 className="text-lg font-medium mb-3">Klassifizierungs-Schema</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3 text-sm">
|
||
{data.buckets.map((b) => (
|
||
<div key={b.rule} className={`rounded border ${RULE_COLOR[b.rule] ?? 'border-slate-200'} p-3`}>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className={`inline-flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold ${RULE_BADGE[b.rule] ?? 'bg-slate-600 text-white'}`}>
|
||
R{b.rule}
|
||
</span>
|
||
<span className="font-medium">{b.label_de}</span>
|
||
</div>
|
||
<ul className="text-xs text-slate-700 space-y-1">
|
||
<li>{b.total_controls.toLocaleString('de-DE')} Controls</li>
|
||
<li>{b.distinct_sources} Quellen</li>
|
||
<li>{b.render_full_text ? 'Volltext-Anzeige erlaubt' : 'Nur Identifier-Verweis'}</li>
|
||
<li>{b.attribution_required ? 'Attribution-Pflicht in Output' : 'keine Attribution-Pflicht'}</li>
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{data.buckets.map((b) => (
|
||
<section key={b.rule} className="mb-8">
|
||
<h2 className="text-lg font-medium mb-3 flex items-center gap-2">
|
||
<span className={`inline-flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold ${RULE_BADGE[b.rule] ?? 'bg-slate-600 text-white'}`}>
|
||
R{b.rule}
|
||
</span>
|
||
{b.label_de}{' '}
|
||
<span className="text-sm text-slate-500 font-normal">
|
||
({b.total_controls.toLocaleString('de-DE')} Controls aus {b.distinct_sources} Quellen)
|
||
</span>
|
||
</h2>
|
||
|
||
<div className="overflow-x-auto border rounded">
|
||
<table className="w-full text-sm">
|
||
<thead className="bg-slate-100 text-slate-700">
|
||
<tr>
|
||
<th className="text-left p-2">Quelle</th>
|
||
<th className="text-left p-2">Lizenztyp</th>
|
||
<th className="text-left p-2">Rechtsraum</th>
|
||
<th className="text-left p-2">Attribution</th>
|
||
<th className="text-right p-2">Controls</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{b.sources.map((s) => (
|
||
<tr key={`${b.rule}-${s.regulation_id}`} className="border-t">
|
||
<td className="p-2">{s.regulation_name_de ?? s.regulation_id}</td>
|
||
<td className="p-2 text-slate-600">{s.license_type ?? '—'}</td>
|
||
<td className="p-2 text-slate-600">{s.jurisdiction ?? '—'}</td>
|
||
<td className="p-2 text-slate-600">{s.attribution ?? '—'}</td>
|
||
<td className="p-2 text-right tabular-nums">{s.n_controls.toLocaleString('de-DE')}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
))}
|
||
|
||
<footer className="text-xs text-slate-500 border-t pt-4 mt-8">
|
||
Klassifizierung: deterministisch über parent_control_uuid-Vererbung,
|
||
control_parent_links → regulation_registry, source_citation,
|
||
canonical_processed_chunks (Pipeline-Ground-Truth) und LLM-Aggregat-
|
||
Identifikation für eigene Werke. Audit-Skripte unter
|
||
breakpilot-core/control-pipeline/scripts/.
|
||
</footer>
|
||
</div>
|
||
)
|
||
}
|