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>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
|
||||
import { CookieDeclarationDiff } from '../CookieDeclarationDiff'
|
||||
|
||||
const DATA = {
|
||||
coverage: { total: 761, checked: 244, discrepant: 1 },
|
||||
rows: [{
|
||||
cookie: '_ga', vendor: 'Google Analytics', severity: 'HIGH',
|
||||
diffs: [
|
||||
{ field: 'Kategorie', declared: 'notwendig', expected: 'Marketing', severe: true },
|
||||
{ field: 'Laufzeit', declared: 'Session', expected: '2 Jahre' },
|
||||
],
|
||||
measures: ['Als einwilligungspflichtig (§ 25) einstufen.'],
|
||||
}],
|
||||
}
|
||||
|
||||
describe('CookieDeclarationDiff', () => {
|
||||
it('zeigt den Funnel + Feld-Diffs deklariert→Library', () => {
|
||||
render(<CookieDeclarationDiff data={DATA} />)
|
||||
expect(screen.getByText('761')).toBeInTheDocument() // gesamt
|
||||
expect(screen.getByText('244')).toBeInTheDocument() // geprüft
|
||||
expect(screen.getByText('_ga')).toBeInTheDocument()
|
||||
expect(screen.getByText('Kategorie')).toBeInTheDocument()
|
||||
expect(screen.getByText('Marketing')).toBeInTheDocument() // Soll-Wert
|
||||
expect(screen.getByText(/2 Abweichungen/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Als einwilligungspflichtig/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('rendert nichts ohne Daten', () => {
|
||||
const { container } = render(<CookieDeclarationDiff data={undefined} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ import React, { use as useUnwrap, useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { CookieLibraryPanel } from '../../_components/CookieLibraryPanel'
|
||||
import { CookieDeclarationDiff } from '../../_components/CookieDeclarationDiff'
|
||||
import { CookieResultView } from '../../_components/CookieResultView'
|
||||
import { AgentModuleTab } from '../../_components/AgentModuleTab'
|
||||
|
||||
@@ -93,6 +94,7 @@ export default function SnapshotDetail(
|
||||
|
||||
{tab === 'cookie' && hasCookies && (
|
||||
<div className="space-y-4">
|
||||
<CookieDeclarationDiff data={check?.declaration_diff} />
|
||||
<CookieLibraryPanel snapshotId={snapshotId} data={check ?? undefined} />
|
||||
<CookieResultView snapshot={snap} cookieCategories={check?.cookie_categories} storageTypes={check?.storage_inventory?.per_cookie} />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user