Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/CookieDeclarationDiff.tsx
T
Benjamin Admin 403e3c66d2 feat(cookie): Deklaration-vs-Bibliothek-Diff-Sicht + Funnel-KPI
Für die Library-getroffene Teilmesse (~32%) pro Cookie die Feld-
Abweichungen deklariert→Library (Kategorie/Laufzeit/Zweck) als Diff-Karte,
plus ehrlicher Funnel (gesamt → geprüft → abweichend) — nicht-getroffene
Cookies sind nicht prüfbar (kein Pass/Fail), passend zur Tonalität.

- analyze_cookies: 'expected'-Soll-Wert an tracker_as_necessary/
  excessive_lifetime/missing_purpose (+ _CAT_LABEL_DE).
- neues cookie_declaration_diff.build_declaration_diff: reine Regroup-
  Aggregation der Findings pro Cookie (single source = analyze_cookies),
  Hinweis-Typen (third_country/eu_alternative) bewusst ausgeschlossen.
- cookie-check exponiert out['declaration_diff'].
- CookieDeclarationDiff.tsx oben im Cookie-Tab (vor Panel/ResultView).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 21:00:50 +02:00

105 lines
3.9 KiB
TypeScript

'use client'
/**
* CookieDeclarationDiff — „Deklaration vs. Bibliothek".
*
* Zeigt pro Cookie der GEPRÜFTEN Teilmenge (Library-Treffer) die Feld-
* Abweichungen deklariert → Library, plus einen ehrlichen Funnel
* (gesamt → geprüft → abweichend). Quelle: cookie-check `declaration_diff`.
*/
import React from 'react'
interface Diff {
field: string
declared: string
expected: string
severe?: boolean
}
interface DiffRow {
cookie: string
vendor: string
severity: string
diffs: Diff[]
measures: string[]
}
export interface DeclarationDiffData {
coverage: { total: number; checked: number; discrepant: number }
rows: DiffRow[]
}
const SEV_BADGE: Record<string, string> = {
HIGH: 'bg-red-100 text-red-700',
MEDIUM: 'bg-amber-100 text-amber-700',
LOW: 'bg-gray-100 text-gray-600',
}
function Funnel({ c }: { c: DeclarationDiffData['coverage'] }) {
const pct = c.total > 0 ? Math.round((c.checked / c.total) * 100) : 0
return (
<div className="text-xs text-gray-600 bg-slate-50 border border-gray-200 rounded-lg px-3 py-2">
<span className="font-semibold text-gray-800">{c.total}</span> Cookies ·{' '}
<span className="font-semibold text-gray-800">{c.checked}</span> gegen Bibliothek
geprüft (<span className="font-semibold">{pct}%</span>) · davon{' '}
<span className={`font-semibold ${c.discrepant > 0 ? 'text-red-700' : 'text-green-700'}`}>
{c.discrepant}
</span>{' '}
mit abweichender Deklaration
<div className="text-[10px] text-gray-400 mt-0.5">
Nicht in der Bibliothek enthaltene Cookies sind nicht prüfbar (kein Pass, kein Fail).
</div>
</div>
)
}
export function CookieDeclarationDiff({ data }: { data?: DeclarationDiffData }) {
if (!data || !data.coverage) return null
const { coverage, rows } = data
return (
<div className="space-y-3">
<div className="flex items-baseline justify-between gap-2">
<h3 className="text-sm font-semibold text-gray-900">Deklaration vs. Bibliothek</h3>
</div>
<Funnel c={coverage} />
{rows.length === 0 ? (
<p className="text-xs text-green-700 px-1">
Keine abweichenden Deklarationen in der geprüften Teilmenge.
</p>
) : (
<div className="space-y-2">
{rows.map((r, i) => (
<div key={i} className="border border-gray-200 rounded-lg overflow-hidden">
<div className="flex items-center gap-2 px-3 py-1.5 bg-slate-50 border-b text-xs">
<span className="font-mono font-medium text-gray-800 break-all">{r.cookie}</span>
{r.vendor && <span className="text-gray-400">· {r.vendor}</span>}
<span className="flex-1" />
<span className={`px-1.5 py-0.5 rounded text-[10px] ${SEV_BADGE[r.severity] || SEV_BADGE.LOW}`}>
{r.diffs.length} {r.diffs.length === 1 ? 'Abweichung' : 'Abweichungen'}
</span>
</div>
<div className="px-3 py-2 space-y-1">
{r.diffs.map((d, j) => (
<div key={j} className="flex items-center gap-2 text-[11px]">
<span className="text-gray-500 w-20 shrink-0">{d.field}</span>
<span className="text-gray-600">{d.declared}</span>
<span className="text-gray-400"></span>
<span className={`font-medium ${d.severe ? 'text-red-700' : 'text-gray-900'}`}>
{d.expected}
</span>
</div>
))}
{r.measures.length > 0 && (
<div className="text-[11px] text-blue-700 pt-1 border-t border-gray-100 mt-1">
<span className="font-medium">Maßnahme:</span> {r.measures.join(' ')}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
)
}