901de1ca97
Jeder Cookie-Befund traegt jetzt ein strukturiertes control-Feld (control_id aus doc_check_controls + regulation + article) statt nur hardcodeter Strings: vague_duration->AUTH-2051-A03 (Art.5(1)e+13), tracker_as_necessary->DATA-2851-A05 (§25 TDDDG), third_country-> DATA-1624-A04 (Art.44). Kette Regulation->Article->Control->Finding. Frontend zeigt die Rechtsgrundlage je Befund. (Controls tragen regulation/article noch NULL -> hier mitgeliefert bis gepflegt.) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* CookieLibraryPanel — Pro-Cookie-Abgleich gegen die Knowledge-Library:
|
|
* findet als „notwendig" deklarierte Tracker + fehlende Zwecke und zeigt je
|
|
* Befund die Abstellmaßnahme. Lädt aus dem Snapshot (kein Re-Crawl).
|
|
*/
|
|
|
|
import React, { useEffect, useState } from 'react'
|
|
|
|
export interface CookieFinding {
|
|
vendor: string
|
|
cookie: string
|
|
type: string
|
|
severity: string
|
|
declared: string
|
|
library_purpose: string
|
|
remediation: string
|
|
control?: { control_id?: string | null; regulation?: string; article?: string }
|
|
}
|
|
|
|
interface CheckData {
|
|
summary?: { checked?: number; in_library?: number; findings?: number }
|
|
findings?: CookieFinding[]
|
|
}
|
|
|
|
const SEV_COLOR: Record<string, string> = {
|
|
HIGH: 'bg-red-100 text-red-700',
|
|
MEDIUM: 'bg-amber-100 text-amber-700',
|
|
LOW: 'bg-blue-100 text-blue-700',
|
|
}
|
|
const TYPE_LABEL: Record<string, string> = {
|
|
tracker_as_necessary: 'Tracker als „notwendig" deklariert',
|
|
missing_purpose: 'Zweck fehlt',
|
|
excessive_lifetime: 'Speicherdauer zu lang',
|
|
vague_duration: 'Speicherdauer nicht konkret',
|
|
third_country: 'Drittland-Transfer',
|
|
eu_alternative: 'EU-Alternative verfügbar',
|
|
}
|
|
|
|
// Pure, testbar.
|
|
export function CookieFindingList({ data }: { data: CheckData }) {
|
|
const findings = data.findings || []
|
|
const s = data.summary || {}
|
|
return (
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<div className="px-4 py-2.5 bg-slate-50 border-b text-sm font-semibold text-gray-800">
|
|
Library-Abgleich — {findings.length} Befund{findings.length !== 1 ? 'e' : ''}
|
|
<span className="ml-2 text-xs font-normal text-gray-400">
|
|
{s.in_library ?? 0}/{s.checked ?? 0} Cookies in der Library erkannt
|
|
</span>
|
|
</div>
|
|
{findings.length === 0 ? (
|
|
<div className="px-4 py-3 text-sm text-green-700">
|
|
Keine Abweichungen gegen die Library.
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-gray-100 max-h-96 overflow-auto">
|
|
{findings.map((f, i) => (
|
|
<div key={i} className="px-4 py-2.5 space-y-1">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${SEV_COLOR[f.severity] || 'bg-gray-100 text-gray-600'}`}>
|
|
{f.severity}
|
|
</span>
|
|
<code className="text-xs text-gray-700">{f.cookie}</code>
|
|
<span className="text-xs text-gray-400">· {f.vendor}</span>
|
|
<span className="text-[10px] text-gray-500 ml-auto">
|
|
{TYPE_LABEL[f.type] || f.type}
|
|
</span>
|
|
</div>
|
|
{f.library_purpose && (
|
|
<div className="text-xs text-gray-500">Library-Zweck: {f.library_purpose}</div>
|
|
)}
|
|
<div className="text-xs text-gray-700">{f.remediation}</div>
|
|
{f.control?.regulation && f.control.regulation !== '—' && (
|
|
<div className="text-[10px] text-gray-400">
|
|
Rechtsgrundlage: {f.control.regulation} {f.control.article}
|
|
{f.control.control_id && ` · Control ${f.control.control_id}`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function CookieLibraryPanel({ snapshotId }: { snapshotId: string }) {
|
|
const [data, setData] = useState<CheckData | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/cookie-check`)
|
|
.then(r => r.json())
|
|
.then(d => { if (!cancelled) setData(d) })
|
|
.catch(() => { if (!cancelled) setData({ findings: [] }) })
|
|
.finally(() => { if (!cancelled) setLoading(false) })
|
|
return () => { cancelled = true }
|
|
}, [snapshotId])
|
|
|
|
if (loading) return <div className="text-xs text-gray-400">Library-Abgleich läuft…</div>
|
|
return <CookieFindingList data={data || {}} />
|
|
}
|