Files
breakpilot-compliance/admin-compliance/app/sdk/agent/audit/[checkId]/BannerTab.tsx
T
Benjamin Admin 6f16507c5f
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m54s
CI / test-go (push) Has been skipped
CI / detect-changes (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 17s
CI / loc-budget (push) Successful in 17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
feat(banner): P19 + P20 — Per-Category-Click-Test + Frontend-Drilldown
P19 (consent-tester):
- dp-cookieconsent (TYPO3, Safetykon-Pattern) als CMP-Profil hinzu —
  Selektoren #dp--cookie-statistics/marketing + a.cc-allow Save-Button
- Neues Signal provider_details_visible: nach Kategorie-Toggle prueft
  Playwright ob im Banner sichtbare Provider-/Cookie-Detail-Elemente
  erscheinen. Bei dp-cookieconsent (Banner ohne Listing) immer False
  -> HIGH-Violation "Kategorie zeigt keine Provider-/Cookie-Details —
  Nutzer kann nicht informiert einwilligen (Art. 7 Abs. 1 DSGVO)"
- main.py serialisiert provider_details_visible + cookies_set pro Kategorie

P20 (Frontend-Drilldown):
- Backend: check_payloads-Tabelle um Spalte 'banner' (JSON) — voller
  banner_result persistiert (vorher nur in-memory). ALTER TABLE
  Migration idempotent.
- Neuer Endpoint GET /api/compliance/agent/banner/<check_id> — liefert
  Quality-Score, Phases, Category-Tests, Banner-Checks, alle 46
  structured_checks.
- Frontend: BannerTab im /sdk/agent/audit/<id> mit Quality-Cards,
  3-Phasen-Cookie-Tabelle, Per-Category-Listing (mit P19-Signal
  rot/gruen), Banner-Verstoesse + Rechtsgrundlagen, 46-Check-Drilldown
  filterbar nach Severity.
- Tab-Switcher in page.tsx um "Cookie-Banner-Analyse" erweitert.
- Bonus: 2 alte route.ts auf Next.js 15 Promise-params umgestellt
  (Build-Fix).

Plus: Critical-Findings-Block nutzt provider_details_visible als
primaeres Signal statt nur tracking_services-Anzahl.

Smoke-Test Safetykon: 4 Critical Findings im Mail, banner-Endpoint
liefert 46 checks + 3 phases + 2 categories mit provider_details_visible=False.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:31:13 +02:00

303 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useEffect, useState } from 'react'
type Phase = {
cookies?: string[]
scripts?: string[]
tracking_services?: (string | { name?: string })[]
new_tracking?: unknown[]
violations?: Array<{ severity?: string; text?: string }>
undocumented?: unknown[]
}
type CategoryTest = {
category: string
category_label: string
tracking_services?: (string | { name?: string })[]
cookies_set?: string[]
provider_details_visible?: boolean
violations?: Array<{ severity?: string; text?: string; legal_ref?: string }>
}
type BannerViolation = {
severity?: string
text?: string
legal_ref?: string
}
type StructuredCheck = {
id: string
label: string
passed: boolean
skipped?: boolean
severity: string
level?: number
hint?: string
}
type BannerResp = {
found: boolean
check_id: string
banner?: {
banner_provider?: string
banner_detected?: boolean
completeness_pct?: number
correctness_pct?: number
phases?: Record<string, Phase>
banner_checks?: { violations?: BannerViolation[] }
category_tests?: CategoryTest[]
structured_checks?: StructuredCheck[]
summary?: Record<string, number>
}
}
const PHASE_LABEL: Record<string, string> = {
before_consent: 'Vor Consent',
after_reject: 'Nach Ablehnung',
after_accept: 'Nach Annahme',
}
const SEV_BADGE: Record<string, string> = {
CRITICAL: 'bg-red-600 text-white',
HIGH: 'bg-red-100 text-red-800',
MEDIUM: 'bg-amber-100 text-amber-800',
LOW: 'bg-blue-100 text-blue-800',
INFO: 'bg-gray-100 text-gray-600',
}
function pctColor(pct?: number): string {
if (pct === undefined || pct === null) return 'text-gray-400'
return pct >= 80 ? 'text-green-700' : pct >= 50 ? 'text-amber-700' : 'text-red-700'
}
export default function BannerTab({ checkId }: { checkId: string }) {
const [data, setData] = useState<BannerResp | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [checkFilter, setCheckFilter] = useState<'all' | 'fail' | 'critical'>('fail')
useEffect(() => {
let cancelled = false
setLoading(true)
fetch(`/api/sdk/v1/agent/banner/${checkId}`)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d) })
.catch(e => { if (!cancelled) setError(String(e)) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [checkId])
if (loading) return <div className="p-6 text-sm text-gray-500">Lade Banner-Daten</div>
if (error) return <div className="p-6 text-sm text-red-600">Fehler: {error}</div>
if (!data?.found || !data.banner) {
return <div className="p-6 text-sm text-gray-500">Keine Banner-Daten zu diesem Check.</div>
}
const b = data.banner
const phases = b.phases || {}
const cats = b.category_tests || []
const violations = b.banner_checks?.violations || []
const checks = b.structured_checks || []
const summary = b.summary || {}
const filteredChecks = checks.filter(c => {
if (checkFilter === 'all') return true
if (checkFilter === 'fail') return !c.passed && !c.skipped
return !c.passed && !c.skipped && ['CRITICAL', 'HIGH'].includes(c.severity)
})
return (
<div className="space-y-6">
{/* Quality Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
<div className="border rounded p-3">
<div className="text-[10px] uppercase text-gray-500">Vollstaendigkeit</div>
<div className={`text-2xl font-semibold ${pctColor(b.completeness_pct)}`}>
{b.completeness_pct ?? ''}{b.completeness_pct !== undefined && '%'}
</div>
</div>
<div className="border rounded p-3">
<div className="text-[10px] uppercase text-gray-500">Korrektheit</div>
<div className={`text-2xl font-semibold ${pctColor(b.correctness_pct)}`}>
{b.correctness_pct ?? ''}{b.correctness_pct !== undefined && '%'}
</div>
</div>
<div className="border rounded p-3">
<div className="text-[10px] uppercase text-gray-500">Verstoesse</div>
<div className="text-2xl font-semibold text-red-700">
{summary.total_violations ?? violations.length}
</div>
<div className="text-[10px] text-gray-500 mt-1">
crit:{summary.critical ?? 0} · high:{summary.high ?? 0}
</div>
</div>
<div className="border rounded p-3">
<div className="text-[10px] uppercase text-gray-500">CMP</div>
<div className="text-sm font-medium text-gray-800 truncate">
{b.banner_provider || 'unbekannt'}
</div>
<div className="text-[10px] text-gray-500 mt-1">
{b.banner_detected ? 'Banner erkannt' : 'kein Banner'}
</div>
</div>
</div>
{/* Phases */}
<div className="border rounded-lg overflow-hidden">
<div className="px-4 py-2 bg-gray-50 border-b text-sm font-medium text-gray-700">
Cookie-Setzungen pro Phase (echter Browser-Test)
</div>
<table className="w-full text-xs">
<thead className="bg-gray-50 text-gray-600">
<tr>
<th className="px-3 py-2 text-left">Phase</th>
<th className="px-3 py-2 text-center">Cookies</th>
<th className="px-3 py-2 text-center">Tracker</th>
<th className="px-3 py-2 text-left">Auffaelligkeiten</th>
</tr>
</thead>
<tbody>
{(['before_consent', 'after_reject', 'after_accept'] as const).map(key => {
const p = phases[key] || {}
const nc = (p.cookies || []).length
const nt = (p.tracking_services || []).length
const issues: string[] = []
if (p.violations?.length) issues.push(`${p.violations.length} Verstoss`)
if (p.new_tracking?.length) issues.push(`${p.new_tracking.length} neue Tracker`)
if (p.undocumented?.length) issues.push(`${p.undocumented.length} undokumentiert`)
const color = key === 'before_consent'
? (nc === 0 ? 'text-green-600' : 'text-red-600')
: key === 'after_reject'
? (nc <= 1 ? 'text-green-600' : 'text-amber-600')
: 'text-gray-700'
return (
<tr key={key} className="border-t">
<td className="px-3 py-2 font-medium">{PHASE_LABEL[key]}</td>
<td className={`px-3 py-2 text-center font-semibold ${color}`}>{nc}</td>
<td className="px-3 py-2 text-center">{nt}</td>
<td className="px-3 py-2 text-gray-500">{issues.join(', ') || '—'}</td>
</tr>
)
})}
</tbody>
</table>
</div>
{/* Per-Category */}
{cats.length > 0 && (
<div className="border rounded-lg overflow-hidden">
<div className="px-4 py-2 bg-gray-50 border-b text-sm font-medium text-gray-700">
Provider-Listing pro Kategorie (P19 Click-Through-Test)
</div>
<table className="w-full text-xs">
<thead className="bg-gray-50 text-gray-600">
<tr>
<th className="px-3 py-2 text-left">Kategorie</th>
<th className="px-3 py-2 text-center">Anbieter sichtbar</th>
<th className="px-3 py-2 text-center">Tracker erkannt</th>
<th className="px-3 py-2 text-left">Violations</th>
</tr>
</thead>
<tbody>
{cats.map(c => {
const pdv = c.provider_details_visible
const pdv_label = pdv === true ? 'Ja' : pdv === false ? 'Nein' : ''
const pdv_color = pdv === false ? 'text-red-700' : pdv === true ? 'text-green-700' : 'text-gray-400'
return (
<tr key={c.category} className="border-t">
<td className="px-3 py-2">{c.category_label}</td>
<td className={`px-3 py-2 text-center font-semibold ${pdv_color}`}>{pdv_label}</td>
<td className="px-3 py-2 text-center">{(c.tracking_services || []).length}</td>
<td className="px-3 py-2 text-red-700 text-[10px]">
{(c.violations || []).map(v => v.text?.slice(0, 80)).join('; ') || '—'}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
{/* Banner-Checks Violations */}
{violations.length > 0 && (
<div className="border rounded-lg overflow-hidden">
<div className="px-4 py-2 bg-gray-50 border-b text-sm font-medium text-gray-700">
Banner-Verstoesse ({violations.length})
</div>
<ul className="text-xs divide-y">
{violations.map((v, i) => {
const sev = (v.severity || 'MEDIUM').toUpperCase()
return (
<li key={i} className="px-3 py-2">
<div className="flex items-start gap-2">
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${SEV_BADGE[sev] || 'bg-gray-100'}`}>{sev}</span>
<div>
<div className="text-gray-900">{v.text}</div>
{v.legal_ref && <div className="text-[10px] text-gray-400 italic mt-1">Quelle: {v.legal_ref}</div>}
</div>
</div>
</li>
)
})}
</ul>
</div>
)}
{/* 46 structured_checks Drilldown */}
<div className="border rounded-lg overflow-hidden">
<div className="px-4 py-2 bg-gray-50 border-b text-sm font-medium text-gray-700 flex items-center gap-3">
<span>Banner-Checks ({checks.length})</span>
<div className="ml-auto flex gap-1">
{(['all', 'fail', 'critical'] as const).map(f => (
<button key={f}
onClick={() => setCheckFilter(f)}
className={`px-2 py-1 rounded text-[10px] border ${
checkFilter === f ? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-600 border-gray-200'
}`}>
{f === 'all' ? 'Alle' : f === 'fail' ? 'Nur Fail' : 'Nur CRIT/HIGH'}
</button>
))}
</div>
</div>
<table className="w-full text-xs">
<thead className="bg-gray-50 text-gray-600">
<tr>
<th className="px-3 py-2 text-left">Status</th>
<th className="px-3 py-2 text-left">Sev</th>
<th className="px-3 py-2 text-left">Check</th>
</tr>
</thead>
<tbody>
{filteredChecks.map(c => (
<tr key={c.id} className="border-t">
<td className="px-3 py-2">
{c.passed ? <span className="text-green-600"></span>
: c.skipped ? <span className="text-gray-400"></span>
: <span className="text-red-600"></span>}
</td>
<td className="px-3 py-2">
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${SEV_BADGE[c.severity] || 'bg-gray-100'}`}>
{c.severity}
</span>
</td>
<td className="px-3 py-2">
<div className="text-gray-900">{c.label}</div>
{c.hint && !c.passed && (
<div className="text-[10px] text-gray-500 mt-1">{c.hint.slice(0, 200)}</div>
)}
</td>
</tr>
))}
{filteredChecks.length === 0 && (
<tr><td colSpan={3} className="px-3 py-4 text-center text-gray-400">Keine Checks fuer den Filter.</td></tr>
)}
</tbody>
</table>
</div>
</div>
)
}