662327e8b4
CI / nodejs-build (push) Successful in 2m47s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / detect-changes (push) Successful in 10s
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 16s
CI / loc-budget (push) Failing after 17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
Massiv-Update auf Basis BMW-Test-Iterationen (v1→v9): Core Compliance-Check - Sonnet check_type Klassifikation: text/process/review fuer alle 1874 MCs in compliance.doc_check_controls (script + Sidecar /data/mc_classification.db). rag_document_checker filtert auf check_type='text' fuer doc_check. Plus fits_doc_type-Audit (v2) + ui_only-Audit fuer DSA/E-Commerce-MCs in falscher doc_type-Schublade. - scope_requires-Filter: biometric/ai_decision/child_targeting MCs werden per business_profile gefiltert (FRT skipped fuer BMW etc.). - Embedding-Match (BGE-M3) als Phase-3 nach Regex-Match: Per-doc_type-Threshold-Override (impressum 0.50, dse/cookie 0.60), Short-Field-Rescue (15-Wort-Chunks) fuer Pflichtfelder im Impressum. Title+check_question als Embedding-Input fuer mehr Kontext. - Cookie-Text-Routing: consent-tester gibt cmp_cookie_text aus dem CMP-Reconstruct zurueck, Backend bevorzugt das gegen DOM-Extraction wenn richer (BMW 1824 vs 600 Worte). Vendor-Redundanz + EU-Alternativen + Cost-Saving - vendor_redundancy.analyze() — funktionale Kategorisierung der CMP-Vendors, Detektion von Mehrfach-Anbietern pro Kategorie, EU-Alternative-Lookup (Matomo, IONOS, HERE, Friendly Captcha, Smart AdServer, ...). - vendor_cost_estimator: Tier-Inferenz aus Cookie-Footprint (Cookie-Anzahl + Premium-Feature-Cookies + Third-Party-Quote → starter/professional/ enterprise/premier). - Self-Service-Werbung (Google/Meta/Pinterest/...) = 0 Lizenz-Kosten (nur Media-Spend, separat). DSP-Plattformen behalten enge Range. - Tier-aware Saving-Range: bei Enterprise/Premier nutzen wir den oberen 40-100%-Band der Listpreise, nicht starter→premier. - Multi-Function-Tools (Matomo Pro, SAP CX, IONOS Cloud, Userlike, Smart AdServer, HERE Maps, Vimeo Pro, LamaPoll) — ein Tool ersetzt mehrere Kategorien gleichzeitig. Cookie-Wissens-DB + Funktionale Klassifikation - cookie_knowledge_db: 50 kuratierte Top-Cookies (Google/Meta/Adobe/MS/...) mit vendor, exact_purpose, data_collected, IAB-TCF-IDs, reid_risk, schrems_ii_status, EuGH-Urteile, EU-Alternative. - cookie_function_classifier: pro Cookie funktionale Rolle (tracking_id, ad_pixel, session_id, ab_test, csrf, ...) + blocking_impact. Country-Inferenz aus Rechtsform - cookie_link_validator: Country-Field wird aus Vendor-Name abgeleitet (A/S=DK, GmbH=DE, Inc=US, B.V.=NL, ...) plus Vendor-Lookup-Table. Reduziert false-positive no_country-Flags bei eindeutig-EU-Vendors (Adform DK, Pinterest IE). Action-Recipes + Doc-Anchor-Locator - finding_action_recipes: pro Finding-Typ (no_cookies_listed, no_country, broken_opt_out, "Auftragsverarbeiter erwaehnen", "Art. 22 Profiling", ...) eine strukturierte Anweisung mit what/why/fix_text/where/example. Zum 1:1-Einfuegen in Kunden-Dokumente. - doc_anchor_locator: Embedding-basiert (BGE-M3 cosine) — sucht den passenden Absatz im existierenden Kundendokument fuer jeden Finding. Per-Run Thread-Local-Cache. Fallback: keyword-Match. - Email-Rendering integriert Recipe + Anchor pro Doc-Pruefungs-Fail + Vendor-Flag-Liste mit aufklappbarer Action-Liste. - Score-Erklaerung pro Vendor-Zeile (3/5-Untertitel + Tooltip). Migration-Pipeline (Compliance-Check -> Customer Banner/Documents) - migration_to_banner.py: Vendor-Liste -> CookieBannerConfig mit 4 Kategorien + Review-Flags. - migration_to_document.py: Vendor-Liste -> Cookie-Policy + VVT-Register + Privacy-Policy-Pre-Fills. - agent_migration_routes: 3 Preview-Endpoints (banner-preview, document-preview, summary). Persistierung der cmp_vendors in /data/compliance_audits.db check_payloads-Tabelle. Borlabs-Parity Cookie-Banner-Features - Consent-Historie im Banner: window.bpShowConsentHistory() + localStorage. - Content-Blocker: cookie-banner-content-blocker.ts — YouTube/Maps/Video Placeholder bis Einwilligung. - Google Consent Mode v2 erweitert: wait_for_update + region=EEA/CH/GB. - Consent-Log Export (CSV/JSON) per einwilligungen_export_routes. Bug-Fixes - canonical_control_routes: _jsonish-Helper fuer string-typed jsonb, similar-controls-Endpoint mit _has_embedding_col()-Cache (kein 500 mehr). - Control-Library Frontend: defensive .map-Coercer in 2 Detail-Views. - Embedding-Service-Batching (32er Batches statt 165 in einem Call). - KeyError 'control_id' in MC-Result-Aggregation (defensive .get). - Master-Controls-Klick-Through von /sdk/master-controls auf /sdk/control-library?control=<id> mit URL-Param-Auto-Open. - Dockerfile: /data pre-chowned auf appuser (Audit-DB-Schreibrecht). - Cookie-Text-Routing-Bug (cmp_reconstructed > DOM-extraction). - doc_type-aware MC-Filter (statt all-text-MCs). - Master-Contract-Dedup (60 BMW-Internal-Eintraege = 1 Adobe-Vertrag). - A3-v2-Audit hat 24 UI-Sprache-MCs als 'process' reklassifiziert. Tests - test_migration_mappers.py (9 Tests) - test_migration_endpoints.py (4 Tests) Skripte (one-shot) - classify_mc_check_type.py (v1) + _v2 (PK=control_id,doc_type) - audit_mc_doctype_fit.py (v1 fits) + _v2 (ui_only + scope_requires) BMW-Run-Bilanz v1 (broken) -> v9 (alle Fixes): DSE 7,5% -> 81-83% Impressum 4% -> 100% (6 echte MCs alle erfuellt) Cookie 0% -> 79-83% (CMP-Text-Routing + Embedding) Plus: 10 Konsolidierungs-Kategorien, geschaetzte Saving 200k-3M / Jahr Plus: Action-Recipes + Doc-Anchors fuer jeden Fail Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
559 lines
17 KiB
TypeScript
559 lines
17 KiB
TypeScript
/**
|
||
* Cookie Banner — Embed Code Generation (CSS, HTML, JS)
|
||
*
|
||
* Generates the embeddable cookie banner code from configuration.
|
||
*/
|
||
|
||
import {
|
||
CookieBannerConfig,
|
||
CookieBannerStyling,
|
||
CookieBannerEmbedCode,
|
||
LocalizedText,
|
||
SupportedLanguage,
|
||
} from '../types'
|
||
|
||
// =============================================================================
|
||
// MAIN EXPORT
|
||
// =============================================================================
|
||
|
||
export function generateEmbedCode(
|
||
config: CookieBannerConfig,
|
||
privacyPolicyUrl: string = '/datenschutz'
|
||
): CookieBannerEmbedCode {
|
||
const css = generateCSS(config.styling)
|
||
const html = generateHTML(config, privacyPolicyUrl)
|
||
const js = generateJS(config)
|
||
|
||
const scriptTag = `<script src="/cookie-banner.js" data-tenant="${config.tenantId}"></script>`
|
||
|
||
return { html, css, js, scriptTag }
|
||
}
|
||
|
||
// =============================================================================
|
||
// CSS GENERATION
|
||
// =============================================================================
|
||
|
||
function generateCSS(styling: CookieBannerStyling): string {
|
||
const positionStyles: Record<string, string> = {
|
||
BOTTOM: 'bottom: 0; left: 0; right: 0;',
|
||
TOP: 'top: 0; left: 0; right: 0;',
|
||
CENTER: 'top: 50%; left: 50%; transform: translate(-50%, -50%);',
|
||
}
|
||
|
||
const isDark = styling.theme === 'DARK'
|
||
const bgColor = isDark ? '#1e293b' : styling.backgroundColor || '#ffffff'
|
||
const textColor = isDark ? '#f1f5f9' : styling.textColor || '#1e293b'
|
||
const borderColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'
|
||
|
||
return `
|
||
/* Cookie Banner Styles */
|
||
.cookie-banner-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
z-index: 9998;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.cookie-banner-overlay.active {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
.cookie-banner {
|
||
position: fixed;
|
||
${positionStyles[styling.position]}
|
||
z-index: 9999;
|
||
background: ${bgColor};
|
||
color: ${textColor};
|
||
border-radius: ${styling.borderRadius || 12}px;
|
||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
|
||
padding: 24px;
|
||
max-width: ${styling.maxWidth}px;
|
||
margin: ${styling.position === 'CENTER' ? '0' : '16px'};
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
transform: translateY(100%);
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.cookie-banner.active {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
.cookie-banner-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.cookie-banner-description {
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
margin-bottom: 16px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.cookie-banner-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.cookie-banner-btn {
|
||
flex: 1;
|
||
min-width: 120px;
|
||
padding: 12px 20px;
|
||
border-radius: ${(styling.borderRadius || 12) / 2}px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border: none;
|
||
}
|
||
|
||
.cookie-banner-btn-primary {
|
||
background: ${styling.primaryColor};
|
||
color: white;
|
||
}
|
||
|
||
.cookie-banner-btn-primary:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
.cookie-banner-btn-secondary {
|
||
background: ${styling.secondaryColor || borderColor};
|
||
color: ${textColor};
|
||
}
|
||
|
||
.cookie-banner-btn-secondary:hover {
|
||
filter: brightness(0.95);
|
||
}
|
||
|
||
.cookie-banner-link {
|
||
display: block;
|
||
margin-top: 16px;
|
||
font-size: 12px;
|
||
color: ${styling.primaryColor};
|
||
text-decoration: none;
|
||
}
|
||
|
||
.cookie-banner-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Category Details */
|
||
.cookie-banner-details {
|
||
margin-top: 16px;
|
||
border-top: 1px solid ${borderColor};
|
||
padding-top: 16px;
|
||
display: none;
|
||
}
|
||
|
||
.cookie-banner-details.active {
|
||
display: block;
|
||
}
|
||
|
||
.cookie-banner-category {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 0;
|
||
border-bottom: 1px solid ${borderColor};
|
||
}
|
||
|
||
.cookie-banner-category:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.cookie-banner-category-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.cookie-banner-category-name {
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.cookie-banner-category-desc {
|
||
font-size: 12px;
|
||
opacity: 0.7;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.cookie-banner-toggle {
|
||
position: relative;
|
||
width: 48px;
|
||
height: 28px;
|
||
background: ${borderColor};
|
||
border-radius: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.cookie-banner-toggle.active {
|
||
background: ${styling.primaryColor};
|
||
}
|
||
|
||
.cookie-banner-toggle.disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.cookie-banner-toggle::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 4px;
|
||
left: 4px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: white;
|
||
border-radius: 50%;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.cookie-banner-toggle.active::after {
|
||
left: 24px;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.cookie-banner {
|
||
margin: 0;
|
||
border-radius: ${styling.position === 'CENTER' ? (styling.borderRadius || 12) : 0}px;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.cookie-banner-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.cookie-banner-btn {
|
||
width: 100%;
|
||
}
|
||
}
|
||
`.trim()
|
||
}
|
||
|
||
// =============================================================================
|
||
// HTML GENERATION
|
||
// =============================================================================
|
||
|
||
function generateHTML(config: CookieBannerConfig, privacyPolicyUrl: string): string {
|
||
const categoriesHTML = config.categories
|
||
.map((cat) => {
|
||
const isRequired = cat.isRequired
|
||
// COMPLIANCE: Only "required" categories may be pre-enabled (EuGH Planet49)
|
||
// Non-required categories must NEVER be defaultEnabled
|
||
const isEnabled = isRequired ? true : false
|
||
return `
|
||
<div class="cookie-banner-category" data-category="${cat.id}">
|
||
<div class="cookie-banner-category-info">
|
||
<div class="cookie-banner-category-name">${cat.name.de}</div>
|
||
<div class="cookie-banner-category-desc">${cat.description.de}</div>
|
||
</div>
|
||
<div class="cookie-banner-toggle ${isEnabled ? 'active' : ''} ${isRequired ? 'disabled' : ''}"
|
||
data-category="${cat.id}"
|
||
data-required="${isRequired}"></div>
|
||
</div>
|
||
`
|
||
})
|
||
.join('')
|
||
|
||
return `
|
||
<div class="cookie-banner-overlay" id="cookieBannerOverlay"></div>
|
||
<div class="cookie-banner" id="cookieBanner" role="dialog" aria-labelledby="cookieBannerTitle" aria-modal="true">
|
||
<div class="cookie-banner-title" id="cookieBannerTitle">${config.texts.title.de}</div>
|
||
<div class="cookie-banner-description">${config.texts.description.de}</div>
|
||
|
||
<div class="cookie-banner-buttons">
|
||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerReject">
|
||
${config.texts.rejectAll.de}
|
||
</button>
|
||
<button class="cookie-banner-btn cookie-banner-btn-secondary" id="cookieBannerCustomize">
|
||
${config.texts.customize.de}
|
||
</button>
|
||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerAccept">
|
||
${config.texts.acceptAll.de}
|
||
</button>
|
||
</div>
|
||
|
||
<div class="cookie-banner-details" id="cookieBannerDetails">
|
||
${categoriesHTML}
|
||
<div class="cookie-banner-buttons" style="margin-top: 16px;">
|
||
<button class="cookie-banner-btn cookie-banner-btn-primary" id="cookieBannerSave">
|
||
${config.texts.save.de}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="cookie-banner-links">
|
||
<a href="${privacyPolicyUrl}" class="cookie-banner-link" target="_blank">
|
||
${config.texts.privacyPolicyLink.de}
|
||
</a>
|
||
<a href="${config.impressumUrl || '/impressum'}" class="cookie-banner-link" target="_blank">
|
||
Impressum
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cookie Settings Re-Open (§7(3) DSGVO — Widerruf so einfach wie Einwilligung) -->
|
||
<a href="#" id="cookieBannerReopen" class="cookie-settings-footer-link"
|
||
onclick="document.getElementById('cookieBanner').style.display='block';document.getElementById('cookieBannerOverlay').classList.add('active');return false;"
|
||
style="position:fixed;bottom:8px;left:8px;z-index:9990;font-size:11px;color:#6b7280;text-decoration:none;background:rgba(255,255,255,0.9);padding:4px 8px;border-radius:4px;border:1px solid #e5e7eb;">
|
||
Cookie-Einstellungen
|
||
</a>
|
||
`.trim()
|
||
}
|
||
|
||
// =============================================================================
|
||
// JS GENERATION
|
||
// =============================================================================
|
||
|
||
function generateJS(config: CookieBannerConfig): string {
|
||
const categoryIds = config.categories.map((c) => c.id)
|
||
const requiredCategories = config.categories.filter((c) => c.isRequired).map((c) => c.id)
|
||
|
||
return `
|
||
(function() {
|
||
'use strict';
|
||
|
||
const COOKIE_NAME = 'cookie_consent';
|
||
const COOKIE_EXPIRY_DAYS = 365;
|
||
const CATEGORIES = ${JSON.stringify(categoryIds)};
|
||
const REQUIRED_CATEGORIES = ${JSON.stringify(requiredCategories)};
|
||
|
||
// Google Consent Mode v2 — PFLICHT seit Maerz 2024 fuer Google Services
|
||
// in EEA. Shim gtag/dataLayer falls Google Tag noch nicht initialisiert
|
||
// wurde, dann sofort den default consent state setzen (DENIED).
|
||
window.dataLayer = window.dataLayer || [];
|
||
if (typeof gtag !== 'function') {
|
||
window.gtag = function () { window.dataLayer.push(arguments); };
|
||
}
|
||
// wait_for_update gibt dem Banner 500ms Zeit, damit der Nutzer
|
||
// entscheiden kann bevor Tags feuern. Empfehlung von Google fuer GCM v2.
|
||
gtag('consent', 'default', {
|
||
analytics_storage: 'denied',
|
||
ad_storage: 'denied',
|
||
ad_user_data: 'denied',
|
||
ad_personalization: 'denied',
|
||
functionality_storage: 'granted',
|
||
security_storage: 'granted',
|
||
wait_for_update: 500,
|
||
region: ['EEA', 'CH', 'GB'],
|
||
});
|
||
|
||
function updateGoogleConsentMode(consent) {
|
||
if (typeof gtag !== 'function') return;
|
||
gtag('consent', 'update', {
|
||
analytics_storage: consent.statistics ? 'granted' : 'denied',
|
||
ad_storage: consent.marketing ? 'granted' : 'denied',
|
||
ad_user_data: consent.marketing ? 'granted' : 'denied',
|
||
ad_personalization: consent.marketing ? 'granted' : 'denied',
|
||
});
|
||
}
|
||
|
||
function getConsent() {
|
||
const cookie = document.cookie.split('; ').find(row => row.startsWith(COOKIE_NAME + '='));
|
||
if (!cookie) return null;
|
||
try {
|
||
return JSON.parse(decodeURIComponent(cookie.split('=')[1]));
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function saveConsent(consent) {
|
||
const date = new Date();
|
||
date.setTime(date.getTime() + (COOKIE_EXPIRY_DAYS * 24 * 60 * 60 * 1000));
|
||
document.cookie = COOKIE_NAME + '=' + encodeURIComponent(JSON.stringify(consent)) +
|
||
';expires=' + date.toUTCString() +
|
||
';path=/;SameSite=Lax';
|
||
// Append to local history (Art. 7(3) DSGVO Best-Practice + Borlabs-Parity).
|
||
// Server-seitiges Logging laeuft separat via consent-service.
|
||
try {
|
||
const HKEY = COOKIE_NAME + '_history';
|
||
const hist = JSON.parse(localStorage.getItem(HKEY) || '[]');
|
||
hist.push({
|
||
ts: new Date().toISOString(),
|
||
choices: consent,
|
||
});
|
||
if (hist.length > 50) hist.splice(0, hist.length - 50);
|
||
localStorage.setItem(HKEY, JSON.stringify(hist));
|
||
} catch (e) { /* localStorage blocked */ }
|
||
window.dispatchEvent(new CustomEvent('cookieConsentUpdated', { detail: consent }));
|
||
updateGoogleConsentMode(consent);
|
||
}
|
||
|
||
// Borlabs-Parity: zeigt dem Nutzer alle seine bisherigen Einwilligungen.
|
||
// Aufruf via window.bpShowConsentHistory() oder Klick auf den Link im Banner-Footer.
|
||
window.bpShowConsentHistory = function () {
|
||
var existing = document.getElementById('bpConsentHistoryModal');
|
||
if (existing) { existing.remove(); return; }
|
||
var hist = [];
|
||
try { hist = JSON.parse(localStorage.getItem(COOKIE_NAME + '_history') || '[]'); } catch (e) {}
|
||
var rows = hist.length === 0
|
||
? '<p style="color:#94a3b8;font-style:italic">Noch keine Einwilligungen gespeichert.</p>'
|
||
: hist.slice().reverse().map(function (h) {
|
||
var d = new Date(h.ts);
|
||
var parts = Object.keys(h.choices).map(function (k) {
|
||
return '<span style="margin-right:8px;font-size:11px;color:' +
|
||
(h.choices[k] ? '#16a34a' : '#dc2626') + '">' +
|
||
(h.choices[k] ? '✓ ' : '✗ ') + k + '</span>';
|
||
}).join('');
|
||
return '<div style="border-bottom:1px solid #e5e7eb;padding:8px 0">' +
|
||
'<div style="font-size:12px;color:#64748b;margin-bottom:4px">' +
|
||
d.toLocaleString('de-DE') + '</div>' +
|
||
'<div>' + parts + '</div></div>';
|
||
}).join('');
|
||
var modal = document.createElement('div');
|
||
modal.id = 'bpConsentHistoryModal';
|
||
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.5);' +
|
||
'z-index:999999;display:flex;align-items:center;justify-content:center;padding:20px';
|
||
modal.innerHTML = '<div style="background:white;border-radius:8px;max-width:500px;' +
|
||
'width:100%;max-height:80vh;overflow:auto;padding:20px;font-family:-apple-system,sans-serif">' +
|
||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">' +
|
||
'<h3 style="margin:0;font-size:16px">Ihre Einwilligungs-Historie</h3>' +
|
||
'<button onclick="document.getElementById(\\'bpConsentHistoryModal\\').remove()" ' +
|
||
'style="background:none;border:none;font-size:24px;cursor:pointer;color:#94a3b8">×</button>' +
|
||
'</div>' +
|
||
'<p style="font-size:12px;color:#64748b;margin:0 0 12px">' +
|
||
'Lokal in Ihrem Browser gespeichert. Server-seitig laufen Audit-Logs gemaess Art. 7(1) DSGVO.</p>' +
|
||
rows + '</div>';
|
||
modal.addEventListener('click', function (e) { if (e.target === modal) modal.remove(); });
|
||
document.body.appendChild(modal);
|
||
};
|
||
|
||
function hasConsent(category) {
|
||
const consent = getConsent();
|
||
if (!consent) return REQUIRED_CATEGORIES.includes(category);
|
||
return consent[category] === true;
|
||
}
|
||
|
||
function initBanner() {
|
||
const banner = document.getElementById('cookieBanner');
|
||
const overlay = document.getElementById('cookieBannerOverlay');
|
||
const details = document.getElementById('cookieBannerDetails');
|
||
|
||
if (!banner) return;
|
||
|
||
const consent = getConsent();
|
||
if (consent) return;
|
||
|
||
setTimeout(() => {
|
||
banner.classList.add('active');
|
||
overlay.classList.add('active');
|
||
}, 500);
|
||
|
||
document.getElementById('cookieBannerAccept')?.addEventListener('click', () => {
|
||
const consent = {};
|
||
CATEGORIES.forEach(cat => consent[cat] = true);
|
||
saveConsent(consent);
|
||
closeBanner();
|
||
});
|
||
|
||
document.getElementById('cookieBannerReject')?.addEventListener('click', () => {
|
||
const consent = {};
|
||
CATEGORIES.forEach(cat => consent[cat] = REQUIRED_CATEGORIES.includes(cat));
|
||
saveConsent(consent);
|
||
closeBanner();
|
||
});
|
||
|
||
document.getElementById('cookieBannerCustomize')?.addEventListener('click', () => {
|
||
details.classList.toggle('active');
|
||
});
|
||
|
||
document.getElementById('cookieBannerSave')?.addEventListener('click', () => {
|
||
const consent = {};
|
||
CATEGORIES.forEach(cat => {
|
||
const toggle = document.querySelector('.cookie-banner-toggle[data-category="' + cat + '"]');
|
||
consent[cat] = toggle?.classList.contains('active') || REQUIRED_CATEGORIES.includes(cat);
|
||
});
|
||
saveConsent(consent);
|
||
closeBanner();
|
||
});
|
||
|
||
document.querySelectorAll('.cookie-banner-toggle').forEach(toggle => {
|
||
if (toggle.dataset.required === 'true') return;
|
||
toggle.addEventListener('click', () => {
|
||
toggle.classList.toggle('active');
|
||
});
|
||
});
|
||
|
||
overlay?.addEventListener('click', () => {
|
||
// Don't close - user must make a choice
|
||
});
|
||
}
|
||
|
||
function closeBanner() {
|
||
const banner = document.getElementById('cookieBanner');
|
||
const overlay = document.getElementById('cookieBannerOverlay');
|
||
banner?.classList.remove('active');
|
||
overlay?.classList.remove('active');
|
||
}
|
||
|
||
// Script-Blocking: activate scripts with data-cookie-category ONLY after consent
|
||
function activateConsentedScripts() {
|
||
const consent = getConsent();
|
||
if (!consent) return;
|
||
|
||
// Find all blocked scripts (type="text/plain" with data-cookie-category)
|
||
document.querySelectorAll('script[data-cookie-category][type="text/plain"]').forEach(script => {
|
||
const category = script.getAttribute('data-cookie-category');
|
||
if (consent[category] === true) {
|
||
// Replace type to activate the script
|
||
const newScript = document.createElement('script');
|
||
if (script.src) newScript.src = script.src;
|
||
else newScript.textContent = script.textContent;
|
||
newScript.type = 'text/javascript';
|
||
script.parentNode.replaceChild(newScript, script);
|
||
}
|
||
});
|
||
|
||
// Also fire custom event for programmatic listeners
|
||
window.dispatchEvent(new CustomEvent('cookieConsentActivated', { detail: consent }));
|
||
}
|
||
|
||
// Run script activation after consent is saved
|
||
window.addEventListener('cookieConsentUpdated', activateConsentedScripts);
|
||
|
||
window.CookieConsent = {
|
||
getConsent,
|
||
saveConsent,
|
||
hasConsent,
|
||
show: () => {
|
||
document.getElementById('cookieBanner')?.classList.add('active');
|
||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
||
},
|
||
hide: closeBanner,
|
||
activateScripts: activateConsentedScripts,
|
||
};
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initBanner();
|
||
activateConsentedScripts();
|
||
});
|
||
} else {
|
||
initBanner();
|
||
activateConsentedScripts();
|
||
}
|
||
})();
|
||
|
||
/*
|
||
* USAGE: Script-Blocking
|
||
*
|
||
* Instead of:
|
||
* <script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
|
||
*
|
||
* Use:
|
||
* <script type="text/plain" data-cookie-category="statistics"
|
||
* src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
|
||
*
|
||
* The script will only execute AFTER the user consents to "statistics".
|
||
*/
|
||
`.trim()
|
||
}
|