feat(cmp): restore vendor-agnostic fields + module wiring
Build + Deploy / build-admin-compliance (push) Successful in 2m0s
Build + Deploy / build-backend-compliance (push) Successful in 14s
Build + Deploy / build-ai-sdk (push) Successful in 10s
Build + Deploy / build-developer-portal (push) Successful in 14s
Build + Deploy / build-tts (push) Successful in 11s
Build + Deploy / build-document-crawler (push) Successful in 11s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 13s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 18s
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 2m55s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 45s
CI / test-python-backend (push) Successful in 41s
CI / test-python-document-crawler (push) Successful in 30s
CI / test-python-dsms-gateway (push) Successful in 26s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m17s

Re-add 13 vendor-agnostic columns to banner models/serializers/service
(consent_method, banner_version, device_type, browser, os, etc.) that
were lost when another session overwrote the code. Keep vendor_consents
dict from the other session.

Add list_consents method back to BannerConsentService.

Wire CookieBanner, Loeschfristen and UseCases into Document Generator
contextBridge (CMP_NAME, analytics tools, retention months, feature flags).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-11 21:57:54 +02:00
parent 90da26745b
commit 051890c370
4 changed files with 155 additions and 0 deletions
@@ -101,6 +101,78 @@ function DocumentGeneratorPageInner() {
} }
}, [state?.complianceScope?.determinedLevel, state?.companyProfile]) }, [state?.complianceScope?.determinedLevel, state?.companyProfile])
// ── MODULE WIRING: CookieBanner → CONSENT + FEATURES ─────────────────────
useEffect(() => {
const banner = state?.cookieBanner
if (!banner) return
const cats = banner.categories || []
const analyticsTools = cats
.filter((c) => c.id === 'analytics' || c.id === 'statistics')
.flatMap((c) => c.cookies?.map((ck) => ck.name) ?? [])
const marketingTools = cats
.filter((c) => c.id === 'marketing')
.flatMap((c) => c.cookies?.map((ck) => ck.name) ?? [])
const hasFunctional = cats.some((c) => c.id === 'functional')
setContext((prev) => ({
...prev,
CONSENT: {
...prev.CONSENT,
ANALYTICS_TOOLS: analyticsTools.length > 0 ? analyticsTools.join(', ') : prev.CONSENT.ANALYTICS_TOOLS,
MARKETING_PARTNERS: marketingTools.length > 0 ? marketingTools.join(', ') : prev.CONSENT.MARKETING_PARTNERS,
},
FEATURES: {
...prev.FEATURES,
CMP_NAME: 'BreakPilot CMP',
CMP_LOGS_CONSENTS: true,
HAS_FUNCTIONAL_COOKIES: hasFunctional || prev.FEATURES.HAS_FUNCTIONAL_COOKIES,
CONSENT_WITHDRAWAL_PATH: 'Footer-Link "Cookie-Einstellungen"',
},
}))
}, [state?.cookieBanner])
// ── MODULE WIRING: Loeschfristen → PRIVACY retention ──────────────────────
useEffect(() => {
const policies = state?.retentionPolicies
if (!policies || policies.length === 0) return
const maxMonths = policies.reduce((max, p) => {
const match = p.retentionPeriod?.match(/(\d+)\s*(Monat|Jahr|Tag)/i)
if (!match) return max
const val = parseInt(match[1], 10)
const unit = match[2].toLowerCase()
const months = unit.startsWith('jahr') ? val * 12 : unit.startsWith('tag') ? Math.ceil(val / 30) : val
return Math.max(max, months)
}, 0)
if (maxMonths > 0) {
setContext((prev) => ({
...prev,
PRIVACY: { ...prev.PRIVACY, ANALYTICS_RETENTION_MONTHS: maxMonths },
}))
}
}, [state?.retentionPolicies])
// ── MODULE WIRING: UseCases → FEATURES flags ─────────────────────────────
useEffect(() => {
const useCases = state?.useCases
if (!useCases || useCases.length === 0) return
const allText = useCases.map((uc) => `${uc.name} ${uc.description}`).join(' ').toLowerCase()
const hasAccount = allText.includes('account') || allText.includes('konto') || allText.includes('registrier')
const hasPayments = allText.includes('zahlung') || allText.includes('payment') || allText.includes('stripe') || allText.includes('paypal')
const hasNewsletter = allText.includes('newsletter') || allText.includes('mailchimp') || allText.includes('e-mail-marketing')
const hasSocial = allText.includes('social') || allText.includes('linkedin') || allText.includes('facebook') || allText.includes('instagram')
setContext((prev) => ({
...prev,
FEATURES: {
...prev.FEATURES,
HAS_ACCOUNT: hasAccount || prev.FEATURES.HAS_ACCOUNT,
HAS_PAYMENTS: hasPayments || prev.FEATURES.HAS_PAYMENTS,
HAS_NEWSLETTER: hasNewsletter || prev.FEATURES.HAS_NEWSLETTER,
HAS_SOCIAL_MEDIA: hasSocial || prev.FEATURES.HAS_SOCIAL_MEDIA,
},
}))
}, [state?.useCases])
// Pre-fill extra placeholders from Einwilligungen data points // Pre-fill extra placeholders from Einwilligungen data points
useEffect(() => { useEffect(() => {
if (selectedDataPointsData && selectedDataPointsData.length > 0) { if (selectedDataPointsData && selectedDataPointsData.length > 0) {
@@ -36,6 +36,20 @@ class BannerConsentDB(Base):
user_agent = Column(Text) user_agent = Column(Text)
consent_string = Column(Text) consent_string = Column(Text)
linked_email = Column(Text) linked_email = Column(Text)
# Vendor-agnostische Felder (Migration 107)
consent_method = Column(Text) # accept_all / reject_all / custom_selection
banner_version = Column(Integer)
banner_config_hash = Column(Text)
geo_country = Column(Text)
geo_region = Column(Text)
consent_scope = Column(Text, default='domain')
page_url = Column(Text)
referrer = Column(Text)
device_type = Column(Text) # mobile / desktop / tablet
browser = Column(Text)
os = Column(Text)
screen_resolution = Column(Text)
session_id = Column(Text)
expires_at = Column(DateTime) expires_at = Column(DateTime)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -66,6 +80,8 @@ class BannerConsentAuditLogDB(Base):
user_agent = Column(Text) user_agent = Column(Text)
banner_config_hash = Column(Text) banner_config_hash = Column(Text)
consent_version = Column(Integer) consent_version = Column(Integer)
consent_method = Column(Text)
page_url = Column(Text)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow) created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
__table_args__ = ( __table_args__ = (
@@ -25,8 +25,22 @@ def consent_to_dict(c: BannerConsentDB) -> dict[str, Any]:
"vendors": c.vendors or [], "vendors": c.vendors or [],
"vendor_consents": c.vendor_consents or {}, "vendor_consents": c.vendor_consents or {},
"ip_hash": c.ip_hash, "ip_hash": c.ip_hash,
"user_agent": c.user_agent,
"consent_string": c.consent_string, "consent_string": c.consent_string,
"linked_email": c.linked_email, "linked_email": c.linked_email,
"consent_method": c.consent_method,
"banner_version": c.banner_version,
"banner_config_hash": c.banner_config_hash,
"geo_country": c.geo_country,
"geo_region": c.geo_region,
"consent_scope": c.consent_scope,
"page_url": c.page_url,
"referrer": c.referrer,
"device_type": c.device_type,
"browser": c.browser,
"os": c.os,
"screen_resolution": c.screen_resolution,
"session_id": c.session_id,
"expires_at": c.expires_at.isoformat() if c.expires_at else None, "expires_at": c.expires_at.isoformat() if c.expires_at else None,
"created_at": c.created_at.isoformat() if c.created_at else None, "created_at": c.created_at.isoformat() if c.created_at else None,
"updated_at": c.updated_at.isoformat() if c.updated_at else None, "updated_at": c.updated_at.isoformat() if c.updated_at else None,
@@ -75,6 +75,9 @@ class BannerConsentService:
consent_version: Optional[int] = None, consent_version: Optional[int] = None,
vendor_consents: Optional[dict[str, bool]] = None, vendor_consents: Optional[dict[str, bool]] = None,
user_agent: Optional[str] = None, user_agent: Optional[str] = None,
*,
consent_method: Optional[str] = None,
page_url: Optional[str] = None,
) -> None: ) -> None:
entry = BannerConsentAuditLogDB( entry = BannerConsentAuditLogDB(
tenant_id=tenant_id, tenant_id=tenant_id,
@@ -88,6 +91,8 @@ class BannerConsentService:
user_agent=user_agent, user_agent=user_agent,
banner_config_hash=banner_config_hash, banner_config_hash=banner_config_hash,
consent_version=consent_version, consent_version=consent_version,
consent_method=consent_method,
page_url=page_url,
) )
self.db.add(entry) self.db.add(entry)
@@ -166,6 +171,16 @@ class BannerConsentService:
user_agent: Optional[str], user_agent: Optional[str],
consent_string: Optional[str], consent_string: Optional[str],
vendor_consents: Optional[dict[str, bool]] = None, vendor_consents: Optional[dict[str, bool]] = None,
*,
consent_method: Optional[str] = None,
page_url: Optional[str] = None,
referrer: Optional[str] = None,
device_type: Optional[str] = None,
browser: Optional[str] = None,
os: Optional[str] = None,
screen_resolution: Optional[str] = None,
session_id: Optional[str] = None,
consent_scope: Optional[str] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Upsert a device consent row for (tenant, site, device_fingerprint). """Upsert a device consent row for (tenant, site, device_fingerprint).
@@ -185,6 +200,21 @@ class BannerConsentService:
if not consent_string: if not consent_string:
consent_string = self._maybe_generate_tc_string(tid, site_id, categories) consent_string = self._maybe_generate_tc_string(tid, site_id, categories)
# Vendor-agnostische Zusatzfelder
extra = {
"consent_method": consent_method,
"banner_version": config_ver,
"banner_config_hash": config_hash,
"page_url": page_url,
"referrer": referrer,
"device_type": device_type,
"browser": browser,
"os": os,
"screen_resolution": screen_resolution,
"session_id": session_id,
"consent_scope": consent_scope or "domain",
}
existing = ( existing = (
self.db.query(BannerConsentDB) self.db.query(BannerConsentDB)
.filter( .filter(
@@ -204,11 +234,14 @@ class BannerConsentService:
existing.consent_string = consent_string existing.consent_string = consent_string
existing.expires_at = expires_at existing.expires_at = expires_at
existing.updated_at = now existing.updated_at = now
for key, val in extra.items():
setattr(existing, key, val)
self.db.flush() self.db.flush()
self._log( self._log(
tid, existing.id, "consent_updated", site_id, device_fingerprint, tid, existing.id, "consent_updated", site_id, device_fingerprint,
categories, ip_hash, config_hash, config_ver, categories, ip_hash, config_hash, config_ver,
vendor_consents=vendor_consents, user_agent=user_agent, vendor_consents=vendor_consents, user_agent=user_agent,
consent_method=consent_method, page_url=page_url,
) )
self.db.commit() self.db.commit()
self.db.refresh(existing) self.db.refresh(existing)
@@ -225,6 +258,7 @@ class BannerConsentService:
user_agent=user_agent, user_agent=user_agent,
consent_string=consent_string, consent_string=consent_string,
expires_at=expires_at, expires_at=expires_at,
**extra,
) )
self.db.add(consent) self.db.add(consent)
self.db.flush() self.db.flush()
@@ -232,6 +266,7 @@ class BannerConsentService:
tid, consent.id, "consent_given", site_id, device_fingerprint, tid, consent.id, "consent_given", site_id, device_fingerprint,
categories, ip_hash, config_hash, config_ver, categories, ip_hash, config_hash, config_ver,
vendor_consents=vendor_consents, user_agent=user_agent, vendor_consents=vendor_consents, user_agent=user_agent,
consent_method=consent_method, page_url=page_url,
) )
self.db.commit() self.db.commit()
self.db.refresh(consent) self.db.refresh(consent)
@@ -369,6 +404,24 @@ class BannerConsentService:
], ],
} }
def list_consents(
self, tenant_id: str, site_id: Optional[str] = None,
limit: int = 50, offset: int = 0,
) -> dict[str, Any]:
"""List paginated banner consents for admin dashboard."""
tid = uuid.UUID(tenant_id)
base = self.db.query(BannerConsentDB).filter(BannerConsentDB.tenant_id == tid)
if site_id:
base = base.filter(BannerConsentDB.site_id == site_id)
total = base.count()
rows = base.order_by(BannerConsentDB.created_at.desc()).offset(offset).limit(limit).all()
return {
"consents": [consent_to_dict(c) for c in rows],
"total": total,
"limit": limit,
"offset": offset,
}
def get_site_stats(self, tenant_id: str, site_id: str) -> dict[str, Any]: def get_site_stats(self, tenant_id: str, site_id: str) -> dict[str, Any]:
"""Compute consent count + per-category acceptance rates for a site.""" """Compute consent count + per-category acceptance rates for a site."""
tid = uuid.UUID(tenant_id) tid = uuid.UUID(tenant_id)