4bfb438c92
Build + Deploy / build-admin-compliance (push) Successful in 2m17s
Build + Deploy / build-backend-compliance (push) Successful in 3m17s
Build + Deploy / build-ai-sdk (push) Successful in 56s
Build + Deploy / build-developer-portal (push) Successful in 1m37s
Build + Deploy / build-tts (push) Successful in 1m33s
Build + Deploy / build-document-crawler (push) Successful in 42s
Build + Deploy / build-dsms-gateway (push) Successful in 33s
Build + Deploy / build-dsms-node (push) Successful in 16s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 25s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 3m33s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 1m18s
CI / test-python-backend (push) Successful in 53s
CI / test-python-document-crawler (push) Successful in 36s
CI / test-python-dsms-gateway (push) Successful in 33s
CI / validate-canonical-controls (push) Successful in 24s
Build + Deploy / trigger-orca (push) Successful in 3m19s
1. 30 CMP selectors (was 10): Added Sourcepoint, Iubenda, Complianz, CookieFirst, HubSpot, Osano, Piwik PRO, Cookie Consent (Insites), Axeptio, Termly, CookieScript, Civic UK, GDPR Cookie Compliance, CookieHub, Ketch, Admiral, Sibbo, Evidon, LiveRamp, Adsimple. Plus improved generic fallback: role=dialog, aria-label, data-* attrs. 2. Playwright stealth mode: playwright-stealth against bot detection. Removes WebDriver flag, simulates plugins, realistic viewport/locale. Launch args: --disable-blink-features=AutomationControlled. 3. Shadow DOM: Recursive JS-based search through shadowRoot elements for consent banners. Fallback click via page.evaluate() when normal Playwright selectors can't penetrate Shadow DOM. 4. Category selection UI: User can choose which cookie categories to test (Notwendig, Statistik, Marketing, Funktional, Praeferenzen). Pill-style checkboxes in BannerCheckTab, forwarded through API chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { ChecklistView } from './ChecklistView'
|
|
|
|
interface CheckItem {
|
|
id: string
|
|
label: string
|
|
passed: boolean
|
|
severity: string
|
|
matched_text: string
|
|
level?: number
|
|
parent?: string | null
|
|
skipped?: boolean
|
|
hint?: string
|
|
}
|
|
|
|
interface BannerResult {
|
|
banner_detected: boolean
|
|
banner_provider: string
|
|
banner_checks?: {
|
|
violations: { code: string; text: string; severity: string }[]
|
|
has_impressum_link?: boolean
|
|
has_dse_link?: boolean
|
|
}
|
|
structured_checks?: CheckItem[]
|
|
completeness_pct?: number
|
|
correctness_pct?: number
|
|
phases?: {
|
|
before_consent: { cookies: string[]; scripts: string[]; tracking_services: string[]; violations: any[] }
|
|
after_reject: { cookies: string[]; scripts: string[]; new_tracking: string[]; violations: any[] }
|
|
after_accept: { cookies: string[]; scripts: string[]; new_tracking: string[]; undocumented: string[] }
|
|
}
|
|
}
|
|
|
|
const CATEGORIES = [
|
|
{ id: 'all', label: 'Alle Kategorien' },
|
|
{ id: 'necessary', label: 'Notwendig' },
|
|
{ id: 'statistics', label: 'Statistik' },
|
|
{ id: 'marketing', label: 'Marketing' },
|
|
{ id: 'functional', label: 'Funktional' },
|
|
{ id: 'preferences', label: 'Praeferenzen' },
|
|
]
|
|
|
|
export function BannerCheckTab() {
|
|
const [url, setUrl] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [progress, setProgress] = useState('')
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [result, setResult] = useState<BannerResult | null>(null)
|
|
const [categories, setCategories] = useState<string[]>(['all'])
|
|
|
|
const toggleCategory = (id: string) => {
|
|
if (id === 'all') {
|
|
setCategories(['all'])
|
|
return
|
|
}
|
|
setCategories(prev => {
|
|
const without = prev.filter(c => c !== 'all' && c !== id)
|
|
const next = prev.includes(id) ? without : [...without, id]
|
|
return next.length === 0 ? ['all'] : next
|
|
})
|
|
}
|
|
|
|
const handleScan = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!url.trim()) return
|
|
|
|
setLoading(true)
|
|
setError(null)
|
|
setResult(null)
|
|
setProgress('Cookie-Banner wird analysiert...')
|
|
|
|
// 'all' selected = empty array (test everything)
|
|
const selectedCategories = categories.includes('all')
|
|
? []
|
|
: categories
|
|
|
|
try {
|
|
const res = await fetch('/api/sdk/v1/agent/banner-check', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url: url.trim(), categories: selectedCategories }),
|
|
})
|
|
if (!res.ok) throw new Error(`Fehler: ${res.status}`)
|
|
const data = await res.json()
|
|
setResult(data)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setLoading(false)
|
|
setProgress('')
|
|
}
|
|
}
|
|
|
|
const structuredChecks = result?.structured_checks || []
|
|
const hasStructured = structuredChecks.length > 0
|
|
const compPct = result?.completeness_pct ?? 0
|
|
const corrPct = result?.correctness_pct ?? 0
|
|
|
|
// Build ChecklistView-compatible result for structured checks
|
|
const checklistResults = hasStructured ? [{
|
|
label: `Cookie-Banner: ${result?.banner_provider || 'Unbekannt'}`,
|
|
url: url,
|
|
doc_type: 'banner',
|
|
word_count: 0,
|
|
completeness_pct: compPct,
|
|
correctness_pct: corrPct,
|
|
checks: structuredChecks,
|
|
findings_count: structuredChecks.filter(c => !c.passed && !c.skipped).length,
|
|
error: '',
|
|
}] : []
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<h3 className="text-sm font-semibold text-blue-900">Cookie-Banner Compliance Check</h3>
|
|
<p className="text-xs text-blue-700 mt-1">
|
|
Playwright-basierter 3-Phasen-Test: Vor Interaktion, nach Ablehnen, nach Akzeptieren.
|
|
Prueft Dark Patterns, Pre-Consent-Cookies, Farbkontrast, Klick-Paritaet und 36 weitere Kriterien.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleScan} className="space-y-3">
|
|
<div className="flex gap-3">
|
|
<input
|
|
type="url" value={url} onChange={e => setUrl(e.target.value)}
|
|
placeholder="https://www.example.com/"
|
|
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm"
|
|
disabled={loading} required
|
|
/>
|
|
<button type="submit" disabled={loading || !url.trim()}
|
|
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors flex items-center gap-2 text-sm font-medium whitespace-nowrap">
|
|
{loading ? (
|
|
<><svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>Pruefe...</>
|
|
) : 'Banner pruefen'}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{CATEGORIES.map(cat => (
|
|
<label key={cat.id}
|
|
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium cursor-pointer border transition-colors ${
|
|
categories.includes(cat.id)
|
|
? 'bg-purple-100 border-purple-300 text-purple-800'
|
|
: 'bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={categories.includes(cat.id)}
|
|
onChange={() => toggleCategory(cat.id)}
|
|
className="sr-only"
|
|
/>
|
|
<span className={`w-3 h-3 rounded-sm border flex items-center justify-center ${
|
|
categories.includes(cat.id)
|
|
? 'bg-purple-600 border-purple-600'
|
|
: 'border-gray-400'
|
|
}`}>
|
|
{categories.includes(cat.id) && (
|
|
<svg className="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 12 12">
|
|
<path d="M10 3L4.5 8.5 2 6" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
)}
|
|
</span>
|
|
{cat.label}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</form>
|
|
|
|
{progress && (
|
|
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 text-sm text-purple-700 flex items-center gap-3">
|
|
<svg className="animate-spin w-5 h-5 text-purple-500 shrink-0" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
{progress}
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">{error}</div>
|
|
)}
|
|
|
|
{result && (
|
|
<div className="space-y-4">
|
|
{/* 3-Phase Summary Card */}
|
|
{result.phases && (
|
|
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
|
|
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">
|
|
{result.banner_detected ? '\u{1F6E1}\u{FE0F}' : '\u26A0\u{FE0F}'}
|
|
</span>
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-900">
|
|
{result.banner_detected
|
|
? `Banner erkannt: ${result.banner_provider || 'Unbekannter Anbieter'}`
|
|
: 'Kein Cookie-Banner erkannt'}
|
|
</h3>
|
|
<p className="text-xs text-gray-500 mt-0.5">
|
|
3-Phasen-Analyse: Cookies und Scripts vor/nach Interaktion
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="px-6 py-3 grid grid-cols-3 gap-4">
|
|
<PhaseBox
|
|
label="Vor Consent"
|
|
icon="\uD83D\uDD12"
|
|
cookies={result.phases.before_consent.cookies?.length ?? 0}
|
|
scripts={result.phases.before_consent.scripts?.length ?? 0}
|
|
violations={result.phases.before_consent.violations?.length ?? 0}
|
|
/>
|
|
<PhaseBox
|
|
label="Nach Ablehnen"
|
|
icon="\uD83D\uDEAB"
|
|
cookies={result.phases.after_reject.cookies?.length ?? 0}
|
|
scripts={result.phases.after_reject.scripts?.length ?? 0}
|
|
violations={result.phases.after_reject.violations?.length ?? 0}
|
|
/>
|
|
<PhaseBox
|
|
label="Nach Akzeptieren"
|
|
icon="\u2705"
|
|
cookies={result.phases.after_accept.cookies?.length ?? 0}
|
|
scripts={result.phases.after_accept.scripts?.length ?? 0}
|
|
violations={0}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Structured L1/L2 Checklist */}
|
|
{hasStructured && (
|
|
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
|
<ChecklistView results={checklistResults} />
|
|
</div>
|
|
)}
|
|
|
|
{!result.banner_detected && !hasStructured && (
|
|
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
|
<p className="text-sm text-gray-500">
|
|
Kein Cookie-Banner auf dieser Seite gefunden. Falls Cookies gesetzt werden, ist ein Banner nach ss25 TDDDG Pflicht.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function PhaseBox({ label, icon, cookies, scripts, violations }: {
|
|
label: string; icon: string; cookies: number; scripts: number; violations: number
|
|
}) {
|
|
return (
|
|
<div className="text-center">
|
|
<div className="text-lg">{icon}</div>
|
|
<div className="text-xs font-medium text-gray-700">{label}</div>
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
{cookies} Cookies, {scripts} Scripts
|
|
</div>
|
|
{violations > 0 && (
|
|
<div className="text-xs text-red-600 font-medium">
|
|
{violations} Verstoesse
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|