fix(iace): confirmation dialog for ungrouping + undo/regroup
Build + Deploy / build-admin-compliance (push) Successful in 1m53s
Build + Deploy / build-backend-compliance (push) Successful in 10s
Build + Deploy / build-ai-sdk (push) Successful in 9s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 12s
Build + Deploy / build-document-crawler (push) Successful in 10s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 15s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m40s
CI / dep-audit (push) Has been skipped
Build + Deploy / build-dsms-node (push) Successful in 13s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 44s
CI / test-python-backend (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 14s
Build + Deploy / trigger-orca (push) Successful in 2m29s

- 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) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-13 15:19:39 +02:00
parent 81a0568537
commit 64e3a47b8c
3 changed files with 110 additions and 28 deletions
@@ -37,6 +37,7 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions,
const [blocks, setBlocks] = useState<BlockData[]>([])
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
const [ungrouped, setUngrouped] = useState<Record<string, boolean>>({})
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 (
<div className="space-y-2">
{/* Confirmation dialog */}
{pendingAction && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 p-5 max-w-md w-full mx-4">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Gefaehrdung aus Block entfernen?</h3>
<p className="text-xs text-gray-600 dark:text-gray-400 mb-1">
<strong>{pendingAction.childName}</strong>
</p>
<p className="text-xs text-gray-500 mb-4">
Der Punkt wird als eigenstaendige Gefaehrdung gefuehrt und muss separat bewertet werden.
Sie koennen ihn jederzeit ueber &quot;Zurueck in Block&quot; wieder zuordnen.
</p>
<div className="flex gap-2">
<button onClick={() => handleUngroup(pendingAction.childId)}
className="flex-1 px-3 py-2 text-xs font-medium bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
Als eigenen Punkt fuehren
</button>
<button onClick={() => setPendingAction(null)}
className="flex-1 px-3 py-2 text-xs font-medium bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 transition-colors">
Abbrechen
</button>
</div>
</div>
</div>
)}
{/* Block info bar */}
{blockCount > 0 && (
<div className="flex items-center gap-4 px-4 py-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-xs">
@@ -129,9 +166,12 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions,
{coveredCount} Kinder durch Mutter abgedeckt
</span>
)}
<span className="text-gray-500">
Klick auf Block-Icon zum Auf-/Zuklappen. Rechtsklick oder Aktion zum Entgruppieren.
</span>
{ungroupedCount > 0 && (
<button onClick={() => setUngrouped({})}
className="text-orange-600 hover:text-orange-700 underline">
{ungroupedCount} entgruppiert alle zuruecksetzen
</button>
)}
</div>
)}
@@ -173,17 +213,26 @@ export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions,
</button>
)}
{isChild && (
<div className="flex items-center justify-center gap-0.5">
<div className="w-1 h-1 rounded-full bg-gray-300" />
<button onClick={() => handleUngroup(h.id)}
className="w-4 h-4 flex items-center justify-center rounded hover:bg-red-100 text-gray-400 hover:text-red-500 transition-colors"
title="Aus Block entfernen">
<svg className="w-2.5 h-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
<div className="flex items-center justify-center">
<button onClick={() => setPendingAction({ childId: h.id, childName: h.name })}
className="w-5 h-5 flex items-center justify-center rounded hover:bg-orange-100 text-gray-300 hover:text-orange-500 transition-colors"
title="Aus Block entfernen (mit Bestaetigung)">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
</svg>
</button>
</div>
)}
{/* Show regroup button for ungrouped items */}
{!isParent && !isChild && ungrouped[h.id] && (
<button onClick={() => handleRegroup(h.id)}
className="w-5 h-5 flex items-center justify-center rounded hover:bg-green-100 text-orange-400 hover:text-green-600 transition-colors"
title="Zurueck in Block">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
</button>
)}
</td>
{/* Name */}
<td className={`px-3 py-2 ${isChild ? 'pl-8' : ''}`}>
+27 -6
View File
@@ -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
+24 -12
View File
@@ -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: