feat: Vendor-level consent + Consent analytics (F4 + F6)

F4: Granular Vendor-Level Consent
- Migration 113: vendor_consents JSONB on banner_consents + audit_log
- ConsentCreate schema + BannerConsentDB model extended
- banner_consent_service stores vendor_consents alongside categories
- Audit trail includes vendor-level decisions + user_agent

F6: Consent Rate Analytics
- Migration 114: user_agent on audit_log + time-series index
- BannerAnalyticsService: time series, category breakdown, device stats
- banner_analytics_routes: 4 endpoints (overview, time-series, categories, devices)
- AnalyticsDashboard.tsx: KPIs, bar chart, category bars, device breakdown
- New "Analytik" tab in cookie-banner page

[migration-approved]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-03 20:58:06 +02:00
parent 36d9f929c6
commit c3fcfe88ee
12 changed files with 432 additions and 1 deletions
@@ -73,6 +73,8 @@ class BannerConsentService:
ip_hash: Optional[str] = None,
banner_config_hash: Optional[str] = None,
consent_version: Optional[int] = None,
vendor_consents: Optional[dict[str, bool]] = None,
user_agent: Optional[str] = None,
) -> None:
entry = BannerConsentAuditLogDB(
tenant_id=tenant_id,
@@ -81,7 +83,9 @@ class BannerConsentService:
site_id=site_id,
device_fingerprint=device_fingerprint,
categories=categories or [],
vendor_consents=vendor_consents or {},
ip_hash=ip_hash,
user_agent=user_agent,
banner_config_hash=banner_config_hash,
consent_version=consent_version,
)
@@ -143,6 +147,7 @@ class BannerConsentService:
ip_address: Optional[str],
user_agent: Optional[str],
consent_string: Optional[str],
vendor_consents: Optional[dict[str, bool]] = None,
) -> dict[str, Any]:
"""Upsert a device consent row for (tenant, site, device_fingerprint).
@@ -171,6 +176,7 @@ class BannerConsentService:
if existing:
existing.categories = categories
existing.vendors = vendors
existing.vendor_consents = vendor_consents or {}
existing.ip_hash = ip_hash
existing.user_agent = user_agent
existing.consent_string = consent_string
@@ -180,6 +186,7 @@ class BannerConsentService:
self._log(
tid, existing.id, "consent_updated", site_id, device_fingerprint,
categories, ip_hash, config_hash, config_ver,
vendor_consents=vendor_consents, user_agent=user_agent,
)
self.db.commit()
self.db.refresh(existing)
@@ -191,6 +198,7 @@ class BannerConsentService:
device_fingerprint=device_fingerprint,
categories=categories,
vendors=vendors,
vendor_consents=vendor_consents or {},
ip_hash=ip_hash,
user_agent=user_agent,
consent_string=consent_string,
@@ -201,6 +209,7 @@ class BannerConsentService:
self._log(
tid, consent.id, "consent_given", site_id, device_fingerprint,
categories, ip_hash, config_hash, config_ver,
vendor_consents=vendor_consents, user_agent=user_agent,
)
self.db.commit()
self.db.refresh(consent)