From 64e3a47b8ce67afb63ff6a15757726280f9fcc35 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 13 May 2026 15:19:39 +0200 Subject: [PATCH] fix(iace): confirmation dialog for ungrouping + undo/regroup - X button replaced with confirmation dialog: "Als eigenen Punkt fuehren" / "Abbrechen" - Dialog explains the action and that it's reversible - Ungrouped items show orange "Zurueck in Block" button - Info bar shows count of ungrouped items + "alle zuruecksetzen" link - No destructive action without user confirmation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../_components/BlockAwareRiskTable.tsx | 69 ++++++++++++++++--- consent-tester/services/banner_detector.py | 33 +++++++-- consent-tester/services/dsi_helpers.py | 36 ++++++---- 3 files changed, 110 insertions(+), 28 deletions(-) diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/BlockAwareRiskTable.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/BlockAwareRiskTable.tsx index 43d52df..f05cfc9 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/BlockAwareRiskTable.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/BlockAwareRiskTable.tsx @@ -37,6 +37,7 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions, const [blocks, setBlocks] = useState([]) const [collapsed, setCollapsed] = useState>({}) const [ungrouped, setUngrouped] = useState>({}) + const [pendingAction, setPendingAction] = useState<{ childId: string; childName: string } | null>(null) useEffect(() => { fetch(`/api/sdk/v1/iace/projects/${projectId}/hazard-blocks`) @@ -110,14 +111,50 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions, const handleUngroup = (childId: string) => { setUngrouped(prev => ({ ...prev, [childId]: true })) + setPendingAction(null) + } + + const handleRegroup = (childId: string) => { + setUngrouped(prev => { + const next = { ...prev } + delete next[childId] + return next + }) } // Count blocks with children const blockCount = blocks.filter(b => b.children.length > 0).length const coveredCount = Object.values(blockMap).filter(b => b.isChild && b.isCovered).length + const ungroupedCount = Object.keys(ungrouped).length return (
+ {/* Confirmation dialog */} + {pendingAction && ( +
+
+

Gefaehrdung aus Block entfernen?

+

+ {pendingAction.childName} +

+

+ Der Punkt wird als eigenstaendige Gefaehrdung gefuehrt und muss separat bewertet werden. + Sie koennen ihn jederzeit ueber "Zurueck in Block" wieder zuordnen. +

+
+ + +
+
+
+ )} + {/* Block info bar */} {blockCount > 0 && (
@@ -129,9 +166,12 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions, {coveredCount} Kinder durch Mutter abgedeckt )} - - Klick auf Block-Icon zum Auf-/Zuklappen. Rechtsklick oder Aktion zum Entgruppieren. - + {ungroupedCount > 0 && ( + + )}
)} @@ -173,17 +213,26 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions, )} {isChild && ( -
-
-
)} + {/* Show regroup button for ungrouped items */} + {!isParent && !isChild && ungrouped[h.id] && ( + + )} {/* Name */} diff --git a/consent-tester/services/banner_detector.py b/consent-tester/services/banner_detector.py index 2f6ba5c..4369f82 100644 --- a/consent-tester/services/banner_detector.py +++ b/consent-tester/services/banner_detector.py @@ -467,15 +467,36 @@ async def click_button(page: Page, selector: str, timeout: int = 5000) -> bool: text_pattern = selector[len("shadow-click:"):] return await _click_in_shadow_dom(page, text_pattern) + # 1. Try main document try: locator = page.locator(selector).first await locator.wait_for(state="visible", timeout=timeout) await locator.click() return True except Exception: - # Fallback: try Shadow DOM click with selector text - # Extract button text from selector like 'button:has-text("Accept all")' - if ':has-text("' in selector: - text = selector.split(':has-text("')[1].rstrip('")') - return await _click_in_shadow_dom(page, text) - return False + pass + + # 2. Fallback: try inside iframes (Sourcepoint, Quantcast, etc.) + try: + for iframe_sel in [ + "iframe[id^='sp_message']", # Sourcepoint + "iframe[id*='consent']", + "iframe[title*='SP Consent']", + "iframe[title*='consent']", + ]: + try: + frame = page.frame_locator(iframe_sel) + btn = frame.locator(selector).first + if await btn.count() > 0: + await btn.click(timeout=timeout) + return True + except Exception: + continue + except Exception: + pass + + # 3. Fallback: Shadow DOM + if ':has-text("' in selector: + text = selector.split(':has-text("')[1].rstrip('")') + return await _click_in_shadow_dom(page, text) + return False diff --git a/consent-tester/services/dsi_helpers.py b/consent-tester/services/dsi_helpers.py index bd80958..9065db9 100644 --- a/consent-tester/services/dsi_helpers.py +++ b/consent-tester/services/dsi_helpers.py @@ -81,20 +81,32 @@ async def try_dismiss_consent_banner(page: Page) -> bool: except Exception: continue - # 3) Sourcepoint (iframe-based CMP, used by Spiegel, Zeit, etc.) + # 3) Sourcepoint / iframe-based CMPs (Spiegel, Zeit, etc.) + # Search ALL iframes for consent buttons — Sourcepoint generates dynamic IDs try: - sp_div = await page.query_selector("div[id^='sp_message']") - if sp_div: - # Sourcepoint renders in an iframe inside sp_message_container - sp_iframe = page.frame_locator("iframe[id^='sp_message']") - accept_btn = sp_iframe.locator(".sp_choice_type_11").first - if await accept_btn.count() > 0: - await accept_btn.click(timeout=5000) - logger.info("Dismissed Sourcepoint consent banner (iframe)") - await page.wait_for_timeout(3000) - return True + for frame in page.frames: + if frame == page.main_frame: + continue + try: + # Sourcepoint accept button + sp_btn = frame.locator(".sp_choice_type_11").first + if await sp_btn.count() > 0 and await sp_btn.is_visible(): + await sp_btn.click(timeout=5000) + logger.info("Dismissed Sourcepoint consent (iframe: %s)", frame.url[:80]) + await page.wait_for_timeout(3000) + return True + # Generic accept text in iframe + for text in ["Akzeptieren", "Zustimmen", "Accept all", "Alle akzeptieren"]: + btn = frame.locator(f'button:has-text("{text}")').first + if await btn.count() > 0 and await btn.is_visible(): + await btn.click(timeout=3000) + logger.info("Dismissed iframe consent via '%s'", text) + await page.wait_for_timeout(3000) + return True + except Exception: + continue except Exception as e: - logger.debug("Sourcepoint dismiss attempt: %s", e) + logger.debug("Iframe consent dismiss: %s", e) # 4) Use banner_detector CMP selectors as fallback try: