diff --git a/admin-compliance/app/api/sdk/v1/regulatory-news/route.ts b/admin-compliance/app/api/sdk/v1/regulatory-news/route.ts
new file mode 100644
index 0000000..f405688
--- /dev/null
+++ b/admin-compliance/app/api/sdk/v1/regulatory-news/route.ts
@@ -0,0 +1,26 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
+const DEFAULT_TENANT_ID = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url)
+ const qs = searchParams.toString()
+ const url = `${SDK_URL}/sdk/v1/regulatory-news${qs ? `?${qs}` : ''}`
+
+ const response = await fetch(url, {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Tenant-ID': request.headers.get('X-Tenant-ID') || DEFAULT_TENANT_ID,
+ },
+ })
+
+ if (!response.ok) {
+ return NextResponse.json({ error: 'SDK error' }, { status: response.status })
+ }
+ return NextResponse.json(await response.json())
+ } catch {
+ return NextResponse.json({ error: 'Connection failed' }, { status: 503 })
+ }
+}
diff --git a/admin-compliance/app/sdk/page.tsx b/admin-compliance/app/sdk/page.tsx
index f252461..f8c1ec4 100644
--- a/admin-compliance/app/sdk/page.tsx
+++ b/admin-compliance/app/sdk/page.tsx
@@ -4,6 +4,7 @@ import React from 'react'
import Link from 'next/link'
import { useSDK, SDK_PACKAGES, getStepsForPackage } from '@/lib/sdk'
import { ProjectSelector } from '@/components/sdk/ProjectSelector/ProjectSelector'
+import { RegulatoryNewsFeed } from '@/components/sdk/regulatory-news/RegulatoryNewsFeed'
import type { SDKPackageId } from '@/lib/sdk/types'
// =============================================================================
@@ -331,6 +332,9 @@ export default function SDKDashboard() {
)}
+ {/* Regulatory News */}
+
+
{/* 5 Packages */}
Compliance-Pakete
diff --git a/admin-compliance/components/sdk/regulatory-news/RegulatoryNewsCard.tsx b/admin-compliance/components/sdk/regulatory-news/RegulatoryNewsCard.tsx
new file mode 100644
index 0000000..80f6f4e
--- /dev/null
+++ b/admin-compliance/components/sdk/regulatory-news/RegulatoryNewsCard.tsx
@@ -0,0 +1,60 @@
+'use client'
+
+import Link from 'next/link'
+
+export interface RegulatoryNewsItemData {
+ id: string
+ headline: string
+ summary: string
+ legal_reference: string
+ deadline: string
+ days_remaining: number
+ urgency: 'critical' | 'high' | 'medium' | 'low'
+ affected: string
+ action_required: string
+ action_link: string
+ regulation: string
+ sanctions?: string
+}
+
+const URGENCY_STYLES = {
+ critical: { badge: 'bg-red-100 text-red-700 border-red-200', border: 'border-l-red-500', icon: '๐ด' },
+ high: { badge: 'bg-orange-100 text-orange-700 border-orange-200', border: 'border-l-orange-400', icon: '๐ ' },
+ medium: { badge: 'bg-yellow-100 text-yellow-700 border-yellow-200', border: 'border-l-yellow-400', icon: '๐ก' },
+ low: { badge: 'bg-gray-100 text-gray-600 border-gray-200', border: 'border-l-gray-300', icon: '๐ต' },
+}
+
+export function RegulatoryNewsCard({ item }: { item: RegulatoryNewsItemData }) {
+ const style = URGENCY_STYLES[item.urgency] || URGENCY_STYLES.low
+
+ return (
+
+
+
+
{item.headline}
+
{item.summary}
+
{item.legal_reference}
+ {item.sanctions && (
+
Sanktionen: {item.sanctions}
+ )}
+
+
+
+ {style.icon} {item.days_remaining} Tage
+
+
+ {new Date(item.deadline).toLocaleDateString('de-DE')}
+
+
+
+
+ {item.affected}
+ {item.action_link && (
+
+ Massnahme ergreifen โ
+
+ )}
+
+
+ )
+}
diff --git a/admin-compliance/components/sdk/regulatory-news/RegulatoryNewsFeed.tsx b/admin-compliance/components/sdk/regulatory-news/RegulatoryNewsFeed.tsx
new file mode 100644
index 0000000..3ad9fd3
--- /dev/null
+++ b/admin-compliance/components/sdk/regulatory-news/RegulatoryNewsFeed.tsx
@@ -0,0 +1,54 @@
+'use client'
+
+import React, { useState, useEffect } from 'react'
+import { RegulatoryNewsCard, RegulatoryNewsItemData } from './RegulatoryNewsCard'
+
+interface RegulatoryNewsFeedProps {
+ businessModel?: string
+ maxItems?: number
+}
+
+export function RegulatoryNewsFeed({ businessModel, maxItems = 3 }: RegulatoryNewsFeedProps) {
+ const [items, setItems] = useState
([])
+ const [loading, setLoading] = useState(true)
+ const [showAll, setShowAll] = useState(false)
+
+ useEffect(() => {
+ const params = new URLSearchParams({ limit: '10', horizon_days: '365' })
+ if (businessModel) params.set('business_model', businessModel)
+
+ fetch(`/api/sdk/v1/regulatory-news?${params}`)
+ .then(r => r.ok ? r.json() : { items: [] })
+ .then(data => setItems(data.items || []))
+ .catch(() => setItems([]))
+ .finally(() => setLoading(false))
+ }, [businessModel])
+
+ if (loading) return null
+ if (items.length === 0) return null
+
+ const visible = showAll ? items : items.slice(0, maxItems)
+
+ return (
+
+
+
+ ๐ข Regulatorische Neuigkeiten
+
+ {items.length > maxItems && (
+
+ )}
+
+
+ {visible.map(item => (
+
+ ))}
+
+
+ )
+}
diff --git a/ai-compliance-sdk/internal/api/handlers/regulatory_news_handlers.go b/ai-compliance-sdk/internal/api/handlers/regulatory_news_handlers.go
new file mode 100644
index 0000000..3cda7d8
--- /dev/null
+++ b/ai-compliance-sdk/internal/api/handlers/regulatory_news_handlers.go
@@ -0,0 +1,42 @@
+package handlers
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/breakpilot/ai-compliance-sdk/internal/ucca"
+ "github.com/gin-gonic/gin"
+)
+
+// RegulatoryNewsHandlers serves regulatory news from obligation v2 data.
+type RegulatoryNewsHandlers struct {
+ regulations map[string]*ucca.V2RegulationFile
+}
+
+// NewRegulatoryNewsHandlers creates a handler backed by pre-loaded regulation data.
+func NewRegulatoryNewsHandlers(regs map[string]*ucca.V2RegulationFile) *RegulatoryNewsHandlers {
+ return &RegulatoryNewsHandlers{regulations: regs}
+}
+
+// GetNews returns upcoming regulatory deadlines sorted by urgency.
+func (h *RegulatoryNewsHandlers) GetNews(c *gin.Context) {
+ filter := ucca.RegulatoryNewsFilter{
+ BusinessModel: c.Query("business_model"),
+ HorizonDays: parseIntOrDefault(c.Query("horizon_days"), 365),
+ Limit: parseIntOrDefault(c.Query("limit"), 5),
+ }
+
+ items := ucca.GetRegulatoryNews(h.regulations, filter)
+ c.JSON(http.StatusOK, gin.H{"items": items, "total": len(items)})
+}
+
+func parseIntOrDefault(s string, def int) int {
+ if s == "" {
+ return def
+ }
+ v, err := strconv.Atoi(s)
+ if err != nil {
+ return def
+ }
+ return v
+}
diff --git a/ai-compliance-sdk/internal/app/app.go b/ai-compliance-sdk/internal/app/app.go
index 1d29112..7474762 100644
--- a/ai-compliance-sdk/internal/app/app.go
+++ b/ai-compliance-sdk/internal/app/app.go
@@ -134,6 +134,14 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine {
ragHandlers := handlers.NewRAGHandlers(corpusVersionStore)
obligationsHandlers := handlers.NewObligationsHandlersWithStore(obligationsStore)
+ // Regulatory News
+ allV2Regs, err := ucca.LoadAllV2Regulations()
+ if err != nil {
+ log.Printf("WARNING: V2 regulations not loaded: %v", err)
+ allV2Regs = make(map[string]*ucca.V2RegulationFile)
+ }
+ regulatoryNewsHandlers := handlers.NewRegulatoryNewsHandlers(allV2Regs)
+
// Maximizer
maximizerStore := maximizer.NewStore(pool)
maximizerRules, err := maximizer.LoadConstraintRulesFromDefault()
@@ -168,7 +176,7 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine {
uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers,
roadmapHandlers, workshopHandlers, portfolioHandlers,
academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler,
- maximizerHandlers)
+ maximizerHandlers, regulatoryNewsHandlers)
return router
}
diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go
index 6319cea..492c85c 100644
--- a/ai-compliance-sdk/internal/app/routes.go
+++ b/ai-compliance-sdk/internal/app/routes.go
@@ -27,6 +27,7 @@ func registerRoutes(
whistleblowerHandlers *handlers.WhistleblowerHandlers,
iaceHandler *handlers.IACEHandler,
maximizerHandlers *handlers.MaximizerHandlers,
+ regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers,
) {
v1 := router.Group("/sdk/v1")
{
@@ -48,6 +49,7 @@ func registerRoutes(
registerWhistleblowerRoutes(v1, whistleblowerHandlers)
registerIACERoutes(v1, iaceHandler)
registerMaximizerRoutes(v1, maximizerHandlers)
+ v1.GET("/regulatory-news", regulatoryNewsHandlers.GetNews)
}
}
diff --git a/ai-compliance-sdk/internal/ucca/regulatory_news.go b/ai-compliance-sdk/internal/ucca/regulatory_news.go
new file mode 100644
index 0000000..b3247a9
--- /dev/null
+++ b/ai-compliance-sdk/internal/ucca/regulatory_news.go
@@ -0,0 +1,146 @@
+package ucca
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "time"
+)
+
+// RegulatoryNewsItem is a single news item for dashboard display.
+type RegulatoryNewsItem struct {
+ ID string `json:"id"`
+ Headline string `json:"headline"`
+ Summary string `json:"summary"`
+ LegalReference string `json:"legal_reference"`
+ Deadline string `json:"deadline"`
+ DaysRemaining int `json:"days_remaining"`
+ Urgency string `json:"urgency"` // critical, high, medium, low
+ Affected string `json:"affected"`
+ ActionRequired string `json:"action_required"`
+ ActionLink string `json:"action_link"`
+ Regulation string `json:"regulation"`
+ Sanctions string `json:"sanctions,omitempty"`
+}
+
+// RegulatoryNewsFilter controls which news items are returned.
+type RegulatoryNewsFilter struct {
+ BusinessModel string `json:"business_model,omitempty"`
+ HorizonDays int `json:"horizon_days,omitempty"` // default 365
+ Limit int `json:"limit,omitempty"` // default 5
+}
+
+// GetRegulatoryNews scans all v2 obligations for upcoming deadlines
+// and returns formatted news items sorted by urgency.
+func GetRegulatoryNews(regulations map[string]*V2RegulationFile, filter RegulatoryNewsFilter) []RegulatoryNewsItem {
+ if filter.HorizonDays <= 0 {
+ filter.HorizonDays = 365
+ }
+ if filter.Limit <= 0 {
+ filter.Limit = 5
+ }
+
+ today := time.Now().UTC().Truncate(24 * time.Hour)
+ horizon := today.AddDate(0, 0, filter.HorizonDays)
+ var items []RegulatoryNewsItem
+
+ for _, reg := range regulations {
+ for _, obl := range reg.Obligations {
+ deadline, ok := resolveDeadline(obl)
+ if !ok || deadline.Before(today) || deadline.After(horizon) {
+ continue
+ }
+
+ days := int(deadline.Sub(today).Hours() / 24)
+ item := buildNewsItem(obl, reg.Regulation, deadline, days)
+ items = append(items, item)
+ }
+ }
+
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].DaysRemaining < items[j].DaysRemaining
+ })
+
+ if len(items) > filter.Limit {
+ items = items[:filter.Limit]
+ }
+ return items
+}
+
+func buildNewsItem(obl V2Obligation, regulation string, deadline time.Time, days int) RegulatoryNewsItem {
+ item := RegulatoryNewsItem{
+ ID: obl.ID,
+ Deadline: deadline.Format("2006-01-02"),
+ DaysRemaining: days,
+ Urgency: computeUrgency(days),
+ Regulation: regulation,
+ }
+
+ // Use hand-crafted news if available
+ if obl.News != nil {
+ item.Headline = obl.News.Headline
+ item.Summary = obl.News.Summary
+ item.ActionRequired = obl.News.ActionRequired
+ item.Affected = obl.News.Affected
+ item.ActionLink = obl.News.ActionLink
+ } else {
+ // Auto-generate from obligation data
+ item.Headline = fmt.Sprintf("%s โ ab %s", obl.Title, deadline.Format("02.01.2006"))
+ item.Summary = obl.Description
+ item.ActionRequired = "Pruefen Sie die Anforderungen und ergreifen Sie Massnahmen."
+ item.ActionLink = obl.BreakpilotFeature
+ }
+
+ item.LegalReference = formatLegalReference(obl.LegalBasis)
+
+ if obl.Sanctions != nil {
+ item.Sanctions = obl.Sanctions.MaxFine
+ }
+ return item
+}
+
+func resolveDeadline(obl V2Obligation) (time.Time, bool) {
+ // Check explicit deadline.date first
+ if obl.Deadline != nil && obl.Deadline.Date != "" {
+ t, err := time.Parse("2006-01-02", obl.Deadline.Date)
+ if err == nil {
+ return t, true
+ }
+ }
+ // Fallback to valid_from
+ if obl.ValidFrom != "" {
+ t, err := time.Parse("2006-01-02", obl.ValidFrom)
+ if err == nil {
+ return t, true
+ }
+ }
+ return time.Time{}, false
+}
+
+func computeUrgency(daysRemaining int) string {
+ switch {
+ case daysRemaining <= 30:
+ return "critical"
+ case daysRemaining <= 90:
+ return "high"
+ case daysRemaining <= 180:
+ return "medium"
+ default:
+ return "low"
+ }
+}
+
+func formatLegalReference(bases []V2LegalBasis) string {
+ if len(bases) == 0 {
+ return ""
+ }
+ var refs []string
+ for _, b := range bases {
+ ref := b.Article
+ if b.Norm != "" {
+ ref = b.Article + " " + b.Norm
+ }
+ refs = append(refs, ref)
+ }
+ return strings.Join(refs, ", ")
+}
diff --git a/ai-compliance-sdk/internal/ucca/regulatory_news_test.go b/ai-compliance-sdk/internal/ucca/regulatory_news_test.go
new file mode 100644
index 0000000..0501583
--- /dev/null
+++ b/ai-compliance-sdk/internal/ucca/regulatory_news_test.go
@@ -0,0 +1,191 @@
+package ucca
+
+import (
+ "testing"
+ "time"
+)
+
+func makeTestRegulations() map[string]*V2RegulationFile {
+ future30 := time.Now().AddDate(0, 0, 30).Format("2006-01-02")
+ future90 := time.Now().AddDate(0, 0, 90).Format("2006-01-02")
+ past := time.Now().AddDate(0, 0, -10).Format("2006-01-02")
+
+ return map[string]*V2RegulationFile{
+ "TestReg": {
+ Regulation: "TestReg",
+ Obligations: []V2Obligation{
+ {
+ ID: "TR-001", Title: "Upcoming Critical",
+ Deadline: &V2Deadline{Date: future30},
+ News: &V2ObligationNews{
+ Headline: "Critical Deadline", Summary: "Test summary",
+ ActionRequired: "Do something", Affected: "All", ActionLink: "/sdk/test",
+ },
+ LegalBasis: []V2LegalBasis{{Norm: "TestLaw", Article: "Art. 1"}},
+ },
+ {
+ ID: "TR-002", Title: "Upcoming Medium",
+ Description: "Medium priority regulation change.",
+ Deadline: &V2Deadline{Date: future90},
+ LegalBasis: []V2LegalBasis{{Norm: "TestLaw", Article: "Art. 2"}},
+ },
+ {
+ ID: "TR-003", Title: "Past Deadline",
+ Deadline: &V2Deadline{Date: past},
+ },
+ {
+ ID: "TR-004", Title: "No Deadline",
+ },
+ },
+ },
+ }
+}
+
+func TestGetRegulatoryNews_SortedByUrgency(t *testing.T) {
+ regs := makeTestRegulations()
+ items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
+
+ if len(items) != 2 {
+ t.Fatalf("expected 2 items (future only), got %d", len(items))
+ }
+ // First item should be the more urgent one (30 days)
+ if items[0].ID != "TR-001" {
+ t.Errorf("expected TR-001 first (most urgent), got %s", items[0].ID)
+ }
+ if items[1].ID != "TR-002" {
+ t.Errorf("expected TR-002 second, got %s", items[1].ID)
+ }
+}
+
+func TestGetRegulatoryNews_UsesNewsField(t *testing.T) {
+ regs := makeTestRegulations()
+ items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
+
+ // TR-001 has hand-crafted news
+ if items[0].Headline != "Critical Deadline" {
+ t.Errorf("expected hand-crafted headline, got %q", items[0].Headline)
+ }
+ if items[0].ActionLink != "/sdk/test" {
+ t.Errorf("expected /sdk/test, got %q", items[0].ActionLink)
+ }
+}
+
+func TestGetRegulatoryNews_AutoGeneratesWithoutNews(t *testing.T) {
+ regs := makeTestRegulations()
+ items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
+
+ // TR-002 has no news field โ should auto-generate
+ if items[1].Headline == "" {
+ t.Error("expected auto-generated headline")
+ }
+ if items[1].Summary == "" {
+ t.Error("expected auto-generated summary from description")
+ }
+}
+
+func TestGetRegulatoryNews_ExcludesPastDeadlines(t *testing.T) {
+ regs := makeTestRegulations()
+ items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 10})
+
+ for _, item := range items {
+ if item.ID == "TR-003" {
+ t.Error("past deadline should be excluded")
+ }
+ if item.ID == "TR-004" {
+ t.Error("no-deadline obligation should be excluded")
+ }
+ }
+}
+
+func TestGetRegulatoryNews_LimitWorks(t *testing.T) {
+ regs := makeTestRegulations()
+ items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 1})
+
+ if len(items) != 1 {
+ t.Errorf("expected 1 item with limit=1, got %d", len(items))
+ }
+}
+
+func TestComputeUrgency(t *testing.T) {
+ tests := []struct {
+ days int
+ expected string
+ }{
+ {5, "critical"},
+ {30, "critical"},
+ {31, "high"},
+ {90, "high"},
+ {91, "medium"},
+ {180, "medium"},
+ {181, "low"},
+ {365, "low"},
+ }
+ for _, tc := range tests {
+ got := computeUrgency(tc.days)
+ if got != tc.expected {
+ t.Errorf("computeUrgency(%d) = %q, want %q", tc.days, got, tc.expected)
+ }
+ }
+}
+
+func TestResolveDeadline_DeadlineDate(t *testing.T) {
+ obl := V2Obligation{Deadline: &V2Deadline{Date: "2026-06-19"}}
+ d, ok := resolveDeadline(obl)
+ if !ok {
+ t.Fatal("expected deadline resolved")
+ }
+ if d.Format("2006-01-02") != "2026-06-19" {
+ t.Errorf("got %s", d.Format("2006-01-02"))
+ }
+}
+
+func TestResolveDeadline_ValidFrom(t *testing.T) {
+ obl := V2Obligation{ValidFrom: "2026-08-02"}
+ d, ok := resolveDeadline(obl)
+ if !ok {
+ t.Fatal("expected deadline resolved from valid_from")
+ }
+ if d.Format("2006-01-02") != "2026-08-02" {
+ t.Errorf("got %s", d.Format("2006-01-02"))
+ }
+}
+
+func TestResolveDeadline_NoDate(t *testing.T) {
+ obl := V2Obligation{}
+ _, ok := resolveDeadline(obl)
+ if ok {
+ t.Error("expected no deadline for empty obligation")
+ }
+}
+
+func TestFormatLegalReference(t *testing.T) {
+ bases := []V2LegalBasis{
+ {Norm: "DSGVO", Article: "Art. 22"},
+ {Norm: "BGB", Article: "ยง 356a"},
+ }
+ ref := formatLegalReference(bases)
+ if ref != "Art. 22 DSGVO, ยง 356a BGB" {
+ t.Errorf("got %q", ref)
+ }
+}
+
+func TestGetRegulatoryNews_FromRealFiles(t *testing.T) {
+ regs, err := LoadAllV2Regulations()
+ if err != nil {
+ t.Skipf("could not load v2 regulations: %v", err)
+ }
+ items := GetRegulatoryNews(regs, RegulatoryNewsFilter{Limit: 20, HorizonDays: 730})
+ // Should find at least the Widerrufsbutton obligation
+ found := false
+ for _, item := range items {
+ if item.ID == "VBR-OBL-001" {
+ found = true
+ if item.Headline != "Widerrufsbutton-Pflicht ab 19. Juni 2026" {
+ t.Errorf("unexpected headline: %q", item.Headline)
+ }
+ }
+ }
+ if !found {
+ t.Error("expected VBR-OBL-001 in regulatory news")
+ }
+}
diff --git a/ai-compliance-sdk/internal/ucca/v2_loader.go b/ai-compliance-sdk/internal/ucca/v2_loader.go
index e31682b..69557c1 100644
--- a/ai-compliance-sdk/internal/ucca/v2_loader.go
+++ b/ai-compliance-sdk/internal/ucca/v2_loader.go
@@ -58,6 +58,17 @@ type V2Obligation struct {
Version string `json:"version,omitempty"`
ISO27001Mapping []string `json:"iso27001_mapping,omitempty"`
HowToImplement string `json:"how_to_implement,omitempty"`
+ News *V2ObligationNews `json:"news,omitempty"`
+}
+
+// V2ObligationNews is news metadata for dashboard display.
+// Own text referencing legal basis โ never copied from external sources.
+type V2ObligationNews struct {
+ Headline string `json:"headline"`
+ Summary string `json:"summary"`
+ ActionRequired string `json:"action_required"`
+ Affected string `json:"affected,omitempty"`
+ ActionLink string `json:"action_link,omitempty"`
}
// V2LegalBasis is a legal reference in v2 format
diff --git a/ai-compliance-sdk/policies/obligations/v2/verbraucherrecht_v2.json b/ai-compliance-sdk/policies/obligations/v2/verbraucherrecht_v2.json
index afdcba4..a57bdd0 100644
--- a/ai-compliance-sdk/policies/obligations/v2/verbraucherrecht_v2.json
+++ b/ai-compliance-sdk/policies/obligations/v2/verbraucherrecht_v2.json
@@ -90,6 +90,13 @@
"priority": "critical",
"tom_control_ids": [],
"breakpilot_feature": "/sdk/consent-management",
+ "news": {
+ "headline": "Widerrufsbutton-Pflicht ab 19. Juni 2026",
+ "summary": "Ab dem 19. Juni 2026 muessen Unternehmen mit B2C-Online-Geschaeft einen digitalen Widerrufsbutton auf ihrer Website bereitstellen. Der Button muss von jeder Seite erreichbar sein und ein Zwei-Klick-Verfahren implementieren (EU-RL 2023/2673, ยง 356a BGB).",
+ "action_required": "Implementieren Sie einen gut sichtbaren 'Vertrag widerrufen'-Button auf Ihrer Website. Dokumentieren Sie das Zwei-Klick-Verfahren und stellen Sie die automatische Bestaetigungs-E-Mail sicher.",
+ "affected": "B2C, Online-Shop, Abo-Modelle",
+ "action_link": "/sdk/consent-management"
+ },
"valid_from": "2026-06-19",
"valid_until": null,
"version": "1.0"