feat: Cookie banner vendors per category + {{COOKIE_TABLE}} generator
Build + Deploy / build-admin-compliance (push) Successful in 2m3s
Build + Deploy / build-backend-compliance (push) Failing after 3m19s
Build + Deploy / build-ai-sdk (push) Successful in 50s
Build + Deploy / build-developer-portal (push) Successful in 1m12s
Build + Deploy / build-tts (push) Successful in 1m44s
Build + Deploy / build-document-crawler (push) Successful in 37s
Build + Deploy / build-dsms-gateway (push) Successful in 22s
Build + Deploy / build-dsms-node (push) Successful in 10s
Build + Deploy / trigger-orca (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / test-go (push) Successful in 41s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Failing after 17s
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 2m44s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-python-backend (push) Successful in 40s
CI / test-python-document-crawler (push) Successful in 25s
Build + Deploy / build-admin-compliance (push) Successful in 2m3s
Build + Deploy / build-backend-compliance (push) Failing after 3m19s
Build + Deploy / build-ai-sdk (push) Successful in 50s
Build + Deploy / build-developer-portal (push) Successful in 1m12s
Build + Deploy / build-tts (push) Successful in 1m44s
Build + Deploy / build-document-crawler (push) Successful in 37s
Build + Deploy / build-dsms-gateway (push) Successful in 22s
Build + Deploy / build-dsms-node (push) Successful in 10s
Build + Deploy / trigger-orca (push) Has been skipped
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / test-go (push) Successful in 41s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Failing after 17s
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 2m44s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-python-backend (push) Successful in 40s
CI / test-python-document-crawler (push) Successful in 25s
- CookieBannerOverlay: shows vendors per category with expandable tables
(Verarbeiter, Cookies, Dauer, Land) for full transparency
- Demo vendors: 4 necessary, 3 statistics, 3 marketing, 3 functional
- cookie_table_generator.py: renders {{COOKIE_TABLE}} Markdown tables
from vendor configs (DB) or service registry (fallback)
- SERVICE_COOKIES: 16 known vendor-to-cookie mappings with provider + country
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,55 @@ interface ConsentState {
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
interface VendorInfo {
|
||||
name: string
|
||||
cookies: string
|
||||
provider: string
|
||||
retention: string
|
||||
country: string
|
||||
}
|
||||
|
||||
// Demo vendors per category — mirrors service registry + cookie_table_generator.py
|
||||
const CATEGORY_VENDORS: Record<string, { label: string; description: string; vendors: VendorInfo[] }> = {
|
||||
necessary: {
|
||||
label: 'Notwendig',
|
||||
description: 'Fuer die Grundfunktionen der Website erforderlich.',
|
||||
vendors: [
|
||||
{ name: 'Session', cookies: 'session_id', provider: 'Eigener Server', retention: 'Session', country: 'DE' },
|
||||
{ name: 'Consent-Cookie', cookies: 'bp_consent', provider: 'Eigener Server', retention: '12 Monate', country: 'DE' },
|
||||
{ name: 'Cloudflare', cookies: '__cf_bm', provider: 'Cloudflare Inc.', retention: '30 Min.', country: 'USA (DPF)' },
|
||||
{ name: 'Stripe', cookies: '__stripe_mid', provider: 'Stripe Inc.', retention: 'Session', country: 'USA (DPF)' },
|
||||
],
|
||||
},
|
||||
statistics: {
|
||||
label: 'Statistik',
|
||||
description: 'Helfen uns zu verstehen, wie Besucher mit der Website interagieren.',
|
||||
vendors: [
|
||||
{ name: 'Google Analytics', cookies: '_ga, _gid', provider: 'Google LLC', retention: '2 Jahre', country: 'USA (DPF)' },
|
||||
{ name: 'Hotjar', cookies: '_hj*', provider: 'Hotjar Ltd.', retention: '1 Jahr', country: 'EU (Malta)' },
|
||||
{ name: 'Google Tag Manager', cookies: '_gcl_au', provider: 'Google LLC', retention: '90 Tage', country: 'USA (DPF)' },
|
||||
],
|
||||
},
|
||||
marketing: {
|
||||
label: 'Marketing',
|
||||
description: 'Werden verwendet, um Besuchern relevante Werbung zu zeigen.',
|
||||
vendors: [
|
||||
{ name: 'Facebook Pixel', cookies: '_fbp, _fbc', provider: 'Meta Platforms', retention: '90 Tage', country: 'USA (DPF)' },
|
||||
{ name: 'Google Ads', cookies: '_gcl_aw, IDE', provider: 'Google LLC', retention: '90 Tage', country: 'USA (DPF)' },
|
||||
{ name: 'LinkedIn Insight', cookies: 'bcookie, li_sugr', provider: 'LinkedIn Ireland', retention: '6 Monate', country: 'EU (Irland)' },
|
||||
],
|
||||
},
|
||||
functional: {
|
||||
label: 'Funktional',
|
||||
description: 'Ermoeglichen erweiterte Funktionen und Personalisierung.',
|
||||
vendors: [
|
||||
{ name: 'Spracheinstellung', cookies: 'bp_lang', provider: 'Eigener Server', retention: '12 Monate', country: 'DE' },
|
||||
{ name: 'YouTube', cookies: 'YSC, VISITOR_INFO1_LIVE', provider: 'Google LLC', retention: '6 Monate', country: 'USA (DPF)' },
|
||||
{ name: 'HubSpot Chat', cookies: '__hstc, hubspotutk', provider: 'HubSpot Inc.', retention: '13 Monate', country: 'USA (DPF)' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
function getStoredConsent(): ConsentState | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
try {
|
||||
@@ -123,33 +172,19 @@ export function CookieBannerOverlay() {
|
||||
|
||||
{/* Category Settings (expandable) */}
|
||||
{showSettings && (
|
||||
<div className="px-6 pb-4 space-y-3 border-t border-gray-100 pt-4">
|
||||
{/* Necessary — always on */}
|
||||
<CategoryToggle
|
||||
label="Notwendig"
|
||||
description="Fuer die Grundfunktionen der Website erforderlich."
|
||||
checked={true}
|
||||
disabled={true}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
<CategoryToggle
|
||||
label="Statistik"
|
||||
description="Helfen uns zu verstehen, wie Besucher mit der Website interagieren."
|
||||
checked={consent.statistics}
|
||||
onChange={(v) => setConsent(prev => ({ ...prev, statistics: v }))}
|
||||
/>
|
||||
<CategoryToggle
|
||||
label="Marketing"
|
||||
description="Werden verwendet, um Besuchern relevante Werbung zu zeigen."
|
||||
checked={consent.marketing}
|
||||
onChange={(v) => setConsent(prev => ({ ...prev, marketing: v }))}
|
||||
/>
|
||||
<CategoryToggle
|
||||
label="Funktional"
|
||||
description="Ermoeglichen erweiterte Funktionen und Personalisierung."
|
||||
checked={consent.functional}
|
||||
onChange={(v) => setConsent(prev => ({ ...prev, functional: v }))}
|
||||
/>
|
||||
<div className="px-6 pb-4 space-y-1 border-t border-gray-100 pt-4 max-h-[50vh] overflow-y-auto">
|
||||
{Object.entries(CATEGORY_VENDORS).map(([key, cat]) => (
|
||||
<CategorySection
|
||||
key={key}
|
||||
categoryKey={key}
|
||||
label={cat.label}
|
||||
description={cat.description}
|
||||
vendors={cat.vendors}
|
||||
checked={key === 'necessary' ? true : consent[key as keyof ConsentState] as boolean}
|
||||
disabled={key === 'necessary'}
|
||||
onChange={(v) => key !== 'necessary' && setConsent(prev => ({ ...prev, [key]: v }))}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -238,40 +273,91 @@ export function CookieBannerFAB() {
|
||||
}
|
||||
|
||||
|
||||
function CategoryToggle({
|
||||
function CategorySection({
|
||||
categoryKey,
|
||||
label,
|
||||
description,
|
||||
vendors,
|
||||
checked,
|
||||
disabled,
|
||||
onChange,
|
||||
}: {
|
||||
categoryKey: string
|
||||
label: string
|
||||
description: string
|
||||
vendors: VendorInfo[]
|
||||
checked: boolean
|
||||
disabled?: boolean
|
||||
onChange: (v: boolean) => void
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 py-2">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-900">{label}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{description}</div>
|
||||
<div className="border border-gray-100 rounded-lg overflow-hidden">
|
||||
{/* Category Header with Toggle */}
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-3 bg-gray-50/50">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-2 flex-1 text-left"
|
||||
>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${expanded ? 'rotate-90' : ''}`}
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{label}
|
||||
<span className="ml-2 text-xs font-normal text-gray-400">
|
||||
({vendors.length} Verarbeiter)
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">{description}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
disabled={disabled}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors shrink-0 ${
|
||||
checked
|
||||
? disabled ? 'bg-gray-400' : 'bg-purple-600'
|
||||
: 'bg-gray-200'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
|
||||
checked ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => !disabled && onChange(!checked)}
|
||||
disabled={disabled}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors shrink-0 ${
|
||||
checked
|
||||
? disabled ? 'bg-gray-400' : 'bg-purple-600'
|
||||
: 'bg-gray-200'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
|
||||
checked ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Vendor Table (expandable) */}
|
||||
{expanded && (
|
||||
<div className="px-4 pb-3">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-400 border-b border-gray-100">
|
||||
<th className="text-left py-1.5 font-medium">Verarbeiter</th>
|
||||
<th className="text-left py-1.5 font-medium">Cookies</th>
|
||||
<th className="text-left py-1.5 font-medium">Dauer</th>
|
||||
<th className="text-left py-1.5 font-medium">Land</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vendors.map((v, i) => (
|
||||
<tr key={i} className="border-b border-gray-50 last:border-0">
|
||||
<td className="py-1.5 text-gray-700 font-medium">{v.name}</td>
|
||||
<td className="py-1.5 text-gray-500 font-mono">{v.cookies}</td>
|
||||
<td className="py-1.5 text-gray-500">{v.retention}</td>
|
||||
<td className="py-1.5 text-gray-500">{v.country}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user