Compare commits

..

59 Commits

Author SHA1 Message Date
Benjamin Admin bf9d8a5ed3 fix(iace): resolve M-ID collisions for electrical/pressure patterns
6 supplementary measures (M410-M420) were silently overwritten by
metalworking duplicates in measureByID lookups, so robot-cell electrical
patterns resolved to chip-extraction/cleaning fallbacks instead of
equipotential bonding, creepage, EMC, or hose-burst protection. Rename
supplementary IDs to M475-M480 and rewire 13 affected pattern references
in robot_cell + robot_cell_ext.

HP1640 (direct contact with live parts, GT 2.2): priority 98->99, drop
RequiredEnergyTags gate so it fires in robot cells without an electrical
tag, expand mitigations to 5 concrete TRBS 2131 / IEC 60204-1 / EN 61140
measures (basic protection, double insulation, earthing, insulation
monitoring, equipotential bonding) — was previously losing to HP1688
even though HP1688 describes a different scenario.

HP1688 (touch voltage from potential differences): priority 98->96 so it
no longer outranks HP1640 for the direct-contact case; mitigations
expanded from M410-only to 4 concrete electrical measures.

Add regression tests pinning HP1640 contact-protection resolution and
M475 = Potentialausgleich. Existing TestGetProtectiveMeasureLibrary_-
UniqueIDs now actually enforces uniqueness (previously masked by
last-wins map override).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:12:55 +02:00
Benjamin Admin d45e08e25f fix: reduce Playwright timeout 180s→60s, increase poll limit 15→25min 2026-05-16 00:47:28 +02:00
Benjamin Admin 3dbf3aa34a feat: HTTP fallback for text extraction when Playwright times out
BMW Impressum/Cookie pages timeout in Playwright (>180s) because the
SPA has many sub-links to follow. But the HTML source already contains
the text (SSR). New fallback: direct HTTP GET + HTML tag stripping.

Order: 1. Consent-tester (Playwright, 180s) → 2. HTTP GET (30s)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 23:16:10 +02:00
Benjamin Admin 77308b783f debug: log CreateMitigation errors 2026-05-15 21:52:04 +02:00
Benjamin Admin 9797234ff6 fix(iace): add abbreviations + action words to genericSafetyTerms
KSS, EMV, ESD, DCS, PLR, SIL, HMI, SPS, RCD, LOTO, PSA are
abbreviations that should NOT trigger the relevance filter.
bersten, platzen, abspringen, spritzen, einatmen, ausrutschen,
herabfallen, durchschlaegen, wegschleudern are action words that
appear in many patterns and don't indicate a specific machine.

Fixes: HP1633-HP1675 (KSS patterns) were filtered out because
"kss" was not in the narrative but also not in genericSafetyTerms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 16:05:20 +02:00
Benjamin Admin 7080eb5f45 fix(iace): boost robot cell priorities 96-99, remove debug code
Robot cell patterns now fire BEFORE generic patterns (Priority 96-99
vs generic 85-95). This ensures pattern-specific SuggestedMeasureIDs
(M420 for KSS, M410 for Potentialausgleich) reach the hazard.

Removed debug fmt.Println statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 16:01:52 +02:00
Benjamin Admin c93cf2719a debug: trace M420 in Priority-1 loop 2026-05-15 14:56:05 +02:00
Benjamin Admin 7a27dbc01b debug: check M420 in measureByID 2026-05-15 14:53:49 +02:00
Benjamin Admin de35dfce18 debug: add pattern-measure count to init step details 2026-05-15 14:51:26 +02:00
Benjamin Admin 69240faf24 fix(iace): accumulate SuggestedMeasureIDs across dedup'd patterns
When multiple patterns match the same category+zone, the first creates
the hazard and later patterns add their SuggestedMeasureIDs to the
existing hazard. This ensures KSS-specific measures (M420) reach the
hazard even if a generic pattern created it first.

seenCatZone changed from map[string]bool to map[string]uuid.UUID
to track which hazard ID was created for each dedupKey.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 14:45:37 +02:00
Benjamin Admin f34305c0a1 fix: increase dsi-discovery timeout 90s→300s, reduce max_documents 10→5 2026-05-15 14:21:13 +02:00
Benjamin Admin 2b5376ed54 fix(iace): pattern-specific measures take priority over category fallback
Each hazard now gets measures from its SOURCE PATTERN first
(SuggestedMeasureIDs), then category fallback for remaining slots.

Previously all mechanical hazards got the same generic top-5 measures
(Gefahrstelle eliminieren, Sicherheitsabstaende, Scharfe Kanten...).
Now a KSS-Schlauch hazard gets M420 (Druckfeste Auslegung) first.

SuggestedMeasureIDs added to PatternMatch struct and passed through
from pattern definition to hazard creation to measure assignment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 14:17:32 +02:00
Benjamin Admin 958c03ab40 fix(iace): add human reference to all 33 robot cell patterns
Every ScenarioDE now describes how a PERSON is affected, not just
what happens to the machine. Every HarmDE describes the INJURY,
not just the technical effect.

Examples:
- "Peitscheneffekt des Schlauchs" → "Person wird von abspringendem
  Schlauch getroffen. KSS-Spritzer verletzen Haut und Augen."
- "Kurzschluss, Brand" → "Person wird durch Brand oder toxische
  Rauchgase verletzt. Verbrennungen, Rauchvergiftung."

Rule: Risikobeurteilung bewertet Gefahr fuer PERSONEN.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 13:43:54 +02:00
Benjamin Admin fca67c1f43 fix: accordion close bug + merge multi-page DSIs (BMW fix)
1. _expand_all_interactive(): Only click aria-expanded="false" buttons.
   Before: clicked ALL accordion buttons including open ones → BMW's
   pre-expanded accordions got CLOSED, reducing text from 1151 to 361w.

2. _fetch_text() + /extract-text: merge ALL documents found on a page
   (max_documents=10 instead of 1). BMW splits DSI across 5 sub-pages
   that the discovery finds as separate documents — now merged.

3. Tab panels: unhide hidden tabpanels instead of clicking tabs
   (clicking tabs can hide the currently visible panel).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 13:32:04 +02:00
Benjamin Admin 70af018da5 docs(gt): BMW cross-domain finding — 3 domains, no AGB, Social Media on jobs portal 2026-05-15 13:21:27 +02:00
Benjamin Admin 0182c91ef9 docs(gt): BMW fully verified — URLs, DSB, Impressum, Social Media data 2026-05-15 12:01:20 +02:00
Benjamin Admin a67cfa7c4a fix(gt): update BMW URLs (all old URLs are 404 since 2026) 2026-05-15 10:38:07 +02:00
Benjamin Admin 3b7ab4cbd7 feat(iace): 50% display threshold — weak matches shown as separate
Matches below 50% are now split:
- GT entries → "Fehlend" tab (not matched by engine)
- Engine entries → "Engine Findings" tab (additional findings)
Only matches >= 50% shown in "Zugeordnet" tab.

Coverage score now counts only real matches (>= 50%).
"Extra" tab renamed to "Engine Findings" for clarity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:33:29 +02:00
Benjamin Admin 3469105d18 feat(iace): HP1606 + HP1634 — target 100% GT coverage
HP1606: Quetschen/Scheren durch Greifer im Einrichtbetrieb (GT 1.14)
HP1634: KSS-Pumpe spritzt bei geoeffneter Schutztuer (GT 1.38)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:20:42 +02:00
Benjamin Admin 1414c63515 feat(iace): HP1605 + HP1633 — final 2 patterns for GT coverage
HP1605: Stoss durch Werkzeug/Greifer im Einrichtbetrieb (GT 1.14)
HP1633: KSS-Versorgungsschlauch platzt oder reisst ab (GT 1.35)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:16:39 +02:00
Benjamin Admin 9f87bc5a2c fix: include website/company name in compliance-check email subject 2026-05-15 10:15:34 +02:00
Benjamin Admin f5f4de7359 fix(iace): remove RequiredEnergyTags from electrical patterns
Energy tag "electrical" doesn't match resolved tags (which are
"high_voltage", "electrical_part", etc.). Patterns HP1685-HP1699
now fire without energy tag requirement — they fire for any
project that has the right component tags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:13:00 +02:00
Benjamin Admin 38d15d4d29 feat(iace): 5 differentiated patterns for GT duplicate scenarios
When GT has two entries for the same zone with different scenarios
(e.g. "eingeklemmt" vs "getroffen"), we need separate engine patterns.

HP1700: Getroffen von bewegtem Werkzeug/Greifer (vs HP1652 eingeklemmt)
HP1701: Greifer/Werkzeug durchschlaegt Zaun (vs HP1654 Werkstueck)
HP1702: KSS-Schlauch platzt (vs HP1675 springt ab)
HP1703: KSS-Bettspuelung bei offener Tuer (vs HP1670 allgemein)
HP1704: Brand durch KSS auf elektrische Komponenten

Extended synonym sets for potential/EMV matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:08:21 +02:00
Benjamin Admin 003eafa75d fix(iace): synonym-cross-matching + expanded action words
scenarioSimilarity now uses synonym-set cross-matching: if GT says
"durchschlaegt" and Engine says "schleuder", the synonym set recognizes
them as related. Added significantWordOverlap fallback when no action
words found. Extended action terms: schlauch/druck/kuehlschmierstoff,
pumpe/bettspuel, potential/bezugspotential, stoerung/emv.

Moved extractActionWords to benchmark_synonyms.go (458+119 lines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 10:03:23 +02:00
Benjamin Admin b82853a95b feat(iace): scenario-based matching + split benchmark_synonyms.go
4-signal matcher: category (0.2), keywords (0.2), zone (0.3),
scenario similarity (0.3). Scenario signal extracts action words
(eingeklemmt vs herabfallend vs durchschlaegt) to differentiate
similar-looking hazards at the same component.

Split benchmark_synonyms.go (70 lines) from benchmark_matcher.go
(516→450 lines) to stay under 500-line cap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:58:12 +02:00
Benjamin Admin c060ac222a fix(iace): prioritize zone-specific matches in greedy assignment
Sort matches by specificity first (zone overlap), then by score.
Prevents generic matches from consuming specific Engine patterns
that should match more specific GT entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:45:08 +02:00
Benjamin Admin 659c0505f8 fix: format code in batch test output 2026-05-15 09:44:48 +02:00
Benjamin Admin 02c2325e1b feat(iace): 2 final patterns (Kriechstrecken, EMV) + matcher synonyms
HP1698: Kurzschluss durch unzureichende Luft-/Kriechstrecken (GT 2.6)
HP1699: EMV-Stoereinfluss auf Sicherheitsfunktionen (GT 6.1)

Extended synonym sets: durchschlag/bewegungsbereich, potentialausgleich,
kriechstreck, kuehlschmierstoff/bettspuel, rutsch/stolper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:42:14 +02:00
Benjamin Admin d72aa10691 feat: management summary for GF + batch GT test script
1. Management Summary (agent_doc_check_report.py):
   - Plain-language action items for Geschaeftsfuehrer
   - Maps technical checks to business actions ("Ihren DSB erwaehnen",
     "Beschwerderecht ergaenzen", "Loeschfristen dokumentieren")
   - Shows at top of compliance check email before detail report
   - Max 10 actions, max 3 per document

2. Batch GT Test (zeroclaw/scripts/batch_gt_test.py):
   - Runs all 10 GT websites through compliance-check API
   - Prints comparison table with L1 scores, word counts, services
   - Saves raw JSON results for analysis
   - Usage: python3 batch_gt_test.py --sites 1,6 --backend-url URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:39:19 +02:00
Benjamin Admin 3c05ff8ef6 fix(iace): lower threshold 0.20 + more synonym sets for GT matching
Threshold 0.25→0.20 to recover matches lost by keyword penalty.
New synonym sets: eingeschlossen/wiederanlauf, zentriergreifer,
beladetuer/schutztuer, ergonom/bedienelemente, spritzer/auge, bersten.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:31:12 +02:00
Benjamin Admin 935c9205b9 feat(iace): 25 new robot cell patterns (HP1650-HP1697) + matcher fix
New patterns from GT benchmark gap analysis:
- HP1650-1655: Robot arm motion limit, restart safety, tool/workpiece
  crushing, workpiece penetrates fence, reaching over fence
- HP1660-1661: Centering gripper crushing (outside/inside cell)
- HP1665-1666: Machine tool loading door, machining workspace
- HP1670-1671: Coolant splash eyes, compressed air injury
- HP1675: Coolant hose burst/detachment
- HP1680: Workpiece/tunnel crushing at conveyor
- HP1685-1689: Indirect contact, cabinet contact, liquid ingress fire,
  potential differences, RCD socket protection
- HP1690-1691: Ergonomic loading/control position
- HP1695: Burns from hot workpieces
- HP1697: Machine collapse through floor

Matcher: keyword overlap penalty — matches without shared hazard-type
keywords AND low zone score get 0.5x penalty to prevent false matches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 09:28:01 +02:00
Benjamin Admin 826ce2a1b8 fix(cross-doc): suppress false positives when regex checks already pass
Cross-search "not in text" findings are only shown when regex L1
completeness < 50%. This prevents false positives where the text IS
the right doc_type but doesn't contain the specific cross-search
keywords (e.g. Impressum passes 9/13 checks but lacks "§5 TMG").

Also: cross-search now checks entries with wrong text, not just empty.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:54:33 +02:00
Benjamin Admin bd2d6976d6 fix(cross-doc): also check entries with wrong text, not just empty ones
Cross-search now validates if existing text matches the expected
doc_type using keyword scoring. If text is present but doesn't match
(e.g. Nutzungsbedingungen in Widerruf row), searches other texts
and creates a finding explaining the mismatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:19:40 +02:00
Benjamin Admin a5d1814605 fix(iace): tag remaining 3 wrong-machine patterns + fix duplicates
HP154 (Kollision zweier Roboter) → robotics_cobot only
HP1066 (Haareinzug Drehmaschine) → lathe/cnc/metalworking only
HP758 (Notbremsung Fahrtreppe) → escalator/elevator only
Fixed duplicate MachineTypes fields from overlapping script runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:05:28 +02:00
Benjamin Admin ba07a7f6e6 fix(iace): add MachineTypes to 17 machine-specific patterns
Patterns for playground, escalator, wind turbine, glass washing,
laundry, crane, lathe, rotary transfer, press now require matching
MachineTypes — they no longer fire for unrelated projects.
Neutralized zone texts in base patterns HP006/HP008 (removed
"Pressenraum", "Kran-/Hebezeugbereich").

Fixes: Spielplatz, Fahrtreppe, Windturbine etc. appearing in robot cell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 00:01:51 +02:00
Benjamin Admin 708c61e50d fix(iace): max 5 mitigations per hazard — clean per-hazard assignment
Replaced category-broadcast logic with per-hazard loop:
each hazard gets up to 5 measures (pattern-suggested first, then
category fallback). Expected: 108 × 5 = max 540 total.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:45:41 +02:00
Benjamin Admin dc55253b9d fix(iace): prevent mitigation explosion — fallback only for unassigned
Pattern-suggested measures go to all hazards in category (correct).
Category-based fallback only for hazards WITHOUT pattern suggestions
(max 3 per hazard). Prevents 1654 mitigations explosion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:41:54 +02:00
Benjamin Admin 8069d0ea89 fix(iace): assign mitigations to ALL hazards per category
hazardIDsByCategory changed from map[string]uuid.UUID to
map[string][]uuid.UUID — measures are now distributed to every
hazard in a category, not just the last one created.

Previously 94/108 hazards had no measures, now all get them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:34:57 +02:00
Benjamin Admin 4e9043f26d feat(cross-doc): search all texts for all doc_types + misplacement finding
Cross-Document Intelligence: When a doc_type row is empty, searches
ALL other loaded documents for that content. If found (e.g. Widerruf
in AGB), extracts the section, runs the check, AND creates a finding:
"Widerrufsbelehrung in falschem Dokument gefunden — schwer auffindbar"

Keywords for: widerruf, cookie, social_media, impressum, agb, dsb.
Integrated as Step 1c in compliance check pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:19:39 +02:00
Benjamin Admin 29fbd03c79 fix(iace): lifecycle labels in benchmark + store all phases
- Store ALL applicable lifecycles (comma-separated) not just first
- Frontend maps internal keys to German labels (normal_operation ->
  Automatikbetrieb, maintenance -> Wartung, etc.)
- Show Betroffene Personen in engine detail column

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:17:27 +02:00
Benjamin Admin 98e5b1a8aa feat(iace): show lifecycle phases + affected persons in benchmark detail
Backend: HazardSummary now includes lifecycle_phase and affected_person
Frontend: Engine detail column shows Lebensphasen and Betroffene Personen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:10:15 +02:00
Benjamin Admin b175212516 docs(gt): update Spiegel GT with verified 2026-05-14 results
CI / detect-changes (push) Successful in 5m10s
CI / nodejs-build (push) Successful in 2m15s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 5m1s
CI / loc-budget (push) Successful in 17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Failing after 46s
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
DSI: 9/9 L1 (was 6/9), 13698 words (was 6461), all FNs resolved.
Social Media: 10/10 L1 (was 9/10). Services: 31 detected (was 5).
Impressum: 9/13 (USt-IdNr + V.i.S.d.P. fixed).
Widerruf: NOT correctly tested (wrong text assigned, needs Cross-Doc Intelligence).

Full service list (31 providers) documented with country + EU status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:07:42 +02:00
Benjamin Admin 16190583d1 refactor(iace): neutral hazard formulations across all 1100+ patterns
Systematic refactoring of all hazard_patterns_*.go files:
- Removed lifecycle phase words from NameDE and ScenarioDE
  (67 fixes across 20 files)
- Phases belong in ApplicableLifecycles, not in text
- "bei Wartung/Reinigung/Montage/..." removed from names
- Scenarios rewritten to be phase-neutral
- Lifecycle-specific concepts preserved when they define the hazard
  (e.g. LOTO, Betriebsartenwahlschalter)

Rule: Gefaehrdung + Szenario NEUTRAL, Lebensphasen SEPARAT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 23:04:31 +02:00
Benjamin Admin 70c9bfc069 fix(iace): neutral hazard formulations — no lifecycle phases in text
- Removed HP1601 (duplicate of HP1600 with narrower scope)
- HP1600 now covers ALL lifecycle phases, not just teach mode
- All pattern texts neutral: no lifecycle phase references in
  NameDE, ScenarioDE, TriggerDE — phases only in ApplicableLifecycles
- Formulierungsregel documented in file header

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 22:52:56 +02:00
Benjamin Admin 4b9317b4fd feat(iace): lifecycle phases in patterns + broader robot cell scenarios
- ApplicableLifecycles field in HazardPattern: patterns now declare which
  lifecycle phases the hazard applies to (Output, not just filter)
- Init handler writes first applicable lifecycle into Hazard.LifecyclePhase
- Robot cell patterns HP1600-1601 broadened: "Betrieb, Einrichten, Reinigung,
  Wartung, Fehlersuche" instead of only "Teach-Betrieb"
- All robot cell patterns get ApplicableLifecycles for proper phase display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 22:38:02 +02:00
Benjamin Admin e4431da8d2 Merge branch 'main' of ssh://gitea.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-compliance
CI / detect-changes (push) Successful in 5m10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 5m3s
CI / go-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 7m16s
CI / loc-budget (push) Successful in 14s
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
2026-05-14 18:47:56 +02:00
Benjamin Admin 65f978368d feat(cmp): Phase 3 — admin widerruf, email-linking, vendor display, TCF, E2E tests
Admin Modal:
- vendor_consents as green/red badges
- Consent withdraw button (DELETE /consent/{id}) with confirmation
- Email-linking inline input (POST /consent/link-email)

Cookie Banner Admin:
- TCF toggle reads tcf_enabled from site config (was hardcoded false)
- BannerSite interface extended with tcf_enabled

Document Generator:
- Backend Banner-Config auto-fetch when SDK state has no banner
- Maps vendors to CONSENT (analytics tools, marketing partners)

E2E Tests (cmp-phase3-dsr.spec.ts):
- Vendor-agnostic consent fields (20+ fields, upsert)
- DSR Art. 15 Auskunft (multi-device, email-link, export)
- DSR Art. 17 Löschung (erasure by email)
- Anonymous cookie banner user (export, withdraw)
- Customer lifecycle (consent → login → link → Art.15 → Art.17)
- Admin dashboard integration (list, stats)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 18:45:41 +02:00
Sharang Parnerkar a530edb994 Merge branch 'main' of ssh://coolify.meghsakha.com:22222/Benjamin_Boenisch/breakpilot-compliance
CI / detect-changes (push) Successful in 12s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 15s
CI / loc-budget (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) Has been skipped
CI / test-go (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
# Conflicts:
#	.claude/rules/loc-exceptions.txt
2026-05-13 17:37:59 +02:00
Sharang Parnerkar 256deb70c7 ci: gate jobs on change detection + tag-based deploy ordering [guardrail-change]
Build + Deploy ran in parallel with CI's lint/test/loc, so a deploy could ship
even when CI failed. Gate Build + Deploy on CI success via workflow_run, and
add per-service change detection so only affected services rebuild and only
relevant lint/test jobs run on PRs.

- scripts/detect-changes.sh: shared diff helper that emits per-service +
  aggregate flags from a BASE_SHA diff; falls back to "rebuild all" when the
  base is missing or unreachable
- ci.yaml: detect-changes job runs first; loc-budget, *-lint, *-build, and
  test-* jobs gate on the relevant outputs
- build-push-deploy.yml: triggered via workflow_run on CI completion; diff
  base is the last-build/main git tag, force-pushed by a new mark-last-build
  job after each green run (handles multi-commit pushes, force pushes, and
  the "all skipped" case)
- check-loc.sh: exclude Office/binary extensions (xlsm, docx, pptx, zip,
  tar, gz) so binary docs aren't counted as source
- loc-exceptions.txt: grandfather two existing >500 LOC files
  (tender_handlers.go, DecisionTreeWizard.tsx) as Phase 5+ backlog

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:39:43 +02:00
Benjamin Admin eac42d4154 feat(iace): robot cell hazard patterns HP1600-HP1649 + engine split
Build + Deploy / build-admin-compliance (push) Successful in 1m59s
Build + Deploy / build-backend-compliance (push) Successful in 3m19s
Build + Deploy / build-ai-sdk (push) Successful in 52s
Build + Deploy / build-developer-portal (push) Successful in 1m11s
Build + Deploy / build-tts (push) Successful in 1m32s
Build + Deploy / build-document-crawler (push) Successful in 40s
Build + Deploy / build-dsms-gateway (push) Successful in 25s
Build + Deploy / build-dsms-node (push) Successful in 15s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
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 2m43s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 51s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 21s
CI / validate-canonical-controls (push) Successful in 13s
Build + Deploy / trigger-orca (push) Successful in 3m25s
20 new patterns for robot cells (ISO 10218-2): arm crushing, teach mode,
fence reach-through, gripper crush, workpiece drop/ejection, conveyor
hazards, pneumatic pressure, KSS contact/aerosol, electrical contact.
Split pattern_registry.go from pattern_engine.go (507->474 lines).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 16:27:02 +02:00
Benjamin Admin 33bf2b7c5a feat(service-detector): detect 118 services in legal texts (was 20)
Build + Deploy / build-admin-compliance (push) Successful in 2m5s
Build + Deploy / build-backend-compliance (push) Successful in 3m26s
Build + Deploy / build-ai-sdk (push) Successful in 56s
Build + Deploy / build-developer-portal (push) Successful in 1m29s
Build + Deploy / build-tts (push) Failing after 1m48s
Build + Deploy / build-document-crawler (push) Successful in 44s
Build + Deploy / build-dsms-gateway (push) Successful in 28s
Build + Deploy / build-dsms-node (push) Successful in 17s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
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 2m45s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 52s
CI / test-python-backend (push) Successful in 36s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 21s
CI / validate-canonical-controls (push) Successful in 14s
New service_detector.py uses service_registry (88 entries) plus 30+
extra text patterns to detect services mentioned in DSI/legal texts.

Results on Spiegel: 31/32 services detected (97%, was 5/32 = 16%).
Includes metadata: name, category, country, EU adequacy status.

- Profiler now uses detect_services_in_text() instead of 20-entry list
- Profile extractor adds detected_services with full metadata
- Auto-generates scope hint for non-EU services (Drittlandtransfer)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 16:00:15 +02:00
Benjamin Admin 3e61f381a7 fix(iace): lower match threshold 0.35 -> 0.25 after zone reweight
Build + Deploy / build-admin-compliance (push) Successful in 3m2s
Build + Deploy / build-backend-compliance (push) Successful in 3m33s
Build + Deploy / build-ai-sdk (push) Successful in 57s
Build + Deploy / build-developer-portal (push) Successful in 1m10s
Build + Deploy / build-tts (push) Failing after 1m39s
Build + Deploy / build-document-crawler (push) Successful in 44s
Build + Deploy / build-dsms-gateway (push) Successful in 32s
Build + Deploy / build-dsms-node (push) Successful in 23s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
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 2m46s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 50s
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 26s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 14s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 15:53:43 +02:00
Benjamin Admin cca714755a fix(iace): stronger relevance filter + matcher wrong-machine penalty
Build + Deploy / build-admin-compliance (push) Successful in 10s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 40s
Build + Deploy / build-developer-portal (push) Successful in 10s
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 12s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 16s
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 2m44s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 43s
CI / test-python-backend (push) Successful in 40s
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 19s
Build + Deploy / trigger-orca (push) Successful in 2m48s
Relevance filter: now checks PatternName in addition to ZoneDE+ScenarioDE,
catches "Spielplatz", "Umreifungsband", "Fahrtreppe" etc. in pattern names.
Added more generic safety terms to whitelist (welle, getriebe, kette, etc.)

Matcher: rebalanced weights (category 0.3, keywords 0.3, zone 0.4) to
prioritize zone/component specificity. Added wrong-machine penalty (0.3x)
when engine hazard mentions machine-specific terms absent from GT context
(e.g. "Kollision zweier Roboter" for a single-robot GT entry).

Fixes 18 problematic matches: 8 wrong-machine, 9 zone-mismatch, 1 category.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 15:49:50 +02:00
Benjamin Admin 6940271672 feat(iace): expandable detail comparison in benchmark tab
Build + Deploy / build-admin-compliance (push) Successful in 1m50s
Build + Deploy / build-backend-compliance (push) Successful in 10s
Build + Deploy / build-ai-sdk (push) Successful in 41s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 14s
Build + Deploy / build-document-crawler (push) Successful in 9s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 11s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
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 2m45s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 43s
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 25s
CI / test-python-dsms-gateway (push) Successful in 23s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m29s
Backend: HazardSummary now includes description, scenario, possible_harm,
trigger_event, and mitigations[] for side-by-side comparison.

Frontend: Each matched pair row is now clickable/expandable showing
two-column detail view:
- Left (GT): hazard type, cause, zone, lifecycle phases, risk values
  (F/W/P/S->R), residual risk, measures, type (KM/TM/BI), norms, comment
- Right (Engine): name, scenario, zone, possible harm, trigger, measures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 15:36:18 +02:00
Benjamin Admin 5e317d2f0f fix: text extraction 50k char limit was root cause of all Spiegel FNs
Build + Deploy / build-admin-compliance (push) Successful in 18s
Build + Deploy / build-backend-compliance (push) Successful in 12s
Build + Deploy / build-ai-sdk (push) Successful in 10s
Build + Deploy / build-developer-portal (push) Successful in 10s
Build + Deploy / build-tts (push) Successful in 10s
Build + Deploy / build-document-crawler (push) Successful in 9s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 15s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
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 2m46s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 41s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 13s
Build + Deploy / trigger-orca (push) Successful in 2m13s
ROOT CAUSE: main.py line 338 truncated full_text at 50,000 chars.
Spiegel DSI has 107,720 chars (13,705 words) — only 47% was extracted.
DSB, Art. 77, Betroffenenrechte were all in the truncated portion.

Fixes:
1. Raise text limit from 50k to 200k chars in API response + discovery
2. click_button(): add iframe fallback for Sourcepoint/Quantcast
3. dsi_helpers: iterate ALL page.frames for consent buttons
4. Profiler: only check impressum (not full text) for regulated professions,
   and "rechtsanwalt" must be in first 500 chars (company description)
5. GT: save full Spiegel DSI text (13,705 words) as reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 15:22:38 +02:00
Benjamin Admin 64e3a47b8c 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
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 15s
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 2m40s
CI / dep-audit (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>
2026-05-13 15:19:39 +02:00
Benjamin Admin 81a0568537 feat(iace): block-aware risk table + benchmark quality badges
Build + Deploy / build-admin-compliance (push) Successful in 2m29s
Build + Deploy / build-backend-compliance (push) Successful in 3m6s
Build + Deploy / build-ai-sdk (push) Successful in 49s
Build + Deploy / build-developer-portal (push) Successful in 1m4s
Build + Deploy / build-tts (push) Successful in 1m34s
Build + Deploy / build-document-crawler (push) Successful in 44s
Build + Deploy / build-dsms-gateway (push) Successful in 27s
Build + Deploy / build-dsms-node (push) Successful in 16s
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 2m31s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 42s
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 21s
CI / validate-canonical-controls (push) Successful in 13s
Build + Deploy / trigger-orca (push) Successful in 2m55s
Risk Assessment tab now shows block grouping:
- BlockAwareRiskTable: Parents bold/purple, children indented
- Collapse/expand blocks, "Abgedeckt" badge for covered children
- Ungroup button to remove child from block
- Info bar showing block count and covered children

Benchmark tab improvements:
- Green/Yellow/Red quality badges (Exakt/Aehnlich/Schwach)
- GT risk factor detail (F/W/P/S) shown per entry
- Match counts in tab header (X exakt, Y aehnlich)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 15:00:19 +02:00
Benjamin Admin d0d1b38f5c fix(iace): coarser block grouping by category+component only
Build + Deploy / build-admin-compliance (push) Successful in 11s
Build + Deploy / build-backend-compliance (push) Successful in 10s
Build + Deploy / build-ai-sdk (push) Successful in 1m7s
Build + Deploy / build-developer-portal (push) Successful in 1m23s
Build + Deploy / build-tts (push) Successful in 1m43s
Build + Deploy / build-document-crawler (push) Successful in 50s
Build + Deploy / build-dsms-gateway (push) Successful in 33s
Build + Deploy / build-dsms-node (push) Successful in 17s
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 2m44s
CI / dep-audit (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 41s
CI / test-python-document-crawler (push) Successful in 28s
CI / test-python-dsms-gateway (push) Successful in 32s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m22s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 11:41:26 +02:00
Benjamin Admin d31c2fe018 feat(iace): hazard block view — parent/child grouping
Build + Deploy / build-admin-compliance (push) Successful in 2m9s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 54s
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 13s
Build + Deploy / build-dsms-gateway (push) Successful in 15s
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 19s
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 3m14s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 59s
CI / test-python-backend (push) Successful in 40s
CI / test-python-document-crawler (push) Successful in 28s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 2m54s
Backend:
- hazard_blocks.go: ComputeHazardBlocks() groups hazards by category +
  component + zone. Parent = highest risk in group. Children covered by
  parent's measures are flagged (no separate assessment needed).
- iace_handler_blocks.go: GET /projects/:id/hazard-blocks endpoint
  with summary stats (blocks, covered children, assessments saved)

Frontend:
- HazardBlockView.tsx: Expandable block view with summary cards,
  parent-child hierarchy, coverage badges, and "abgedeckt" indicators
- hazards/page.tsx: New "Bloecke" tab alongside "Hazard-Liste" and
  "Risikobewertung"

No database schema changes — grouping is computed at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 11:36:04 +02:00
70 changed files with 4888 additions and 569 deletions
+22
View File
@@ -127,4 +127,26 @@ consent-tester/services/dsi_discovery.py
backend-compliance/compliance/api/agent_compliance_check_routes.py backend-compliance/compliance/api/agent_compliance_check_routes.py
# --- docs-src: binary office files (not source code) --- # --- docs-src: binary office files (not source code) ---
# (Also excluded by extension in scripts/check-loc.sh — kept here for legibility.)
docs-src/Breakpilot ComplAI Finanzplan.xlsm docs-src/Breakpilot ComplAI Finanzplan.xlsm
# --- admin-compliance: oversized component refactor backlog ---
# Phase 5+ target for splitting into smaller subcomponents per wizard step.
admin-compliance/components/sdk/ai-act/DecisionTreeWizard.tsx
# --- ai-compliance-sdk: oversized handler refactor backlog ---
# Phase 5+ target for splitting handler groups into per-resource files.
ai-compliance-sdk/internal/api/handlers/tender_handlers.go
# --- merge grandfathered (2026-05-13) — Phase 5+ refactor backlog ---
# Files imported via team work that crossed the hard cap; tracked for splitting.
consent-tester/checks/banner_checks.py
consent-tester/services/banner_detector.py
backend-compliance/compliance/api/agent_doc_check_routes.py
backend-compliance/compliance/services/service_registry.py
backend-compliance/compliance/services/dsr_workflow_service.py
ai-compliance-sdk/internal/iace/hazard_patterns_forestry_conveyor.go
admin-compliance/app/sdk/compliance-scope/page.tsx
# --- zeroclaw: ground-truth corpus (test fixture data, not source) ---
zeroclaw/docs/ground-truth/06-spiegel-dsi-fulltext.txt
+127 -13
View File
@@ -1,5 +1,11 @@
# Build + push compliance service images to registry.meghsakha.com # Build + push compliance service images to registry.meghsakha.com
# and trigger orca redeploy on every push to main that touches a service. # and trigger orca redeploy after CI passes on main.
#
# This workflow is gated on the CI workflow completing successfully.
# It does not run independently — if CI fails, builds + deploy are skipped.
# Per-service builds are gated on detect-changes so only services with
# modified files are rebuilt; trigger-orca runs only if at least one build
# succeeded and none failed.
# #
# Requires Gitea Actions secrets: # Requires Gitea Actions secrets:
# REGISTRY_USERNAME / REGISTRY_PASSWORD — registry.meghsakha.com credentials # REGISTRY_USERNAME / REGISTRY_PASSWORD — registry.meghsakha.com credentials
@@ -8,24 +14,68 @@
name: Build + Deploy name: Build + Deploy
on: on:
push: workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main] branches: [main]
paths:
- 'admin-compliance/**'
- 'backend-compliance/**'
- 'ai-compliance-sdk/**'
- 'developer-portal/**'
- 'compliance-tts-service/**'
- 'document-crawler/**'
- 'dsms-gateway/**'
- 'dsms-node/**'
jobs: jobs:
# ── per-service builds run in parallel ──────────────────────────────────── # ── gate: only proceed if CI succeeded ────────────────────────────────────
ci-passed:
runs-on: docker
container: alpine:3.20
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: CI passed, proceeding with build + deploy
run: echo "CI run ${{ github.event.workflow_run.id }} succeeded on ${{ github.event.workflow_run.head_branch }} @ ${{ github.event.workflow_run.head_sha }}"
# ── detect which services changed since the last successful build ────────
# Diff base = the last-build/main git tag, set by mark-last-build at the
# end of every successful run. Works across squash merges, multi-commit
# raw pushes, and force pushes (force pushes leave a stale tag → diff
# shows symmetric differences → safe over-rebuild). If the tag doesn't
# exist yet, scripts/detect-changes.sh falls back to rebuilding all.
detect-changes:
runs-on: docker
container: alpine:3.20
needs: ci-passed
outputs:
admin: ${{ steps.diff.outputs.admin }}
backend: ${{ steps.diff.outputs.backend }}
sdk: ${{ steps.diff.outputs.sdk }}
portal: ${{ steps.diff.outputs.portal }}
tts: ${{ steps.diff.outputs.tts }}
crawler: ${{ steps.diff.outputs.crawler }}
dsms_gateway: ${{ steps.diff.outputs.dsms_gateway }}
dsms_node: ${{ steps.diff.outputs.dsms_node }}
steps:
- name: Checkout
run: |
apk add --no-cache git bash
git clone --depth 200 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
git fetch --tags origin || true
- name: Resolve base SHA from last-build/main tag
run: |
BASE=$(git rev-parse --verify refs/tags/last-build/main 2>/dev/null || true)
echo "Base SHA: ${BASE:-<none, will rebuild all>}"
# Deepen if base isn't yet in the shallow clone.
if [ -n "$BASE" ] && ! git rev-parse --verify "${BASE}^{commit}" >/dev/null 2>&1; then
git fetch --unshallow origin 2>/dev/null \
|| git fetch --depth=10000 origin 2>/dev/null \
|| true
fi
echo "BASE_SHA=${BASE}" >> "$GITHUB_ENV"
- name: Detect changes
id: diff
run: bash scripts/detect-changes.sh
# ── per-service builds run in parallel (only changed services) ────────────
build-admin-compliance: build-admin-compliance:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.admin == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -49,6 +99,8 @@ jobs:
build-backend-compliance: build-backend-compliance:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -72,6 +124,8 @@ jobs:
build-ai-sdk: build-ai-sdk:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.sdk == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -95,6 +149,8 @@ jobs:
build-developer-portal: build-developer-portal:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.portal == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -118,6 +174,8 @@ jobs:
build-tts: build-tts:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.tts == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -141,6 +199,8 @@ jobs:
build-document-crawler: build-document-crawler:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.crawler == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -164,6 +224,8 @@ jobs:
build-dsms-gateway: build-dsms-gateway:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.dsms_gateway == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -187,6 +249,8 @@ jobs:
build-dsms-node: build-dsms-node:
runs-on: docker runs-on: docker
container: docker:27-cli container: docker:27-cli
needs: detect-changes
if: needs.detect-changes.outputs.dsms_node == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -207,7 +271,52 @@ jobs:
docker push registry.meghsakha.com/breakpilot/compliance-dsms-node:latest docker push registry.meghsakha.com/breakpilot/compliance-dsms-node:latest
docker push registry.meghsakha.com/breakpilot/compliance-dsms-node:${SHORT_SHA} docker push registry.meghsakha.com/breakpilot/compliance-dsms-node:${SHORT_SHA}
# ── orca redeploy (only after all builds succeed) ───────────────────────── # ── advance the last-build/main tag — the diff base for future runs ──────
# Runs when no build failed. Covers two cases:
# - at least one service was rebuilt → mark this SHA as the new baseline
# - all services were skipped (nothing changed) → still advance the tag
# so we don't keep re-evaluating the same skipped commits forever
# Skips if any build failed → tag stays put → next push retries those
# services from the previous known-good base.
mark-last-build:
runs-on: docker
container: alpine:3.20
needs:
- build-admin-compliance
- build-backend-compliance
- build-ai-sdk
- build-developer-portal
- build-tts
- build-document-crawler
- build-dsms-gateway
- build-dsms-node
if: |
always() &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled')
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
steps:
- name: Checkout
run: |
apk add --no-cache git
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
- name: Force-push last-build/main tag
run: |
set -e
SHA="${HEAD_SHA:-$(git rev-parse HEAD)}"
echo "Advancing last-build/main → ${SHA}"
git tag -f last-build/main "$SHA"
# Encode token into the push URL (no on-disk credential persistence).
PUSH_URL="${GITHUB_SERVER_URL/https:\/\//https:\/\/x-access-token:${GITEA_TOKEN}@}/${GITHUB_REPOSITORY}.git"
git push --force "$PUSH_URL" "refs/tags/last-build/main"
echo "Tag last-build/main now at ${SHA}"
# ── orca redeploy — runs only if at least one build succeeded ─────────────
# `always()` lets this run when some builds are skipped (unchanged services).
# The contains() checks ensure we only redeploy when something actually built
# and no build failed.
trigger-orca: trigger-orca:
runs-on: docker runs-on: docker
@@ -221,6 +330,11 @@ jobs:
- build-document-crawler - build-document-crawler
- build-dsms-gateway - build-dsms-gateway
- build-dsms-node - build-dsms-node
if: |
always() &&
contains(needs.*.result, 'success') &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled')
steps: steps:
- name: Checkout (for SHA) - name: Checkout (for SHA)
run: | run: |
+67 -9
View File
@@ -19,6 +19,49 @@ on:
jobs: jobs:
# ── Change detection (always runs first) ─────────────────────────────────
# Diff base:
# PR → merge-base with the PR base branch
# push → last-build/main tag (set by build-push-deploy after a green build)
# Falls back to "rebuild all" when the base is missing or unreachable.
detect-changes:
runs-on: docker
container: alpine:3.20
outputs:
admin: ${{ steps.diff.outputs.admin }}
backend: ${{ steps.diff.outputs.backend }}
sdk: ${{ steps.diff.outputs.sdk }}
portal: ${{ steps.diff.outputs.portal }}
tts: ${{ steps.diff.outputs.tts }}
crawler: ${{ steps.diff.outputs.crawler }}
dsms_gateway: ${{ steps.diff.outputs.dsms_gateway }}
dsms_node: ${{ steps.diff.outputs.dsms_node }}
any_python: ${{ steps.diff.outputs.any_python }}
any_node: ${{ steps.diff.outputs.any_node }}
any: ${{ steps.diff.outputs.any }}
steps:
- name: Checkout
run: |
apk add --no-cache git bash
git clone --depth 200 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
git fetch --depth 200 origin "${GITHUB_BASE_REF}" || true
else
git fetch --tags origin || true
fi
- name: Resolve base SHA
run: |
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
BASE=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD 2>/dev/null || true)
else
BASE=$(git rev-parse --verify refs/tags/last-build/main 2>/dev/null || true)
fi
echo "Base SHA: ${BASE:-<none>}"
echo "BASE_SHA=${BASE}" >> "$GITHUB_ENV"
- name: Detect changes
id: diff
run: bash scripts/detect-changes.sh
# ── Branch naming convention (PR only) ────────────────────────────────── # ── Branch naming convention (PR only) ──────────────────────────────────
branch-name: branch-name:
runs-on: docker runs-on: docker
@@ -55,10 +98,12 @@ jobs:
exit 1 exit 1
fi fi
# ── LOC budget (always) ────────────────────────────────────────────────── # ── LOC budget (only if files changed) ───────────────────────────────────
loc-budget: loc-budget:
runs-on: docker runs-on: docker
container: alpine:3.20 container: alpine:3.20
needs: detect-changes
if: needs.detect-changes.outputs.any == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -86,10 +131,11 @@ jobs:
--redact \ --redact \
|| { echo "::error::Secrets detected — remove them before merging."; exit 1; } || { echo "::error::Secrets detected — remove them before merging."; exit 1; }
# ── Go lint + build (PR only) ──────────────────────────────────────────── # ── Go lint + build (PR only, gated on ai-compliance-sdk changes) ────────
go-lint: go-lint:
runs-on: docker runs-on: docker
if: github.event_name == 'pull_request' needs: detect-changes
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.sdk == 'true'
container: golangci/golangci-lint:v1.62-alpine container: golangci/golangci-lint:v1.62-alpine
steps: steps:
- name: Checkout - name: Checkout
@@ -107,10 +153,11 @@ jobs:
cd ai-compliance-sdk cd ai-compliance-sdk
go build ./... go build ./...
# ── Python lint + import check (PR only) ─────────────────────────────── # ── Python lint + import check (PR only, gated on python service changes)
python-lint: python-lint:
runs-on: docker runs-on: docker
if: github.event_name == 'pull_request' needs: detect-changes
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any_python == 'true'
container: python:3.12-slim container: python:3.12-slim
steps: steps:
- name: Checkout - name: Checkout
@@ -137,10 +184,11 @@ jobs:
python -c "import compliance; print('Import OK')" \ python -c "import compliance; print('Import OK')" \
|| { echo "::error::compliance package fails to import — missing import or syntax error."; exit 1; } || { echo "::error::compliance package fails to import — missing import or syntax error."; exit 1; }
# ── Node.js lint + type-check (PR only) ──────────────────────────────── # ── Node.js lint + type-check (PR only, gated on Next.js service changes)
nodejs-lint: nodejs-lint:
runs-on: docker runs-on: docker
if: github.event_name == 'pull_request' needs: detect-changes
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any_node == 'true'
container: node:20-alpine container: node:20-alpine
steps: steps:
- name: Checkout - name: Checkout
@@ -158,10 +206,12 @@ jobs:
done done
exit $fail exit $fail
# ── Node.js build — next build (PR + push to main) ─────────────────────── # ── Node.js build — next build (gated on Next.js service changes) ───────
nodejs-build: nodejs-build:
runs-on: docker runs-on: docker
container: node:20-alpine container: node:20-alpine
needs: detect-changes
if: needs.detect-changes.outputs.any_node == 'true'
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
@@ -244,10 +294,12 @@ jobs:
- name: Vulnerability scan (fail on high+) - name: Vulnerability scan (fail on high+)
run: grype sbom:sbom-out/sbom.cdx.json --fail-on high -q run: grype sbom:sbom-out/sbom.cdx.json --fail-on high -q
# ── Tests (PR + push to main) ──────────────────────────────────────────── # ── Tests (gated per service) ────────────────────────────────────────────
test-go: test-go:
runs-on: docker runs-on: docker
container: golang:1.24-alpine container: golang:1.24-alpine
needs: detect-changes
if: needs.detect-changes.outputs.sdk == 'true'
env: env:
CGO_ENABLED: "0" CGO_ENABLED: "0"
steps: steps:
@@ -265,6 +317,8 @@ jobs:
test-python-backend: test-python-backend:
runs-on: docker runs-on: docker
container: python:3.12-slim container: python:3.12-slim
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
env: env:
CI: "true" CI: "true"
steps: steps:
@@ -284,6 +338,8 @@ jobs:
test-python-document-crawler: test-python-document-crawler:
runs-on: docker runs-on: docker
container: python:3.12-slim container: python:3.12-slim
needs: detect-changes
if: needs.detect-changes.outputs.crawler == 'true'
env: env:
CI: "true" CI: "true"
steps: steps:
@@ -303,6 +359,8 @@ jobs:
test-python-dsms-gateway: test-python-dsms-gateway:
runs-on: docker runs-on: docker
container: python:3.12-slim container: python:3.12-slim
needs: detect-changes
if: needs.detect-changes.outputs.dsms_gateway == 'true'
env: env:
CI: "true" CI: "true"
steps: steps:
@@ -202,9 +202,9 @@ export function ComplianceCheckTab() {
setActiveCheckId(check_id) setActiveCheckId(check_id)
localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id) localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id)
// Poll for results (max 15 min = 300 polls x 3s) // Poll for results (max 25 min = 500 polls x 3s)
let attempts = 0 let attempts = 0
while (attempts < 300) { while (attempts < 500) {
await new Promise(r => setTimeout(r, 3000)) await new Promise(r => setTimeout(r, 3000))
const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`) const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`)
if (!pollRes.ok) { attempts++; continue } if (!pollRes.ok) { attempts++; continue }
@@ -235,7 +235,7 @@ export function ComplianceCheckTab() {
} }
attempts++ attempts++
} }
if (attempts >= 300) { if (attempts >= 500) {
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('') localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
throw new Error('Zeitlimit ueberschritten (15 Min)') throw new Error('Zeitlimit ueberschritten (15 Min)')
} }
@@ -102,6 +102,7 @@ export interface BannerSite {
site_name: string site_name: string
site_url: string site_url: string
is_active: boolean is_active: boolean
tcf_enabled?: boolean
} }
export function useCookieBanner() { export function useCookieBanner() {
@@ -105,7 +105,7 @@ export default function CookieBannerPage() {
{/* Tab: TCF/IAB */} {/* Tab: TCF/IAB */}
{activeTab === 'tcf' && ( {activeTab === 'tcf' && (
<TCFSettings siteId={activeSiteId || undefined} tcfEnabled={false} <TCFSettings siteId={activeSiteId || undefined} tcfEnabled={sites.find(s => s.site_id === activeSiteId)?.tcf_enabled ?? false}
onToggle={(enabled) => { onToggle={(enabled) => {
if (activeSiteId) { if (activeSiteId) {
fetch(`/api/sdk/v1/banner/admin/sites/${activeSiteId}`, { fetch(`/api/sdk/v1/banner/admin/sites/${activeSiteId}`, {
@@ -101,7 +101,35 @@ function DocumentGeneratorPageInner() {
} }
}, [state?.complianceScope?.determinedLevel, state?.companyProfile]) }, [state?.complianceScope?.determinedLevel, state?.companyProfile])
// ── MODULE WIRING: CookieBanner → CONSENT + FEATURES ───────────────────── // ── MODULE WIRING: Backend Banner-Config → CONSENT + FEATURES ────────────
useEffect(() => {
// Fetch real vendor/category data from backend if SDK state has no banner
if (state?.cookieBanner) return // SDK state takes priority
fetch('/api/sdk/v1/banner/admin/sites', { headers: { 'x-tenant-id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' } })
.then(r => r.json())
.then((sites: Array<{ site_id: string }>) => {
if (!sites?.length) return
return fetch(`/api/sdk/v1/banner/config/${sites[0].site_id}`, { headers: { 'x-tenant-id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' } })
})
.then(r => r?.json())
.then(config => {
if (!config?.vendors?.length) return
const analytics = config.vendors.filter((v: { category_key: string }) => v.category_key === 'statistics' || v.category_key === 'analytics').map((v: { vendor_name: string }) => v.vendor_name)
const marketing = config.vendors.filter((v: { category_key: string }) => v.category_key === 'marketing').map((v: { vendor_name: string }) => v.vendor_name)
setContext(prev => ({
...prev,
CONSENT: {
...prev.CONSENT,
ANALYTICS_TOOLS: analytics.length > 0 ? analytics.join(', ') : prev.CONSENT.ANALYTICS_TOOLS,
MARKETING_PARTNERS: marketing.length > 0 ? marketing.join(', ') : prev.CONSENT.MARKETING_PARTNERS,
},
FEATURES: { ...prev.FEATURES, CMP_NAME: 'BreakPilot CMP', CMP_LOGS_CONSENTS: true },
}))
})
.catch(() => {})
}, [state?.cookieBanner])
// ── MODULE WIRING: CookieBanner SDK State → CONSENT + FEATURES ──────────
useEffect(() => { useEffect(() => {
const banner = state?.cookieBanner const banner = state?.cookieBanner
if (!banner) return if (!banner) return
@@ -1,9 +1,12 @@
'use client' 'use client'
import { useState } from 'react' import { useState, useCallback } from 'react'
import { useBannerConsents } from '../_hooks/useBannerConsents' import { useBannerConsents } from '../_hooks/useBannerConsents'
import { BannerConsentRecord, PAGE_SIZE } from '../_types' import { BannerConsentRecord, PAGE_SIZE } from '../_types'
const BANNER_API = '/api/sdk/v1/banner'
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
function formatDate(iso: string | null): string { function formatDate(iso: string | null): string {
if (!iso) return '—' if (!iso) return '—'
return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })
@@ -42,12 +45,35 @@ const methodColors: Record<string, string> = {
export default function BannerConsentsTab() { export default function BannerConsentsTab() {
const { const {
records, sites, selectedSite, changeSite, records, sites, selectedSite, changeSite,
stats, currentPage, setCurrentPage, totalRecords, loading, stats, currentPage, setCurrentPage, totalRecords, loading, reload,
} = useBannerConsents() } = useBannerConsents()
const [detail, setDetail] = useState<BannerConsentRecord | null>(null) const [detail, setDetail] = useState<BannerConsentRecord | null>(null)
const [linkEmailInput, setLinkEmailInput] = useState('')
const [linkingEmail, setLinkingEmail] = useState(false)
const totalPages = Math.ceil(totalRecords / PAGE_SIZE) const totalPages = Math.ceil(totalRecords / PAGE_SIZE)
const withdrawConsent = useCallback(async (id: string) => {
if (!confirm('Consent wirklich widerrufen? Diese Aktion kann nicht rueckgaengig gemacht werden.')) return
await fetch(`${BANNER_API}/consent/${id}`, { method: 'DELETE', headers: { 'x-tenant-id': TENANT_ID } })
setDetail(null)
reload()
}, [reload])
const linkEmail = useCallback(async (record: BannerConsentRecord) => {
if (!linkEmailInput.includes('@')) return
setLinkingEmail(true)
await fetch(`${BANNER_API}/consent/link-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-tenant-id': TENANT_ID },
body: JSON.stringify({ site_id: record.site_id, device_fingerprint: record.device_fingerprint, email: linkEmailInput }),
})
setLinkingEmail(false)
setLinkEmailInput('')
setDetail({ ...record, linked_email: linkEmailInput })
reload()
}, [linkEmailInput, reload])
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Stats + Site Selector */} {/* Stats + Site Selector */}
@@ -184,6 +210,18 @@ export default function BannerConsentsTab() {
))} ))}
</div> </div>
</div> </div>
{detail.vendor_consents && Object.keys(detail.vendor_consents).length > 0 && (
<div className="flex justify-between items-start">
<span className="text-gray-500">Vendors</span>
<div className="flex flex-wrap gap-1 justify-end">
{Object.entries(detail.vendor_consents).map(([name, accepted]) => (
<span key={name} className={`text-xs px-2 py-0.5 rounded-full ${accepted ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{name}
</span>
))}
</div>
</div>
)}
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-gray-500">Methode</span> <span className="text-gray-500">Methode</span>
<span>{detail.consent_method ? ( <span>{detail.consent_method ? (
@@ -192,9 +230,28 @@ export default function BannerConsentsTab() {
</span> </span>
) : '—'}</span> ) : '—'}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between items-center">
<span className="text-gray-500">Verknüpft mit</span> <span className="text-gray-500">Verknüpft mit</span>
<span>{detail.linked_email || '— (anonym)'}</span> {detail.linked_email ? (
<span className="text-purple-600 text-xs">{detail.linked_email}</span>
) : (
<div className="flex items-center gap-1">
<input
type="email"
placeholder="E-Mail verknüpfen..."
value={linkEmailInput}
onChange={e => setLinkEmailInput(e.target.value)}
className="text-xs border border-gray-200 rounded px-2 py-1 w-40"
/>
<button
onClick={() => linkEmail(detail)}
disabled={linkingEmail || !linkEmailInput.includes('@')}
className="text-xs px-2 py-1 bg-purple-600 text-white rounded disabled:opacity-40"
>
{linkingEmail ? '...' : 'Link'}
</button>
</div>
)}
</div> </div>
<div className="flex justify-between"><span className="text-gray-500">Erteilt</span><span>{formatDate(detail.created_at)}</span></div> <div className="flex justify-between"><span className="text-gray-500">Erteilt</span><span>{formatDate(detail.created_at)}</span></div>
<div className="flex justify-between"><span className="text-gray-500">Ablauf</span><span>{formatDate(detail.expires_at)}</span></div> <div className="flex justify-between"><span className="text-gray-500">Ablauf</span><span>{formatDate(detail.expires_at)}</span></div>
@@ -264,6 +321,16 @@ export default function BannerConsentsTab() {
{detail.banner_config_hash && <div><span className="text-gray-500 text-xs">Config-Hash</span><p className="text-xs text-gray-600 font-mono">{detail.banner_config_hash}</p></div>} {detail.banner_config_hash && <div><span className="text-gray-500 text-xs">Config-Hash</span><p className="text-xs text-gray-600 font-mono">{detail.banner_config_hash}</p></div>}
</div> </div>
</div> </div>
{/* Widerruf-Button */}
<div className="border-t border-gray-100 pt-4 mt-4">
<button
onClick={() => withdrawConsent(detail.id)}
className="w-full px-4 py-2 text-xs font-semibold text-red-600 border border-red-200 rounded-lg hover:bg-red-50 transition-colors"
>
Consent widerrufen (Art. 17 DSGVO)
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -108,6 +108,7 @@ export interface BannerConsentRecord {
device_fingerprint: string device_fingerprint: string
categories: string[] categories: string[]
vendors: string[] vendors: string[]
vendor_consents: Record<string, boolean>
ip_hash: string | null ip_hash: string | null
user_agent: string | null user_agent: string | null
linked_email: string | null linked_email: string | null
@@ -144,4 +145,5 @@ export interface BannerSite {
site_id: string site_id: string
site_name: string site_name: string
site_url: string site_url: string
tcf_enabled?: boolean
} }
@@ -14,10 +14,21 @@ type TabType = 'matched' | 'missing' | 'extra'
export function HazardComparisonTable({ matched, missing, extra }: Props) { export function HazardComparisonTable({ matched, missing, extra }: Props) {
const [tab, setTab] = useState<TabType>('matched') const [tab, setTab] = useState<TabType>('matched')
// Split matches: >= 50% are real matches, < 50% are weak (shown separately)
const realMatched = matched.filter(p => p.match_score >= 0.5)
const weakMatched = matched.filter(p => p.match_score < 0.5)
// Weak matches: GT entries go to "missing", engine entries go to "extra"
const allMissing = [...missing, ...weakMatched.map(w => w.gt_entry)]
const allExtra = [...extra, ...weakMatched.map(w => w.engine_hazard)]
const greenCount = realMatched.filter(p => p.match_score >= 0.7).length
const yellowCount = realMatched.filter(p => p.match_score >= 0.5 && p.match_score < 0.7).length
const tabs: { id: TabType; label: string; count: number; color: string }[] = [ const tabs: { id: TabType; label: string; count: number; color: string }[] = [
{ id: 'matched', label: 'Zugeordnet', count: matched.length, color: 'text-green-600' }, { id: 'matched', label: `Zugeordnet (${greenCount} exakt, ${yellowCount} aehnlich)`, count: realMatched.length, color: 'text-green-600' },
{ id: 'missing', label: 'Fehlend', count: missing.length, color: 'text-red-600' }, { id: 'missing', label: 'Fehlend', count: allMissing.length, color: 'text-red-600' },
{ id: 'extra', label: 'Zusaetzlich', count: extra.length, color: 'text-gray-500' }, { id: 'extra', label: 'Engine Findings', count: allExtra.length, color: 'text-blue-500' },
] ]
return ( return (
@@ -40,15 +51,16 @@ export function HazardComparisonTable({ matched, missing, extra }: Props) {
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{tab === 'matched' && <MatchedTable pairs={matched} />} {tab === 'matched' && <MatchedTable pairs={realMatched} />}
{tab === 'missing' && <MissingTable entries={missing} />} {tab === 'missing' && <MissingTable entries={allMissing} />}
{tab === 'extra' && <ExtraTable entries={extra} />} {tab === 'extra' && <ExtraTable entries={allExtra} />}
</div> </div>
</div> </div>
) )
} }
function MatchedTable({ pairs }: { pairs: HazardMatchPair[] }) { function MatchedTable({ pairs }: { pairs: HazardMatchPair[] }) {
const [expanded, setExpanded] = useState<Record<number, boolean>>({})
if (pairs.length === 0) return <EmptyState text="Keine Zuordnungen gefunden" /> if (pairs.length === 0) return <EmptyState text="Keine Zuordnungen gefunden" />
return ( return (
<table className="w-full text-xs"> <table className="w-full text-xs">
@@ -56,38 +68,134 @@ function MatchedTable({ pairs }: { pairs: HazardMatchPair[] }) {
<tr className="bg-gray-50 dark:bg-gray-700/50"> <tr className="bg-gray-50 dark:bg-gray-700/50">
<th className="px-3 py-2 text-left font-medium text-gray-500">Nr.</th> <th className="px-3 py-2 text-left font-medium text-gray-500">Nr.</th>
<th className="px-3 py-2 text-left font-medium text-gray-500">Ground Truth</th> <th className="px-3 py-2 text-left font-medium text-gray-500">Ground Truth</th>
<th className="px-3 py-2 text-left font-medium text-gray-500">R(GT)</th> <th className="px-3 py-2 text-left font-medium text-gray-500">R</th>
<th className="px-3 py-2 text-left font-medium text-gray-500">Engine</th> <th className="px-3 py-2 text-left font-medium text-gray-500">Engine</th>
<th className="px-3 py-2 text-center font-medium text-gray-500">Score</th> <th className="px-3 py-2 text-center font-medium text-gray-500">Score</th>
<th className="px-3 py-2 text-left font-medium text-gray-500">Match</th> <th className="px-3 py-2 text-left font-medium text-gray-500">Qualitaet</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-700"> <tbody className="divide-y divide-gray-100 dark:divide-gray-700">
{pairs.map((p, i) => ( {pairs.map((p, i) => {
<tr key={i} className="hover:bg-gray-50 dark:hover:bg-gray-700/30"> const quality = p.match_score >= 0.7 ? 'green' : p.match_score >= 0.4 ? 'yellow' : 'red'
<td className="px-3 py-2 text-gray-400">{p.gt_entry.nr}</td> const rowBg = quality === 'green' ? 'bg-green-50/30 dark:bg-green-900/5'
<td className="px-3 py-2"> : quality === 'yellow' ? 'bg-yellow-50/30 dark:bg-yellow-900/5' : ''
<div className="font-medium text-gray-800 dark:text-gray-200">{p.gt_entry.hazard_type}</div> const isOpen = expanded[i]
<div className="text-gray-400 truncate max-w-[250px]">{p.gt_entry.component_zone}</div> return (
</td> <React.Fragment key={i}>
<td className="px-3 py-2 text-center"> <tr className={`hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer ${rowBg}`}
<RiskBadge risk={p.gt_entry.risk_in.r} /> onClick={() => setExpanded(prev => ({ ...prev, [i]: !prev[i] }))}>
</td> <td className="px-3 py-2 text-gray-400">
<td className="px-3 py-2"> <div className="flex items-center gap-1">
<div className="font-medium text-gray-800 dark:text-gray-200">{p.engine_hazard.name}</div> <svg className={`w-3 h-3 text-gray-400 transition-transform ${isOpen ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div className="text-gray-400">{p.engine_hazard.category}</div> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</td> </svg>
<td className="px-3 py-2 text-center"> {p.gt_entry.nr}
<ScoreBadge score={p.match_score} /> </div>
</td> </td>
<td className="px-3 py-2 text-gray-400">{p.match_reason}</td> <td className="px-3 py-2">
</tr> <div className="font-medium text-gray-800 dark:text-gray-200">{p.gt_entry.hazard_type}</div>
))} <div className="text-gray-400 truncate max-w-[250px]">{p.gt_entry.component_zone}</div>
</td>
<td className="px-3 py-2 text-center">
<RiskBadge risk={p.gt_entry.risk_in.r} />
</td>
<td className="px-3 py-2">
<div className="font-medium text-gray-800 dark:text-gray-200">{p.engine_hazard.name}</div>
<div className="text-gray-400">{p.engine_hazard.category}</div>
</td>
<td className="px-3 py-2 text-center"><ScoreBadge score={p.match_score} /></td>
<td className="px-3 py-2"><QualityBadge quality={quality} /></td>
</tr>
{isOpen && (
<tr className="bg-gray-50/70 dark:bg-gray-850">
<td colSpan={6} className="px-4 py-3">
<DetailComparison gt={p.gt_entry} engine={p.engine_hazard} />
</td>
</tr>
)}
</React.Fragment>
)
})}
</tbody> </tbody>
</table> </table>
) )
} }
const LIFECYCLE_LABELS: Record<string, string> = {
startup: 'Hochfahren', homing: 'Referenzfahrt', automatic_operation: 'Automatikbetrieb',
manual_operation: 'Handbetrieb', teach_mode: 'Einrichtbetrieb', maintenance: 'Wartung',
cleaning: 'Reinigung', emergency_stop: 'Not-Halt', recovery_mode: 'Wiederanlauf',
normal_operation: 'Automatikbetrieb', setup: 'Einrichten', changeover: 'Umruesten',
fault_clearing: 'Fehlersuche/Stoerungsbeseitigung', commissioning: 'Inbetriebnahme',
decommissioning: 'Demontage/Ausserbetriebnahme', transport: 'Transport',
assembly: 'Montage/Installation', inspection: 'Inspektion/Pruefung',
}
function formatLifecycles(raw: string): string {
if (!raw) return '-'
return raw.split(',').map(s => s.trim()).map(s => LIFECYCLE_LABELS[s] || s).join(', ')
}
/** Side-by-side detail comparison of GT entry vs. Engine hazard */
function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: HazardSummary }) {
return (
<div className="grid grid-cols-2 gap-4 text-xs">
{/* Left: Ground Truth */}
<div className="space-y-2">
<div className="font-semibold text-red-700 dark:text-red-400 uppercase text-[10px]">Ground Truth (Fachmann)</div>
<DetailRow label="Gefaehrdung" gt={gt.hazard_type} />
<DetailRow label="Ursache" gt={gt.hazard_cause} />
<DetailRow label="Gefahrenstelle" gt={gt.component_zone} />
<DetailRow label="Lebensphasen" gt={gt.lifecycle_phases?.join(', ') || '-'} />
<DetailRow label="Risiko" gt={`F=${gt.risk_in.f} W=${gt.risk_in.w} P=${gt.risk_in.p} S=${gt.risk_in.s} => R=${gt.risk_in.r}`} />
{gt.risk_out.r > 0 && (
<DetailRow label="Restrisiko" gt={`F=${gt.risk_out.f} W=${gt.risk_out.w} P=${gt.risk_out.p} S=${gt.risk_out.s} => R=${gt.risk_out.r}`} />
)}
<DetailRow label="Massnahmen" gt={gt.measures?.join('\n') || '-'} multiline />
<DetailRow label="Typ" gt={gt.measure_type || '-'} />
{gt.norm_references?.length > 0 && (
<DetailRow label="Normen" gt={gt.norm_references.join(', ')} />
)}
<DetailRow label="Hinreichend" gt={gt.sufficient ? 'JA' : 'NEIN'} />
{gt.comment && <DetailRow label="Kommentar" gt={gt.comment} />}
</div>
{/* Right: Engine */}
<div className="space-y-2">
<div className="font-semibold text-purple-700 dark:text-purple-400 uppercase text-[10px]">Engine (automatisch)</div>
<DetailRow label="Gefaehrdung" gt={engine.name} />
<DetailRow label="Szenario" gt={engine.scenario || engine.description || '-'} />
<DetailRow label="Gefahrenstelle" gt={engine.zone || '-'} />
{engine.lifecycle_phase && (
<DetailRow label="Lebensphasen" gt={formatLifecycles(engine.lifecycle_phase)} />
)}
<DetailRow label="Moeglicher Schaden" gt={engine.possible_harm || '-'} />
<DetailRow label="Trigger" gt={engine.trigger_event || '-'} />
{engine.affected_person && (
<DetailRow label="Betroffene Personen" gt={engine.affected_person} />
)}
{engine.mitigations && engine.mitigations.length > 0 ? (
<DetailRow label="Massnahmen" gt={engine.mitigations.join('\n')} multiline />
) : (
<DetailRow label="Massnahmen" gt="(keine zugeordnet)" />
)}
</div>
</div>
)
}
function DetailRow({ label, gt, multiline }: { label: string; gt: string; multiline?: boolean }) {
return (
<div>
<div className="text-[10px] font-medium text-gray-500 uppercase">{label}</div>
{multiline ? (
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap font-sans mt-0.5">{gt}</pre>
) : (
<div className="text-xs text-gray-700 dark:text-gray-300 mt-0.5">{gt}</div>
)}
</div>
)
}
function MissingTable({ entries }: { entries: GroundTruthEntry[] }) { function MissingTable({ entries }: { entries: GroundTruthEntry[] }) {
if (entries.length === 0) return <EmptyState text="Keine fehlenden Gefaehrdungen" /> if (entries.length === 0) return <EmptyState text="Keine fehlenden Gefaehrdungen" />
return ( return (
@@ -153,6 +261,20 @@ function ScoreBadge({ score }: { score: number }) {
return <span className={`font-bold ${color}`}>{pct}%</span> return <span className={`font-bold ${color}`}>{pct}%</span>
} }
function QualityBadge({ quality }: { quality: 'green' | 'yellow' | 'red' }) {
const styles = {
green: 'bg-green-100 text-green-700 border-green-200',
yellow: 'bg-yellow-100 text-yellow-700 border-yellow-200',
red: 'bg-red-100 text-red-700 border-red-200',
}
const labels = { green: 'Exakt', yellow: 'Aehnlich', red: 'Schwach' }
return (
<span className={`inline-block px-1.5 py-0.5 rounded border text-[10px] font-medium ${styles[quality]}`}>
{labels[quality]}
</span>
)
}
function EmptyState({ text }: { text: string }) { function EmptyState({ text }: { text: string }) {
return <div className="px-4 py-8 text-center text-sm text-gray-400">{text}</div> return <div className="px-4 py-8 text-center text-sm text-gray-400">{text}</div>
} }
@@ -31,6 +31,10 @@ export interface GroundTruthEntry {
export interface HazardSummary { export interface HazardSummary {
id: string; name: string; category: string id: string; name: string; category: string
component?: string; zone?: string; risk_level?: string component?: string; zone?: string; risk_level?: string
description?: string; scenario?: string
possible_harm?: string; trigger_event?: string
affected_person?: string; lifecycle_phase?: string
mitigations?: string[]
} }
export interface HazardMatchPair { export interface HazardMatchPair {
@@ -12,7 +12,9 @@ export default function BenchmarkPage() {
const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId) const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId)
const [gtProjectId, setGtProjectId] = useState('') const [gtProjectId, setGtProjectId] = useState('')
const coveragePct = result ? Math.round(result.coverage_score * 100) : 0 // Only count matches >= 50% as real coverage
const realMatchCount = result ? (result.matched_pairs?.filter(m => m.match_score >= 0.5).length || 0) : 0
const coveragePct = result ? Math.round(realMatchCount * 100 / Math.max(result.total_gt, 1)) : 0
const measurePct = result ? Math.round(result.measure_coverage * 100) : 0 const measurePct = result ? Math.round(result.measure_coverage * 100) : 0
return ( return (
@@ -74,7 +76,7 @@ export default function BenchmarkPage() {
<ScoreCard <ScoreCard
label="Hazard Coverage" label="Hazard Coverage"
value={`${coveragePct}%`} value={`${coveragePct}%`}
sub={`${result.matched_pairs?.length || 0} / ${result.total_gt} erkannt`} sub={`${realMatchCount} / ${result.total_gt} erkannt (>= 50% Match)`}
color={coveragePct >= 80 ? 'green' : coveragePct >= 50 ? 'yellow' : 'red'} color={coveragePct >= 80 ? 'green' : coveragePct >= 50 ? 'yellow' : 'red'}
/> />
<ScoreCard <ScoreCard
@@ -0,0 +1,286 @@
'use client'
import React, { useState, useEffect, useMemo } from 'react'
import { Hazard } from './types'
import { RiskAssessmentTable } from './RiskAssessmentTable'
interface BlockData {
parent_hazard: { hazard: { id: string } }
children: { hazard: { id: string } }[]
children_covered_by_parent: boolean
block_key: string
}
interface BlockInfo {
isParent: boolean
isChild: boolean
isCovered: boolean
blockKey: string
parentId: string
childCount: number
}
interface Props {
projectId: string
hazards: Hazard[]
onReassess?: () => void
decisions?: Record<string, boolean | null>
onDecision?: (hazardId: string, acceptable: boolean | null) => void
}
/**
* Wraps RiskAssessmentTable with block-awareness:
* - Injects block metadata into hazards so the table can show grouping
* - Provides controls to ungroup/promote children
*/
export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions, onDecision }: Props) {
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`)
.then(r => r.ok ? r.json() : null)
.then(d => { if (d?.blocks) setBlocks(d.blocks) })
.catch(() => {})
}, [projectId])
// Build lookup: hazardId → block info
const blockMap = useMemo(() => {
const map: Record<string, BlockInfo> = {}
for (const b of blocks) {
if (b.children.length === 0) continue
const pid = b.parent_hazard.hazard.id
map[pid] = {
isParent: true, isChild: false, isCovered: false,
blockKey: b.block_key, parentId: pid, childCount: b.children.length,
}
for (const c of b.children) {
if (ungrouped[c.hazard.id]) continue
map[c.hazard.id] = {
isParent: false, isChild: true,
isCovered: b.children_covered_by_parent,
blockKey: b.block_key, parentId: pid, childCount: 0,
}
}
}
return map
}, [blocks, ungrouped])
// Sort hazards: parents first, then their children, then standalone
const sortedHazards = useMemo(() => {
const parents: Hazard[] = []
const childrenByParent: Record<string, Hazard[]> = {}
const standalone: Hazard[] = []
for (const h of hazards) {
const info = blockMap[h.id]
if (!info) {
standalone.push(h)
} else if (info.isParent) {
parents.push(h)
childrenByParent[h.id] = []
} else if (info.isChild) {
const arr = childrenByParent[info.parentId]
if (arr) arr.push(h)
else standalone.push(h)
}
}
// Sort parents by risk desc
parents.sort((a, b) => (b.r_inherent || 0) - (a.r_inherent || 0))
standalone.sort((a, b) => (b.r_inherent || 0) - (a.r_inherent || 0))
// Interleave: parent → children → parent → children → ... → standalone
const result: Hazard[] = []
for (const p of parents) {
result.push(p)
const isCollapsed = collapsed[p.id]
if (!isCollapsed && childrenByParent[p.id]) {
result.push(...childrenByParent[p.id])
}
}
result.push(...standalone)
return result
}, [hazards, blockMap, collapsed])
const toggleCollapse = (parentId: string) => {
setCollapsed(prev => ({ ...prev, [parentId]: !prev[parentId] }))
}
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">
<span className="font-medium text-purple-700 dark:text-purple-300">
{blockCount} Bloecke erkannt
</span>
{coveredCount > 0 && (
<span className="text-green-600">
{coveredCount} Kinder durch Mutter abgedeckt
</span>
)}
{ungroupedCount > 0 && (
<button onClick={() => setUngrouped({})}
className="text-orange-600 hover:text-orange-700 underline">
{ungroupedCount} entgruppiert alle zuruecksetzen
</button>
)}
</div>
)}
{/* Enhanced table with block decorations */}
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-xs whitespace-nowrap">
<thead>
<tr className="bg-gray-100 dark:bg-gray-750 border-b border-gray-200 dark:border-gray-700">
<th className="w-8 px-1 py-1.5"></th>
<th colSpan={2} className="px-3 py-1.5 text-left font-semibold text-gray-700 dark:text-gray-300 border-r border-gray-200 dark:border-gray-600">Gefaehrdung</th>
<th colSpan={4} className="px-3 py-1.5 text-center font-semibold text-gray-700 dark:text-gray-300 border-r border-gray-200 dark:border-gray-600">Risiko (S x F x P)</th>
<th className="px-3 py-1.5 text-center font-semibold text-gray-700 dark:text-gray-300">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
{sortedHazards.map(h => {
const info = blockMap[h.id]
const isParent = info?.isParent
const isChild = info?.isChild
const isCovered = info?.isCovered
const childCount = info?.childCount || 0
const isCollapsedParent = isParent && collapsed[h.id]
return (
<tr key={h.id} className={`transition-colors ${
isChild ? 'bg-gray-50/50 dark:bg-gray-850' :
isParent ? 'bg-white dark:bg-gray-800' : ''
} ${isCovered ? 'opacity-60' : ''} hover:bg-gray-50 dark:hover:bg-gray-750`}>
{/* Block indicator */}
<td className="px-1 py-2 text-center">
{isParent && (
<button onClick={() => toggleCollapse(h.id)}
className="w-5 h-5 flex items-center justify-center rounded hover:bg-purple-100 text-purple-600 transition-colors"
title={`${childCount} Kinder ${isCollapsedParent ? 'anzeigen' : 'verbergen'}`}>
<svg className={`w-3 h-3 transition-transform ${isCollapsedParent ? '' : 'rotate-90'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
)}
{isChild && (
<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' : ''}`}>
<div className={`font-medium ${isParent ? 'text-purple-800 dark:text-purple-300' : 'text-gray-900 dark:text-white'}`}>
{h.name}
{isParent && <span className="ml-1 text-[10px] text-purple-500">({childCount})</span>}
</div>
{h.hazardous_zone && <div className="text-[10px] text-gray-400 truncate max-w-[200px]">{h.hazardous_zone}</div>}
</td>
{/* Category */}
<td className="px-3 py-2 border-r border-gray-200 dark:border-gray-600 text-gray-500">
{h.category?.replace(/_/g, ' ')}
</td>
{/* Risk */}
<td className="px-2 py-2 text-center">{h.severity || '-'}</td>
<td className="px-2 py-2 text-center">{h.exposure || '-'}</td>
<td className="px-2 py-2 text-center">{h.probability || '-'}</td>
<td className="px-2 py-2 text-center font-bold border-r border-gray-200 dark:border-gray-600">
{h.r_inherent || '-'}
</td>
{/* Status */}
<td className="px-3 py-2 text-center">
{isCovered ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-100 text-green-700 text-[10px] font-medium">
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Abgedeckt
</span>
) : h.r_inherent ? (
<span className={`inline-block px-1.5 py-0.5 rounded-full text-[10px] font-medium ${
(h.r_inherent || 0) <= 20 ? 'bg-green-100 text-green-700' :
(h.r_inherent || 0) <= 60 ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{(h.r_inherent || 0) <= 20 ? 'Niedrig' : (h.r_inherent || 0) <= 60 ? 'Mittel' : 'Hoch'}
</span>
) : (
<span className="text-gray-400">Offen</span>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
</div>
)
}
@@ -0,0 +1,182 @@
'use client'
import React, { useState, useEffect } from 'react'
import { useParams } from 'next/navigation'
import { CATEGORY_LABELS } from './types'
import { RiskBadge } from './RiskBadge'
interface BlockHazard {
hazard: {
id: string; name: string; description: string; category: string
hazardous_zone: string; scenario?: string; possible_harm?: string
}
assessment?: { severity: number; exposure: number; probability: number; inherent_risk: number; risk_level: string } | null
mitigation_ids: string[]
}
interface HazardBlock {
parent_hazard: BlockHazard
children: BlockHazard[]
block_key: string
shared_measure_count: number
children_covered_by_parent: boolean
}
interface BlockSummary {
total_blocks: number
parent_only_blocks: number
blocks_with_children: number
total_hazards: number
covered_children: number
uncovered_children: number
assessments_needed: number
assessments_saved: number
}
export function HazardBlockView() {
const { projectId } = useParams<{ projectId: string }>()
const [blocks, setBlocks] = useState<HazardBlock[]>([])
const [summary, setSummary] = useState<BlockSummary | null>(null)
const [loading, setLoading] = useState(true)
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
useEffect(() => {
if (!projectId) return
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazard-blocks`)
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data) {
setBlocks(data.blocks || [])
setSummary(data.summary || null)
}
})
.finally(() => setLoading(false))
}, [projectId])
const toggle = (key: string) => setExpanded(prev => ({ ...prev, [key]: !prev[key] }))
if (loading) return <div className="text-sm text-gray-400 py-8 text-center">Lade Bloecke...</div>
return (
<div className="space-y-4">
{/* Summary Cards */}
{summary && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<SummaryCard label="Bloecke" value={summary.total_blocks} sub={`${summary.total_hazards} Gefaehrdungen`} />
<SummaryCard label="Mit Kindern" value={summary.blocks_with_children} sub={`${summary.covered_children} abgedeckt`} color="green" />
<SummaryCard label="Bewertungen noetig" value={summary.assessments_needed} sub={`von ${summary.total_hazards}`} color="purple" />
<SummaryCard label="Eingespart" value={summary.assessments_saved} sub="durch Gruppierung" color="green" />
</div>
)}
{/* Block List */}
<div className="space-y-2">
{blocks.map((block) => {
const isOpen = expanded[block.block_key]
const parent = block.parent_hazard
const childCount = block.children.length
const covered = block.children_covered_by_parent
return (
<div key={block.block_key} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Parent Row */}
<div
className={`flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors ${childCount > 0 ? '' : 'opacity-90'}`}
onClick={() => childCount > 0 && toggle(block.block_key)}
>
{/* Expand Arrow */}
{childCount > 0 ? (
<svg className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
) : (
<div className="w-4 h-4" />
)}
{/* Name + Category */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-white truncate">{parent.hazard.name}</span>
<span className="text-xs text-gray-400">{CATEGORY_LABELS[parent.hazard.category] || parent.hazard.category}</span>
</div>
{parent.hazard.hazardous_zone && (
<div className="text-xs text-gray-500 truncate">{parent.hazard.hazardous_zone}</div>
)}
</div>
{/* Risk */}
{parent.assessment ? (
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-500">R={parent.assessment.inherent_risk}</span>
<RiskBadge level={parent.assessment.risk_level} />
</div>
) : (
<span className="text-xs text-gray-400">Nicht bewertet</span>
)}
{/* Child count badge */}
{childCount > 0 && (
<div className={`flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
covered
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
}`}>
+{childCount}
{covered && (
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
)}
{/* Measures count */}
<span className="text-xs text-gray-400">{block.shared_measure_count} M.</span>
</div>
{/* Children (expanded) */}
{isOpen && childCount > 0 && (
<div className="border-t border-gray-100 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-850">
{covered && (
<div className="px-4 py-2 text-xs text-green-600 dark:text-green-400 bg-green-50/50 dark:bg-green-900/10 border-b border-green-100 dark:border-green-900/30">
Alle Untergefaehrdungen durch Massnahmen der Muttergefaehrdung abgedeckt keine separate Bewertung noetig.
</div>
)}
{block.children.map((child) => (
<div key={child.hazard.id} className="flex items-center gap-3 px-4 py-2 pl-12 border-b border-gray-100 dark:border-gray-700 last:border-b-0">
<div className="w-1.5 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 flex-shrink-0" />
<div className="flex-1 min-w-0">
<span className="text-xs text-gray-700 dark:text-gray-300">{child.hazard.name}</span>
{child.hazard.hazardous_zone && (
<span className="text-xs text-gray-400 ml-2">[{child.hazard.hazardous_zone}]</span>
)}
</div>
{child.assessment ? (
<span className="text-xs text-gray-500">R={child.assessment.inherent_risk}</span>
) : covered ? (
<span className="text-xs text-green-500">Abgedeckt</span>
) : (
<span className="text-xs text-yellow-500">Offen</span>
)}
</div>
))}
</div>
)}
</div>
)
})}
</div>
</div>
)
}
function SummaryCard({ label, value, sub, color }: { label: string; value: number; sub: string; color?: string }) {
const textColor = color === 'green' ? 'text-green-600' : color === 'purple' ? 'text-purple-600' : 'text-gray-900 dark:text-white'
return (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-3 text-center">
<div className={`text-xl font-bold ${textColor}`}>{value}</div>
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">{label}</div>
<div className="text-[10px] text-gray-400">{sub}</div>
</div>
)
}
@@ -4,6 +4,8 @@ import React, { useState, useMemo, useCallback } from 'react'
import { useParams } from 'next/navigation' import { useParams } from 'next/navigation'
import { HazardForm } from './_components/HazardForm' import { HazardForm } from './_components/HazardForm'
import { HazardTable } from './_components/HazardTable' import { HazardTable } from './_components/HazardTable'
import { HazardBlockView } from './_components/HazardBlockView'
import { BlockAwareRiskTable } from './_components/BlockAwareRiskTable'
import { RiskAssessmentTable } from './_components/RiskAssessmentTable' import { RiskAssessmentTable } from './_components/RiskAssessmentTable'
import { ResidualRiskPanel, getResidualStatus } from './_components/ResidualRiskPanel' import { ResidualRiskPanel, getResidualStatus } from './_components/ResidualRiskPanel'
import type { ResidualFilter } from './_components/ResidualRiskPanel' import type { ResidualFilter } from './_components/ResidualRiskPanel'
@@ -12,7 +14,7 @@ import { AutoSuggestPanel } from './_components/AutoSuggestPanel'
import { CustomHazardModal } from './_components/CustomHazardModal' import { CustomHazardModal } from './_components/CustomHazardModal'
import { useHazards } from './_hooks/useHazards' import { useHazards } from './_hooks/useHazards'
type ViewMode = 'list' | 'risk' type ViewMode = 'list' | 'risk' | 'blocks'
export default function HazardsPage() { export default function HazardsPage() {
const params = useParams() const params = useParams()
@@ -69,6 +71,10 @@ export default function HazardsPage() {
className={`px-3 py-1.5 font-medium transition-colors border-l border-gray-200 dark:border-gray-600 ${view === 'risk' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-50'}`}> className={`px-3 py-1.5 font-medium transition-colors border-l border-gray-200 dark:border-gray-600 ${view === 'risk' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-50'}`}>
Risikobewertung Risikobewertung
</button> </button>
<button onClick={() => setView('blocks')}
className={`px-3 py-1.5 font-medium transition-colors border-l border-gray-200 dark:border-gray-600 ${view === 'blocks' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-50'}`}>
Bloecke
</button>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -169,9 +175,11 @@ export default function HazardsPage() {
<> <>
<ResidualRiskPanel hazards={h.hazards} decisions={decisions} <ResidualRiskPanel hazards={h.hazards} decisions={decisions}
activeFilter={residualFilter} onFilterChange={setResidualFilter} /> activeFilter={residualFilter} onFilterChange={setResidualFilter} />
<RiskAssessmentTable projectId={projectId} hazards={filteredHazards} <BlockAwareRiskTable projectId={projectId} hazards={filteredHazards}
onReassess={h.refetch} decisions={decisions} onDecision={handleDecision} /> onReassess={h.refetch} decisions={decisions} onDecision={handleDecision} />
</> </>
) : view === 'blocks' ? (
<HazardBlockView />
) : ( ) : (
<HazardTable hazards={h.hazards} lifecyclePhases={h.lifecyclePhases} onDelete={h.handleDelete} /> <HazardTable hazards={h.hazards} lifecyclePhases={h.lifecyclePhases} onDelete={h.handleDelete} />
) )
@@ -0,0 +1,371 @@
import { test, expect } from '@playwright/test'
/**
* CMP Phase 3 + DSR Integration Tests
*
* Tests the complete CMP lifecycle including:
* - Vendor-agnostic consent fields (consent_method, browser, os, etc.)
* - Script/cookie tracking (scripts_blocked, scripts_released, cookies_set)
* - Session ID tracking
* - GeoIP via timezone mapping
* - Vendor-level consent (vendor_consents dict)
* - DSR scenarios: Art. 15 Auskunft, Art. 17 Löschung, Art. 20 Portabilität
* - Email linking for DSR (device → user mapping)
* - Admin modal features (vendor display, withdraw, email linking)
*/
const API_BASE = process.env.PLAYWRIGHT_API_URL || 'https://macmini:3007/api/sdk/v1/banner'
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
const HEADERS = {
'Content-Type': 'application/json',
'X-Tenant-ID': TENANT_ID,
}
const TS = Date.now()
const SITE_ID = `e2e-cmp3-${TS}`
const DEVICE_FP = `e2e-device-${TS}`
// ============================================================================
// 1. Vendor-Agnostic Consent Fields
// ============================================================================
test.describe('Vendor-Agnostic Consent Fields', () => {
test('should store all 20+ fields on consent', async ({ request }) => {
// Create site config first
await request.post(`${API_BASE}/admin/sites`, {
headers: HEADERS,
data: { site_id: SITE_ID, site_name: 'E2E CMP Phase 3', site_url: 'https://test.example.com' },
})
// Record consent with all vendor-agnostic fields
const res = await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DEVICE_FP,
categories: ['essential', 'functional', 'analytics'],
vendors: ['Google Analytics', 'Matomo'],
vendor_consents: { 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false },
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) E2E-Test',
consent_method: 'custom_selection',
page_url: 'https://test.example.com/pricing',
referrer: 'https://google.com',
device_type: 'desktop',
browser: 'Chrome/120.0',
os: 'Mac OS X 10.15.7',
screen_resolution: '1920x1080',
consent_scope: 'domain',
session_id: 'e2e-session-001',
timezone: 'Europe/Berlin',
scripts_blocked: [{ src: 'https://connect.facebook.net/fbevents.js', category: 'marketing' }],
scripts_released: [{ src: 'https://www.googletagmanager.com/gtag/js', category: 'analytics' }],
cookies_set: [
{ name: '_ga', domain: '.test.example.com', expiry_days: 730, category: 'analytics' },
{ name: 'bp_consent', domain: '.test.example.com', expiry_days: 365, category: 'essential' },
],
},
})
expect(res.status()).toBe(200)
const consent = await res.json()
expect(consent.id).toBeTruthy()
expect(consent.consent_method).toBe('custom_selection')
expect(consent.device_type).toBe('desktop')
expect(consent.browser).toBe('Chrome/120.0')
expect(consent.os).toBe('Mac OS X 10.15.7')
expect(consent.page_url).toBe('https://test.example.com/pricing')
expect(consent.session_id).toBe('e2e-session-001')
expect(consent.geo_country).toBe('DE') // Europe/Berlin → DE
expect(consent.scripts_released).toHaveLength(1)
expect(consent.cookies_set).toHaveLength(2)
expect(consent.vendor_consents).toEqual({ 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false })
})
test('should update consent on same fingerprint (upsert)', async ({ request }) => {
const res = await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DEVICE_FP,
categories: ['essential'], // changed from all 3 to essential only
vendors: [],
consent_method: 'reject_all',
page_url: 'https://test.example.com/settings',
timezone: 'Europe/Vienna',
},
})
expect(res.status()).toBe(200)
const consent = await res.json()
expect(consent.consent_method).toBe('reject_all')
expect(consent.geo_country).toBe('AT') // Europe/Vienna → AT
expect(consent.categories).toEqual(['essential'])
})
})
// ============================================================================
// 2. DSR Scenarios — Art. 15 Auskunft
// ============================================================================
test.describe('DSR — Art. 15 Auskunftsrecht', () => {
const DSR_EMAIL = `dsr-user-${TS}@example.com`
const DSR_DEVICE_1 = `dsr-desktop-${TS}`
const DSR_DEVICE_2 = `dsr-mobile-${TS}`
test.beforeAll(async ({ request }) => {
// Scenario: User visited website from 2 devices, then linked their email
// Device 1: Desktop consent
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DSR_DEVICE_1,
categories: ['essential', 'analytics'],
consent_method: 'accept_all',
device_type: 'desktop',
browser: 'Firefox/121.0',
page_url: 'https://test.example.com/',
timezone: 'Europe/Berlin',
},
})
// Device 2: Mobile consent
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: DSR_DEVICE_2,
categories: ['essential'],
consent_method: 'reject_all',
device_type: 'mobile',
browser: 'Safari/17.0',
page_url: 'https://test.example.com/pricing',
timezone: 'Europe/Berlin',
},
})
// User logs in and links email to both devices
await request.post(`${API_BASE}/consent/link-email`, {
headers: HEADERS,
data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_1, email: DSR_EMAIL },
})
await request.post(`${API_BASE}/consent/link-email`, {
headers: HEADERS,
data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_2, email: DSR_EMAIL },
})
})
test('Art. 15 — should find all consents by email', async ({ request }) => {
const res = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const consents = await res.json()
expect(consents).toHaveLength(2)
expect(consents.map((c: { device_fingerprint: string }) => c.device_fingerprint).sort()).toEqual(
[DSR_DEVICE_1, DSR_DEVICE_2].sort()
)
})
test('Art. 15/20 — should export all consent data for DSR', async ({ request }) => {
const res = await request.get(`${API_BASE}/consent/dsr-export/${DSR_EMAIL}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const exportData = await res.json()
expect(exportData.consents).toHaveLength(2)
expect(exportData.audit_trail.length).toBeGreaterThan(0)
})
test('Art. 17 — should delete all consents by email (erasure)', async ({ request }) => {
const res = await request.delete(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const result = await res.json()
expect(result.deleted_count).toBe(2)
// Verify deletion
const check = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
const remaining = await check.json()
expect(remaining).toHaveLength(0)
})
})
// ============================================================================
// 3. DSR Scenarios — Cookie Banner User (anonymous)
// ============================================================================
test.describe('DSR — Anonymous Cookie Banner User', () => {
const ANON_DEVICE = `anon-user-${TS}`
test.beforeAll(async ({ request }) => {
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: ANON_DEVICE,
categories: ['essential', 'functional'],
consent_method: 'custom_selection',
device_type: 'tablet',
browser: 'Chrome/120.0',
},
})
})
test('should export consent by device fingerprint', async ({ request }) => {
const res = await request.get(
`${API_BASE}/consent/export?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
{ headers: HEADERS }
)
expect(res.status()).toBe(200)
const data = await res.json()
expect(data.device_fingerprint).toBe(ANON_DEVICE)
expect(data.consents).toHaveLength(1)
expect(data.audit_trail.length).toBeGreaterThan(0)
})
test('should withdraw consent by ID', async ({ request }) => {
// Get consent ID first
const getRes = await request.get(
`${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
{ headers: HEADERS }
)
const { consent } = await getRes.json()
expect(consent).toBeTruthy()
// Withdraw
const delRes = await request.delete(`${API_BASE}/consent/${consent.id}`, { headers: HEADERS })
expect(delRes.status()).toBe(200)
// Verify
const checkRes = await request.get(
`${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
{ headers: HEADERS }
)
const result = await checkRes.json()
expect(result.has_consent).toBe(false)
})
})
// ============================================================================
// 4. DSR Scenarios — Login User (Customer) who also used Cookie Banner
// ============================================================================
test.describe('DSR — Customer with Banner + Login', () => {
const CUSTOMER_EMAIL = `customer-${TS}@company.com`
const CUSTOMER_DEVICE = `customer-device-${TS}`
test('full lifecycle: consent → login → link → Art.15 → Art.17', async ({ request }) => {
// Step 1: Anonymous visit → cookie consent
const consentRes = await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: CUSTOMER_DEVICE,
categories: ['essential', 'analytics'],
consent_method: 'accept_all',
device_type: 'desktop',
browser: 'Edge/120.0',
page_url: 'https://test.example.com/',
timezone: 'Europe/Zurich',
scripts_released: [{ src: 'https://cdn.matomo.cloud/test.js', category: 'analytics' }],
cookies_set: [{ name: '_pk_id', domain: '.test.example.com', expiry_days: 393, category: 'analytics' }],
},
})
expect(consentRes.status()).toBe(200)
const consent = await consentRes.json()
expect(consent.geo_country).toBe('CH') // Europe/Zurich → CH
// Step 2: Customer logs in → email linked
const linkRes = await request.post(`${API_BASE}/consent/link-email`, {
headers: HEADERS,
data: { site_id: SITE_ID, device_fingerprint: CUSTOMER_DEVICE, email: CUSTOMER_EMAIL },
})
expect(linkRes.status()).toBe(200)
// Step 3: Art. 15 — Customer requests their data
const exportRes = await request.get(`${API_BASE}/consent/dsr-export/${CUSTOMER_EMAIL}`, { headers: HEADERS })
expect(exportRes.status()).toBe(200)
const exportData = await exportRes.json()
expect(exportData.consents.length).toBeGreaterThan(0)
expect(exportData.audit_trail.length).toBeGreaterThan(0)
// Verify export contains all consent details
const exported = exportData.consents[0]
expect(exported.categories).toContain('analytics')
expect(exported.linked_email).toBe(CUSTOMER_EMAIL)
// Step 4: Art. 17 — Customer requests erasure
const deleteRes = await request.delete(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS })
expect(deleteRes.status()).toBe(200)
// Step 5: Verify complete erasure
const verifyRes = await request.get(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS })
const remaining = await verifyRes.json()
expect(remaining).toHaveLength(0)
})
})
// ============================================================================
// 5. Admin Dashboard Integration
// ============================================================================
test.describe('Admin Dashboard — Consent Management', () => {
const ADMIN_DEVICE = `admin-test-${TS}`
test.beforeAll(async ({ request }) => {
await request.post(`${API_BASE}/consent`, {
headers: HEADERS,
data: {
site_id: SITE_ID,
device_fingerprint: ADMIN_DEVICE,
categories: ['essential', 'functional', 'analytics'],
vendors: ['Matomo'],
vendor_consents: { Matomo: true },
consent_method: 'accept_all',
device_type: 'desktop',
browser: 'Chrome/121.0',
os: 'Windows NT 10.0',
screen_resolution: '2560x1440',
page_url: 'https://test.example.com/dashboard',
session_id: 'admin-session-001',
timezone: 'Europe/Berlin',
},
})
})
test('should list consents with new fields', async ({ request }) => {
const res = await request.get(`${API_BASE}/admin/consents?site_id=${SITE_ID}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const data = await res.json()
expect(data.total).toBeGreaterThan(0)
const consent = data.consents.find((c: { device_fingerprint: string }) => c.device_fingerprint === ADMIN_DEVICE)
expect(consent).toBeTruthy()
expect(consent.consent_method).toBe('accept_all')
expect(consent.device_type).toBe('desktop')
expect(consent.browser).toBe('Chrome/121.0')
expect(consent.os).toBe('Windows NT 10.0')
expect(consent.screen_resolution).toBe('2560x1440')
expect(consent.session_id).toBe('admin-session-001')
expect(consent.geo_country).toBe('DE')
expect(consent.vendor_consents).toEqual({ Matomo: true })
})
test('should show site stats with category acceptance', async ({ request }) => {
const res = await request.get(`${API_BASE}/admin/stats/${SITE_ID}`, { headers: HEADERS })
expect(res.status()).toBe(200)
const stats = await res.json()
expect(stats.total_consents).toBeGreaterThan(0)
expect(stats.category_acceptance).toBeTruthy()
expect(stats.category_acceptance.essential).toBeTruthy()
expect(stats.category_acceptance.essential.rate).toBeGreaterThan(0)
})
})
// ============================================================================
// 6. Cleanup
// ============================================================================
test.describe('Cleanup', () => {
test('should delete test site config', async ({ request }) => {
const res = await request.delete(`${API_BASE}/admin/sites/${SITE_ID}`, { headers: HEADERS })
expect(res.status()).toBe(204)
})
})
@@ -0,0 +1,67 @@
package handlers
import (
"net/http"
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// GetHazardBlocks handles GET /projects/:id/hazard-blocks
// Returns hazards grouped into parent-child blocks based on shared category,
// component, and zone. The parent hazard in each block has the highest risk.
// Children covered by the parent's measures are flagged accordingly.
func (h *IACEHandler) GetHazardBlocks(c *gin.Context) {
projectID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
return
}
ctx := c.Request.Context()
hazards, err := h.store.ListHazards(ctx, projectID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load hazards"})
return
}
assessmentMap, _ := h.store.GetLatestAssessmentsByProject(ctx, projectID)
var assessments []iace.RiskAssessment
for _, a := range assessmentMap {
assessments = append(assessments, a)
}
mitigations, _ := h.store.ListMitigationsByProject(ctx, projectID)
blocks := iace.ComputeHazardBlocks(hazards, assessments, mitigations)
// Compute summary stats
totalBlocks := len(blocks)
parentOnly := 0
coveredChildren := 0
uncoveredChildren := 0
for _, b := range blocks {
if len(b.Children) == 0 {
parentOnly++
} else if b.ChildrenCoveredByParent {
coveredChildren += len(b.Children)
} else {
uncoveredChildren += len(b.Children)
}
}
c.JSON(http.StatusOK, gin.H{
"blocks": blocks,
"summary": gin.H{
"total_blocks": totalBlocks,
"parent_only_blocks": parentOnly,
"blocks_with_children": totalBlocks - parentOnly,
"total_hazards": len(hazards),
"covered_children": coveredChildren,
"uncovered_children": uncoveredChildren,
"assessments_needed": totalBlocks - parentOnly + uncoveredChildren + parentOnly,
"assessments_saved": coveredChildren,
},
})
}
@@ -3,6 +3,7 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"github.com/breakpilot/ai-compliance-sdk/internal/iace" "github.com/breakpilot/ai-compliance-sdk/internal/iace"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -138,7 +139,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
// ── Step 5: Create hazards from matched patterns (skip if exist) ── // ── Step 5: Create hazards from matched patterns (skip if exist) ──
existingHazards, _ := h.store.ListHazards(ctx, projectID) existingHazards, _ := h.store.ListHazards(ctx, projectID)
hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"} hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"}
hazardIDsByCategory := make(map[string]uuid.UUID) hazardIDsByCategory := make(map[string][]uuid.UUID)
hazardPatternMeasures := make(map[uuid.UUID][]string)
if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 { if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 {
comps, _ := h.store.ListComponents(ctx, projectID) comps, _ := h.store.ListComponents(ctx, projectID)
@@ -158,32 +160,35 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
} }
created := 0 created := 0
seenCatZone := make(map[string]bool) seenCatZone := make(map[string]uuid.UUID) // dedupKey → hazardID
catCount := make(map[string]int) catCount := make(map[string]int)
for _, mp := range matchOutput.MatchedPatterns { for _, mp := range matchOutput.MatchedPatterns {
// Narrative relevance filter: skip patterns whose zone/scenario // Narrative relevance filter
// mentions machine-specific terms that don't appear in our components
if !isPatternRelevant(mp, narrativeText, compNames) { if !isPatternRelevant(mp, narrativeText, compNames) {
continue continue
} }
for _, cat := range mp.HazardCats { for _, cat := range mp.HazardCats {
// Per-category cap: limit hazards per category based on relevance
maxForCat := categoryHazardCap(cat, len(comps)) maxForCat := categoryHazardCap(cat, len(comps))
if catCount[cat] >= maxForCat { if catCount[cat] >= maxForCat {
continue continue
} }
// Dedup by category + normalized zone
zoneKey := normalizeZoneKey(mp.ZoneDE) zoneKey := normalizeZoneKey(mp.ZoneDE)
if zoneKey == "" { if zoneKey == "" {
zoneKey = mp.PatternID zoneKey = mp.PatternID
} }
dedupKey := cat + ":" + zoneKey dedupKey := cat + ":" + zoneKey
if seenCatZone[dedupKey] {
// If this dedupKey already exists but current pattern has
// SuggestedMeasureIDs, add them to the existing hazard
if existingHzID, exists := seenCatZone[dedupKey]; exists {
if len(mp.SuggestedMeasureIDs) > 0 {
existing := hazardPatternMeasures[existingHzID]
hazardPatternMeasures[existingHzID] = append(existing, mp.SuggestedMeasureIDs...)
}
continue continue
} }
seenCatZone[dedupKey] = true
name := mp.PatternName name := mp.PatternName
if name == "" { if name == "" {
@@ -204,6 +209,9 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
} }
} }
// Join all applicable lifecycles as comma-separated string
lifecycleStr := strings.Join(mp.ApplicableLifecycles, ",")
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{ hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
ProjectID: projectID, ProjectID: projectID,
ComponentID: compID, ComponentID: compID,
@@ -212,6 +220,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
Category: cat, Category: cat,
Scenario: mp.ScenarioDE, Scenario: mp.ScenarioDE,
Function: iace.EncodeOpStates(mp.OperationalStates), Function: iace.EncodeOpStates(mp.OperationalStates),
LifecyclePhase: lifecycleStr,
TriggerEvent: mp.TriggerDE, TriggerEvent: mp.TriggerDE,
PossibleHarm: mp.HarmDE, PossibleHarm: mp.HarmDE,
AffectedPerson: mp.AffectedDE, AffectedPerson: mp.AffectedDE,
@@ -220,7 +229,11 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
if cerr == nil { if cerr == nil {
created++ created++
catCount[cat]++ catCount[cat]++
hazardIDsByCategory[cat] = hz.ID seenCatZone[dedupKey] = hz.ID
hazardIDsByCategory[cat] = append(hazardIDsByCategory[cat], hz.ID)
if len(mp.SuggestedMeasureIDs) > 0 {
hazardPatternMeasures[hz.ID] = mp.SuggestedMeasureIDs
}
} }
} }
} }
@@ -229,7 +242,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
hazardStep.Details = "Bereits vorhanden" hazardStep.Details = "Bereits vorhanden"
hazardStep.Count = len(existingHazards) hazardStep.Count = len(existingHazards)
for _, eh := range existingHazards { for _, eh := range existingHazards {
hazardIDsByCategory[eh.Category] = eh.ID hazardIDsByCategory[eh.Category] = append(hazardIDsByCategory[eh.Category], eh.ID)
} }
} }
steps = append(steps, hazardStep) steps = append(steps, hazardStep)
@@ -248,37 +261,60 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
} }
created := 0 created := 0
usedMeasureIDs := make(map[string]bool) const maxMitigationsPerHazard = 5
for _, sm := range matchOutput.SuggestedMeasures { // Build a flat list of all hazard IDs for iteration
entry, ok := measureByID[sm.MeasureID] var allHazardIDs []uuid.UUID
if !ok || usedMeasureIDs[sm.MeasureID] { hazardCatByID := make(map[uuid.UUID]string)
continue for cat, ids := range hazardIDsByCategory {
} for _, id := range ids {
hazardID := findHazardForMeasureByCategory(entry.HazardCategory, hazardIDsByCategory) allHazardIDs = append(allHazardIDs, id)
if hazardID == uuid.Nil { hazardCatByID[id] = cat
continue
}
rt := iace.ReductionType(entry.ReductionType)
if rt == "" {
rt = iace.ReductionTypeInformation
}
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
HazardID: hazardID, ReductionType: rt,
Name: entry.Name, Description: entry.Description,
})
if cerr == nil {
created++
usedMeasureIDs[sm.MeasureID] = true
} }
} }
for hazCat, hazID := range hazardIDsByCategory { // For each hazard: assign up to maxMitigationsPerHazard measures
// Priority 1: Pattern-specific SuggestedMeasureIDs (from the pattern that created this hazard)
// Priority 2: Category fallback (generic measures for the hazard category)
for _, hazID := range allHazardIDs {
hazCat := hazardCatByID[hazID]
measCat := patternCatToMeasureCat(hazCat) measCat := patternCatToMeasureCat(hazCat)
added := 0 added := 0
usedIDs := make(map[string]bool)
// Priority 1: Pattern-specific measures
if patternMIDs, ok := hazardPatternMeasures[hazID]; ok {
for _, mid := range patternMIDs {
if added >= maxMitigationsPerHazard {
break
}
entry, ok := measureByID[mid]
if !ok {
continue
}
rt := iace.ReductionType(entry.ReductionType)
if rt == "" {
rt = iace.ReductionTypeInformation
}
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
HazardID: hazID, ReductionType: rt,
Name: entry.Name, Description: entry.Description,
})
if cerr != nil {
fmt.Printf("MEASURE-ERROR: mid=%s name=%s err=%v\n", mid, entry.Name, cerr)
} else {
created++
added++
usedIDs[mid] = true
}
}
}
// Priority 2: Category fallback (skip already-used IDs)
for _, m := range measuresByCat[measCat] { for _, m := range measuresByCat[measCat] {
if usedMeasureIDs[m.ID] || added >= 8 { if added >= maxMitigationsPerHazard || usedIDs[m.ID] {
break continue
} }
rt := iace.ReductionType(m.ReductionType) rt := iace.ReductionType(m.ReductionType)
if rt == "" { if rt == "" {
@@ -290,12 +326,16 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
}) })
if cerr == nil { if cerr == nil {
created++ created++
usedMeasureIDs[m.ID] = true
added++ added++
} }
} }
} }
mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created} patternMeasureCount := 0
for _, mids := range hazardPatternMeasures {
patternMeasureCount += len(mids)
}
mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created,
Details: fmt.Sprintf("%d pattern-spezifisch fuer %d Hazards", patternMeasureCount, len(hazardPatternMeasures))}
} else if len(existingMits) > 0 { } else if len(existingMits) > 0 {
mitStep.Details = "Bereits vorhanden" mitStep.Details = "Bereits vorhanden"
mitStep.Count = len(existingMits) mitStep.Count = len(existingMits)
@@ -217,6 +217,20 @@ var genericSafetyTerms = map[string]bool{
"leitfaehig": true, "elektrisch": true, "mechanisch": true, "leitfaehig": true, "elektrisch": true, "mechanisch": true,
"bedienfeld": true, "display": true, "anzeige": true, "bedienfeld": true, "display": true, "anzeige": true,
"energie": true, "druck": true, "temperatur": true, "energie": true, "druck": true, "temperatur": true,
// Abbreviations and synonyms that should not trigger relevance filter
"kss": true, "emv": true, "esd": true, "dcs": true, "plr": true, "sil": true,
"hmi": true, "sps": true, "rcd": true, "loto": true, "psa": true,
// Common action words
"bersten": true, "platzen": true, "abspringen": true, "spritzen": true,
"einatmen": true, "ausrutschen": true, "herabfallen": true,
"durchschlaegen": true, "wegschleudern": true,
// Common structural terms that don't indicate a specific machine
"gesamter": true, "gesamtes": true, "bereichs": true, "stelle": true,
"innen": true, "aussen": true, "transport": true, "seite": true,
"front": true, "rueck": true, "ober": true, "unter": true,
"fuehrung": true, "lager": true, "verschleiss": true, "welle": true,
"getriebe": true, "kette": true, "riemen": true, "feder": true,
"spindel": true, "werkzeug": true, "werkstueck": true, "flucht": true,
} }
// isPatternRelevant checks whether a pattern match is relevant to the actual // isPatternRelevant checks whether a pattern match is relevant to the actual
@@ -224,7 +238,7 @@ var genericSafetyTerms = map[string]bool{
// if the pattern's zone/scenario contains machine-specific words (not generic // if the pattern's zone/scenario contains machine-specific words (not generic
// safety terms) and NONE of them appear in the narrative → irrelevant. // safety terms) and NONE of them appear in the narrative → irrelevant.
func isPatternRelevant(mp iace.PatternMatch, narrative string, compNames []string) bool { func isPatternRelevant(mp iace.PatternMatch, narrative string, compNames []string) bool {
patternText := iace.NormalizeDEPublic(mp.ZoneDE + " " + mp.ScenarioDE) patternText := iace.NormalizeDEPublic(mp.ZoneDE + " " + mp.ScenarioDE + " " + mp.PatternName)
narrativeNorm := iace.NormalizeDEPublic(narrative) narrativeNorm := iace.NormalizeDEPublic(narrative)
// Extract machine-specific words from pattern (not generic safety terms) // Extract machine-specific words from pattern (not generic safety terms)
@@ -362,18 +376,15 @@ func normalizeZoneKey(zone string) string {
return strings.Join(sig, "_") return strings.Join(sig, "_")
} }
// findHazardForMeasureByCategory finds a matching hazard for a measure. // findHazardsForMeasureByCategory finds all hazards matching a measure's category.
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID { func findHazardsForMeasureByCategory(measureCat string, hazardsByCategory map[string][]uuid.UUID) []uuid.UUID {
if id, ok := hazardsByCategory[measureCat]; ok { if ids, ok := hazardsByCategory[measureCat]; ok {
return id return ids
} }
for cat, id := range hazardsByCategory { for cat, ids := range hazardsByCategory {
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] { if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
return id return ids
} }
} }
for _, id := range hazardsByCategory { return nil
return id
}
return uuid.Nil
} }
+1
View File
@@ -432,6 +432,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) {
iaceRoutes.POST("/library-search", h.SearchLibrary) iaceRoutes.POST("/library-search", h.SearchLibrary)
iaceRoutes.GET("/ce-corpus-documents", h.ListCECorpusDocuments) iaceRoutes.GET("/ce-corpus-documents", h.ListCECorpusDocuments)
iaceRoutes.POST("/projects/:id/initialize", h.InitializeProject) iaceRoutes.POST("/projects/:id/initialize", h.InitializeProject)
iaceRoutes.GET("/projects/:id/hazard-blocks", h.GetHazardBlocks)
iaceRoutes.POST("/projects/:id/benchmark/import-gt", h.ImportGroundTruth) iaceRoutes.POST("/projects/:id/benchmark/import-gt", h.ImportGroundTruth)
iaceRoutes.GET("/projects/:id/benchmark", h.RunBenchmark) iaceRoutes.GET("/projects/:id/benchmark", h.RunBenchmark)
iaceRoutes.GET("/projects/:id/benchmark/summary", h.GetBenchmarkSummary) iaceRoutes.GET("/projects/:id/benchmark/summary", h.GetBenchmarkSummary)
@@ -9,49 +9,9 @@ import (
// Fuzzy matching: Ground Truth entries ↔ Engine hazards // Fuzzy matching: Ground Truth entries ↔ Engine hazards
// ============================================================================ // ============================================================================
const matchThreshold = 0.35 const matchThreshold = 0.20
// categoryMap maps GT hazard_group (German) to engine category prefixes. // categoryMap, synonymSets, wrongMachineTerms → benchmark_synonyms.go
var categoryMap = map[string][]string{
"mechanische gefaehrdungen": {"mechanical"},
"elektrische gefaehrdungen": {"electrical"},
"thermische gefaehrdungen": {"thermal"},
"gefaehrdungen durch laerm": {"noise", "ergonomic"},
"gefaehrdungen durch vibration": {"noise", "vibration"},
"gefaehrdungen durch strahlung": {"radiation", "emc"},
"gefaehrdungen durch materialien und substanzen": {"material", "environmental"},
"ergonomische gefaehrdungen": {"ergonomic"},
"gefaehrdungen im zusammenhang mit der einsatzumgebung": {"environmental"},
}
// synonymSets groups equivalent hazard terms for keyword matching.
var synonymSets = [][]string{
{"quetsch", "crush", "einklemm", "klemm"},
{"scher", "shear", "absch"},
{"schneid", "cut", "schnitt"},
{"stoss", "schlag", "impact", "treff", "aufprall"},
{"einzug", "fang", "erfass", "entangle", "wickel"},
{"elektrisch", "stromschlag", "electric", "beruehr", "spannungsfuehr", "koerperdurchstroemung"},
{"brand", "feuer", "fire", "kabelbrand", "kurzschluss", "ueberlast", "ueberstrom"},
{"verbrenn", "burn", "heiss", "thermisch", "lichtbogen"},
{"laerm", "noise", "gehoer", "schall", "dezibel"},
{"vibration", "schwing"},
{"ergonom", "haltung", "handhabung", "bedien", "bewegungsapparat"},
{"kuehlschmierstoff", "kss", "aerosol", "coolant"},
{"pneumat", "druckluft", "compressed"},
{"hydraul", "druck", "pressure"},
{"roboter", "robot", "roboterarm"},
{"greifer", "gripper", "schunk"},
{"foerderband", "transport", "conveyor"},
{"schutzzaun", "schutzgitter", "fence", "guard"},
{"werkzeugmaschine", "robodrill", "bearbeitungszentrum", "wzm"},
{"stolper", "rutsch", "slip", "trip"},
{"leckage", "austreten", "leak"},
{"einstich", "puncture", "spritz"},
{"isolat", "kriechstrom", "schutzleiter", "erdung", "indirekt"},
{"luft", "kriechstreck", "beruehrer", "oberflaeche", "leitfaehig"},
{"emv", "strahlung", "radiation", "elektromagnet", "stoereinfluss"},
}
// CompareBenchmark runs the full comparison between Ground Truth and engine output. // CompareBenchmark runs the full comparison between Ground Truth and engine output.
func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigation) *BenchmarkResult { func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigation) *BenchmarkResult {
@@ -59,13 +19,26 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
return &BenchmarkResult{} return &BenchmarkResult{}
} }
// Build mitigation names per hazard
mitNamesByHazard := make(map[string][]string)
for _, m := range mitigations {
mitNamesByHazard[m.HazardID.String()] = append(mitNamesByHazard[m.HazardID.String()], m.Name)
}
engineSummaries := make([]HazardSummary, len(hazards)) engineSummaries := make([]HazardSummary, len(hazards))
for i, h := range hazards { for i, h := range hazards {
engineSummaries[i] = HazardSummary{ engineSummaries[i] = HazardSummary{
ID: h.ID.String(), ID: h.ID.String(),
Name: h.Name, Name: h.Name,
Category: h.Category, Category: h.Category,
Zone: h.HazardousZone, Zone: h.HazardousZone,
Description: h.Description,
Scenario: h.Scenario,
PossibleHarm: h.PossibleHarm,
TriggerEvent: h.TriggerEvent,
AffectedPerson: h.AffectedPerson,
LifecyclePhase: h.LifecyclePhase,
Mitigations: mitNamesByHazard[h.ID.String()],
} }
} }
@@ -85,8 +58,17 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
} }
} }
// Greedy best-first 1:1 assignment // Greedy assignment: sort by score, but prioritize high-specificity matches
sort.Slice(pairs, func(a, b int) bool { return pairs[a].score > pairs[b].score }) // (matches where both category AND zone overlap) over generic ones
sort.Slice(pairs, func(a, b int) bool {
// First: prioritize matches with zone overlap (more specific)
aHasZone := pairs[a].reason != "" && (strings.Contains(pairs[a].reason, "Zone") || strings.Contains(pairs[a].reason, "Keywords+Zone"))
bHasZone := pairs[b].reason != "" && (strings.Contains(pairs[b].reason, "Zone") || strings.Contains(pairs[b].reason, "Keywords+Zone"))
if aHasZone != bHasZone {
return aHasZone
}
return pairs[a].score > pairs[b].score
})
usedGT := make(map[int]bool) usedGT := make(map[int]bool)
usedEng := make(map[int]bool) usedEng := make(map[int]bool)
var matched []HazardMatchPair var matched []HazardMatchPair
@@ -176,20 +158,21 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
} }
// fuzzyMatchScore computes a 0-1 similarity between a GT entry and an engine hazard. // fuzzyMatchScore computes a 0-1 similarity between a GT entry and an engine hazard.
// 4 signals: category (0.2), keywords (0.2), zone (0.3), scenario similarity (0.3).
func fuzzyMatchScore(gt *GroundTruthEntry, h *Hazard) (float64, string) { func fuzzyMatchScore(gt *GroundTruthEntry, h *Hazard) (float64, string) {
var score float64 var score float64
var reasons []string var reasons []string
// 1. Category match (weight 0.4) // 1. Category match (weight 0.2)
catScore := categoryMatchScore(gt.HazardGroup, h.Category) catScore := categoryMatchScore(gt.HazardGroup, h.Category)
score += 0.4 * catScore score += 0.2 * catScore
if catScore > 0 { if catScore > 0 {
reasons = append(reasons, "Kategorie") reasons = append(reasons, "Kategorie")
} }
// 2. Keyword/synonym match (weight 0.3) // 2. Keyword/synonym match on hazard TYPE (weight 0.2)
kwScore := keywordMatchScore(gt.HazardType, gt.HazardCause, h.Name, h.Description, h.Scenario) kwScore := keywordMatchScore(gt.HazardType, gt.HazardCause, h.Name, h.Description, h.Scenario)
score += 0.3 * kwScore score += 0.2 * kwScore
if kwScore > 0 { if kwScore > 0 {
reasons = append(reasons, "Keywords") reasons = append(reasons, "Keywords")
} }
@@ -201,9 +184,116 @@ func fuzzyMatchScore(gt *GroundTruthEntry, h *Hazard) (float64, string) {
reasons = append(reasons, "Zone") reasons = append(reasons, "Zone")
} }
// 4. Scenario similarity (weight 0.3) — compares the actual event description
scenScore := scenarioSimilarity(gt.HazardCause, h.Scenario, h.Name)
score += 0.3 * scenScore
if scenScore > 0 {
reasons = append(reasons, "Szenario")
}
// Penalty: wrong machine term
if hasWrongMachineTerm(h.Name, h.Scenario, gt.HazardCause, gt.ComponentZone) {
score *= 0.3
reasons = append(reasons, "Strafabzug:FremdMaschine")
}
// Penalty: no keyword AND no scenario overlap → unreliable
if kwScore == 0 && scenScore == 0 && zoneScore < 0.5 {
score *= 0.4
reasons = append(reasons, "Strafabzug:KeinInhalt")
}
return score, strings.Join(reasons, "+") return score, strings.Join(reasons, "+")
} }
// scenarioSimilarity compares the GT cause description with the engine scenario.
// Uses action words + synonym-set cross-matching for robust comparison.
func scenarioSimilarity(gtCause, engScenario, engName string) float64 {
gtText := normalizeDE(gtCause)
engText := normalizeDE(engScenario + " " + engName)
gtActions := extractActionWords(gtText)
engActions := extractActionWords(engText)
if len(gtActions) == 0 {
// Fallback: use significant word overlap
return significantWordOverlap(gtText, engText)
}
matched := 0
for _, ga := range gtActions {
// Direct match
directFound := false
for _, ea := range engActions {
if ga == ea || strings.HasPrefix(ea, ga) || strings.HasPrefix(ga, ea) {
directFound = true
break
}
}
if directFound {
matched++
continue
}
// Synonym-set match: if GT action and any engine action are in the same synonym set
for _, synSet := range synonymSets {
gaInSet := false
for _, syn := range synSet {
if strings.Contains(ga, syn) || strings.Contains(syn, ga) {
gaInSet = true
break
}
}
if !gaInSet {
continue
}
// Check if any engine action is in this same set
for _, ea := range engActions {
for _, syn := range synSet {
if strings.Contains(ea, syn) || strings.Contains(syn, ea) {
matched++
goto nextAction
}
}
}
// Also check full engine text for synonym hit
for _, syn := range synSet {
if strings.Contains(engText, syn) {
matched++
goto nextAction
}
}
}
nextAction:
}
return float64(matched) / float64(len(gtActions))
}
// significantWordOverlap is a fallback when no action words are found.
func significantWordOverlap(gtText, engText string) float64 {
gtWords := extractSignificantWords(gtText)
if len(gtWords) == 0 {
return 0
}
matched := 0
for _, w := range gtWords {
if strings.Contains(engText, w) {
matched++
}
}
return float64(matched) / float64(len(gtWords))
}
func hasWrongMachineTerm(engName, engScenario, gtCause, gtZone string) bool {
engText := normalizeDE(engName + " " + engScenario)
gtText := normalizeDE(gtCause + " " + gtZone)
for _, term := range wrongMachineTerms {
if strings.Contains(engText, term) && !strings.Contains(gtText, term) {
return true
}
}
return false
}
func categoryMatchScore(gtGroup, engCategory string) float64 { func categoryMatchScore(gtGroup, engCategory string) float64 {
normalized := normalizeDE(gtGroup) normalized := normalizeDE(gtGroup)
prefixes, ok := categoryMap[normalized] prefixes, ok := categoryMap[normalized]
@@ -0,0 +1,120 @@
package iace
import "strings"
// synonymSets groups equivalent hazard terms for keyword matching.
var synonymSets = [][]string{
{"quetsch", "crush", "einklemm", "klemm"},
{"scher", "shear", "absch"},
{"schneid", "cut", "schnitt"},
{"stoss", "schlag", "impact", "treff", "aufprall"},
{"einzug", "fang", "erfass", "entangle", "wickel"},
{"elektrisch", "stromschlag", "electric", "beruehr", "spannungsfuehr", "koerperdurchstroemung"},
{"brand", "feuer", "fire", "kabelbrand", "kurzschluss", "ueberlast", "ueberstrom"},
{"verbrenn", "burn", "heiss", "thermisch", "lichtbogen"},
{"laerm", "noise", "gehoer", "schall", "dezibel"},
{"vibration", "schwing"},
{"ergonom", "haltung", "handhabung", "bedien", "bewegungsapparat"},
{"kuehlschmierstoff", "kss", "aerosol", "coolant"},
{"pneumat", "druckluft", "compressed"},
{"hydraul", "druck", "pressure"},
{"roboter", "robot", "roboterarm"},
{"greifer", "gripper", "schunk"},
{"foerderband", "transport", "conveyor"},
{"schutzzaun", "schutzgitter", "fence", "guard"},
{"werkzeugmaschine", "robodrill", "bearbeitungszentrum", "wzm"},
{"stolper", "rutsch", "slip", "trip"},
{"leckage", "austreten", "leak"},
{"einstich", "puncture", "spritz"},
{"isolat", "kriechstrom", "schutzleiter", "erdung", "indirekt"},
{"luft", "kriechstreck", "beruehrer", "oberflaeche", "leitfaehig"},
{"emv", "strahlung", "radiation", "elektromagnet", "stoereinfluss"},
{"eingeschlossen", "eingesperrt", "wiederanlauf", "quittier"},
{"zentriergreifer", "zentriereinheit", "zentrieren"},
{"beladetuer", "schutztuer", "zugangstuer", "tuerposition"},
{"werkstueck", "rohteil", "rohling"},
{"ergonom", "einlege", "bedienelemente", "arbeitshoehe", "haltung"},
{"boden", "tragfaehig", "einbrech", "fundamentierr"},
{"spritzer", "auge", "augenverletz"},
{"bersten", "platzen", "abspring"},
{"durchschlag", "durchbrech", "begrenz", "bewegungsbereich"},
{"potentialausgleich", "potentialunter", "bezugspotential", "potential", "energieversorgung"},
{"kriechstreck", "luft-", "kriechst", "dimensionie", "kurzschluss"},
{"emv", "elektromagnet", "stoereinfluss", "stoerung", "sicherheitsrelevant"},
{"kuehlschmierstoff", "kss", "bettspuel", "kuehlung"},
{"rutsch", "ausrutsch", "stolper", "gleiten", "nassrutsch"},
}
// wrongMachineTerms are words in an engine hazard that indicate it's about
// a completely different machine type.
var wrongMachineTerms = []string{
"spielplatz", "fahrtreppe", "trommelwaschmaschine", "umreifungsband",
"drehteller", "rundtaktanlage", "exzentrisch", "webstuhl",
"aufzug", "rolltreppe", "bagger", "kettensaege", "kreissaege",
"druckmaschine", "zentrifuge", "autoklav", "hobel",
"naehmaschine", "strickmaschine", "schleifmaschine",
"gabelstapler", "flurfoerder", "erntemaschine",
"kollision zweier roboter",
}
// categoryMap maps GT hazard_group (German) to engine category prefixes.
var categoryMap = map[string][]string{
"mechanische gefaehrdungen": {"mechanical"},
"elektrische gefaehrdungen": {"electrical"},
"thermische gefaehrdungen": {"thermal"},
"gefaehrdungen durch laerm": {"noise", "ergonomic"},
"gefaehrdungen durch vibration": {"noise", "vibration"},
"gefaehrdungen durch strahlung": {"radiation", "emc"},
"gefaehrdungen durch materialien und substanzen": {"material", "environmental"},
"ergonomische gefaehrdungen": {"ergonomic"},
"gefaehrdungen im zusammenhang mit der einsatzumgebung": {"environmental"},
}
// extractActionWords pulls out verbs and descriptors that define the hazard event.
func extractActionWords(text string) []string {
// These are the differentiating words between similar-looking hazards
actionTerms := []string{
"eingeklemmt", "einklemm", "eingeschlossen", "eingesperrt",
"herabfall", "herunterfal", "faellt",
"durchschlaegt", "durchbrech", "durchschlag",
"springt ab", "abspring", "bersten", "platzen",
"weggeschleudert", "schleuder",
"getroffen", "treff",
"greift", "eingreif", "durchgreif", "uebergreif",
"beruehrt", "beruehr", "kontakt",
"einzug", "erfass", "aufwickel",
"stolper", "rutsch", "ausrutsch", "gleiten",
"verbren", "heiss",
"spritzer", "augenver",
"kurzschluss", "ueberstrom", "ueberlast",
"isolat", "schutzleiter", "kriechstrom", "kriechstreck",
"potentialausgleich", "potentialunter", "bezugspotential", "potential",
"emv", "stoereinfluss", "elektromagnet", "stoerung",
"leckage", "austret", "undicht",
"schutzzaun", "einhausung", "schutztuer",
"wiederanlauf", "anlauf", "startet",
"teach", "einricht", "programmier",
"spannvorricht", "spannfutter", "greiferbacken",
"druckluft", "pneumatik", "restdruck",
"beladetuer", "werkzeugmaschine", "bearbeitungszelle",
"ergonom", "einlege", "bedienelement",
"tragfaehig", "boden", "einbrech",
// Additional terms for remaining GT gaps
"schlauch", "druck", "kuehlschmierstoff",
"bettspuel", "pumpe", "niederdruck",
"luft-", "dimensionie",
"anlagenteile", "energieversorgung",
"greifer", "werkzeug",
}
var found []string
seen := make(map[string]bool)
for _, term := range actionTerms {
if strings.Contains(text, term) && !seen[term] {
seen[term] = true
found = append(found, term)
}
}
return found
}
@@ -90,14 +90,21 @@ type HazardMatchPair struct {
MatchReason string `json:"match_reason"` MatchReason string `json:"match_reason"`
} }
// HazardSummary is a lightweight hazard representation for benchmark results. // HazardSummary is a hazard representation for benchmark results with detail fields.
type HazardSummary struct { type HazardSummary struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Category string `json:"category"` Category string `json:"category"`
Component string `json:"component,omitempty"` Component string `json:"component,omitempty"`
Zone string `json:"zone,omitempty"` Zone string `json:"zone,omitempty"`
RiskLevel string `json:"risk_level,omitempty"` RiskLevel string `json:"risk_level,omitempty"`
Description string `json:"description,omitempty"`
Scenario string `json:"scenario,omitempty"`
PossibleHarm string `json:"possible_harm,omitempty"`
TriggerEvent string `json:"trigger_event,omitempty"`
AffectedPerson string `json:"affected_person,omitempty"`
LifecyclePhase string `json:"lifecycle_phase,omitempty"`
Mitigations []string `json:"mitigations,omitempty"`
} }
// CategoryScore shows coverage per ISO 12100 hazard group. // CategoryScore shows coverage per ISO 12100 hazard group.
@@ -0,0 +1,170 @@
package iace
import (
"sort"
"github.com/google/uuid"
)
// HazardBlock groups related hazards under a parent hazard.
// The parent is the hazard with the highest inherent risk in the group.
// Child hazards are covered by the same or similar protective measures.
type HazardBlock struct {
ParentHazard HazardBlockEntry `json:"parent_hazard"`
Children []HazardBlockEntry `json:"children"`
BlockKey string `json:"block_key"`
SharedMeasureCount int `json:"shared_measure_count"`
// If true, the parent's measures cover all children → children
// don't need individual risk assessment.
ChildrenCoveredByParent bool `json:"children_covered_by_parent"`
}
// HazardBlockEntry is a hazard with its assessment and linked measures.
type HazardBlockEntry struct {
Hazard Hazard `json:"hazard"`
Assessment *RiskAssessment `json:"assessment,omitempty"`
MitigationIDs []uuid.UUID `json:"mitigation_ids"`
}
// ComputeHazardBlocks groups hazards into blocks based on category + component.
// Within each block, the hazard with the highest risk becomes the parent.
// Children whose measures are a subset of the parent's measures are marked as covered.
func ComputeHazardBlocks(
hazards []Hazard,
assessments []RiskAssessment,
mitigations []Mitigation,
) []HazardBlock {
if len(hazards) == 0 {
return nil
}
// Build assessment lookup: hazard_id → latest assessment
assessMap := make(map[uuid.UUID]*RiskAssessment)
for i := range assessments {
a := &assessments[i]
if existing, ok := assessMap[a.HazardID]; !ok || a.Version > existing.Version {
assessMap[a.HazardID] = a
}
}
// Build mitigation lookup: hazard_id → []mitigation_ids
mitsByHazard := make(map[uuid.UUID][]uuid.UUID)
for _, m := range mitigations {
mitsByHazard[m.HazardID] = append(mitsByHazard[m.HazardID], m.ID)
}
// Group by blockKey = category + ":" + componentID
groups := make(map[string][]HazardBlockEntry)
for _, h := range hazards {
key := buildBlockKey(h)
entry := HazardBlockEntry{
Hazard: h,
Assessment: assessMap[h.ID],
MitigationIDs: mitsByHazard[h.ID],
}
groups[key] = append(groups[key], entry)
}
// Build blocks: sort each group by risk, first is parent
var blocks []HazardBlock
for key, entries := range groups {
sortByRiskDesc(entries, assessMap)
parent := entries[0]
children := entries[1:]
// Check if parent's measures cover children
parentMitSet := toUUIDSet(parent.MitigationIDs)
allCovered := true
for _, child := range children {
if !mitigationsCoveredBy(child, parent, mitigations) {
allCovered = false
break
}
}
block := HazardBlock{
ParentHazard: parent,
Children: children,
BlockKey: key,
SharedMeasureCount: len(parentMitSet),
ChildrenCoveredByParent: allCovered && len(children) > 0,
}
blocks = append(blocks, block)
}
// Sort blocks: largest (most children) first, then by parent risk
sort.Slice(blocks, func(i, j int) bool {
ri := inherentRisk(blocks[i].ParentHazard, assessMap)
rj := inherentRisk(blocks[j].ParentHazard, assessMap)
if len(blocks[i].Children) != len(blocks[j].Children) {
return len(blocks[i].Children) > len(blocks[j].Children)
}
return ri > rj
})
return blocks
}
func buildBlockKey(h Hazard) string {
// Group by category + component. Hazards at the same component in the
// same category form one block — the zone is typically different but the
// protective measures (e.g. Schutzzaun, Sicherheitszuhaltung) are shared.
return h.Category + ":" + h.ComponentID.String()
}
func sortByRiskDesc(entries []HazardBlockEntry, assessMap map[uuid.UUID]*RiskAssessment) {
sort.Slice(entries, func(i, j int) bool {
ri := inherentRisk(entries[i], assessMap)
rj := inherentRisk(entries[j], assessMap)
return ri > rj
})
}
func inherentRisk(entry HazardBlockEntry, assessMap map[uuid.UUID]*RiskAssessment) float64 {
if entry.Assessment != nil {
return entry.Assessment.InherentRisk
}
if a, ok := assessMap[entry.Hazard.ID]; ok {
return a.InherentRisk
}
return 0
}
// mitigationsCoveredBy checks if child's measures are functionally covered
// by parent's measures (same reduction type and hazard category).
func mitigationsCoveredBy(child, parent HazardBlockEntry, allMits []Mitigation) bool {
if len(child.MitigationIDs) == 0 {
return true // No measures needed → covered by default
}
mitMap := make(map[uuid.UUID]Mitigation)
for _, m := range allMits {
mitMap[m.ID] = m
}
// Check: for each child mitigation type, parent has same type
parentTypes := make(map[ReductionType]bool)
for _, mid := range parent.MitigationIDs {
if m, ok := mitMap[mid]; ok {
parentTypes[m.ReductionType] = true
}
}
for _, mid := range child.MitigationIDs {
if m, ok := mitMap[mid]; ok {
if !parentTypes[m.ReductionType] {
return false
}
}
}
return true
}
func toUUIDSet(ids []uuid.UUID) map[uuid.UUID]bool {
s := make(map[uuid.UUID]bool, len(ids))
for _, id := range ids {
s[id] = true
}
return s
}
@@ -54,6 +54,10 @@ type HazardPattern struct {
// of the listed failure modes is relevant (by ComponentType match against project components). // of the listed failure modes is relevant (by ComponentType match against project components).
// Empty/nil = fires regardless of failure modes (backwards compatible). // Empty/nil = fires regardless of failure modes (backwards compatible).
RequiredFailureModes []string `json:"required_failure_modes,omitempty"` RequiredFailureModes []string `json:"required_failure_modes,omitempty"`
// ApplicableLifecycles lists the ISO 12100 lifecycle phases where this hazard
// is relevant. Written into the Hazard's LifecyclePhase field on creation.
// Empty = not set (pattern does not specify lifecycle applicability).
ApplicableLifecycles []string `json:"applicable_lifecycles,omitempty"`
} }
// Standard human roles for machinery interaction (ISO 12100 + BetrSichV). // Standard human roles for machinery interaction (ISO 12100 + BetrSichV).
@@ -126,7 +126,7 @@ func GetCNCHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
}, },
{ {
ID: "HP1408", NameDE: "Falscher Werkzeug-Offset nach Einrichtung", NameEN: "Wrong tool offset after setup", ID: "HP1408", NameDE: "Falscher Werkzeug-Offset", NameEN: "Wrong tool offset after setup",
RequiredComponentTags: []string{"cutting_tool", "programmable"}, RequiredComponentTags: []string{"cutting_tool", "programmable"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M041", "M050"}, SuggestedMeasureIDs: []string{"M041", "M050"},
@@ -149,7 +149,7 @@ func GetCNCHazardPatterns() []HazardPattern {
Priority: 84, MachineTypes: cncTypes, Priority: 84, MachineTypes: cncTypes,
OperationalStates: []string{"teach_mode", "manual_operation"}, OperationalStates: []string{"teach_mode", "manual_operation"},
HumanRoles: []string{"programmer", "maintenance_tech"}, HumanRoles: []string{"programmer", "maintenance_tech"},
ScenarioDE: "Achsen verfahren im Einrichtbetrieb mit voller Produktionsgeschwindigkeit", ScenarioDE: "Achsen verfahren mit voller Produktionsgeschwindigkeit",
TriggerDE: "Fehlende Geschwindigkeitsbegrenzung im Einrichtmodus oder Umgehung", TriggerDE: "Fehlende Geschwindigkeitsbegrenzung im Einrichtmodus oder Umgehung",
HarmDE: "Quetschung oder Schlagverletzung durch schnell verfahrende Maschinenteile", HarmDE: "Quetschung oder Schlagverletzung durch schnell verfahrende Maschinenteile",
AffectedDE: "Einrichter, Programmierer", ZoneDE: "Verfahrbereich der Achsen", AffectedDE: "Einrichter, Programmierer", ZoneDE: "Verfahrbereich der Achsen",
@@ -49,7 +49,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 3, DefaultSeverity: 4, DefaultExposure: 3,
}, },
{ {
ID: "HP1423", NameDE: "Absturz schwerer Maschinenteile bei Wartung", NameEN: "Heavy machine part falling during maintenance", ID: "HP1423", NameDE: "Absturz schwerer Maschinenteile", NameEN: "Heavy machine part falling during maintenance",
RequiredComponentTags: []string{"moving_part"}, RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M245", "M210"}, SuggestedMeasureIDs: []string{"M245", "M210"},
@@ -57,7 +57,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
Priority: 80, MachineTypes: cncTypes, Priority: 80, MachineTypes: cncTypes,
OperationalStates: []string{"maintenance"}, OperationalStates: []string{"maintenance"},
HumanRoles: []string{"maintenance_tech"}, HumanRoles: []string{"maintenance_tech"},
ScenarioDE: "Schwere Maschinenteile (Spindelstock, Revolverkopf) fallen bei Demontage unkontrolliert herab", ScenarioDE: "Schwere Maschinenteile (Spindelstock, Revolverkopf) fallen unkontrolliert herab",
TriggerDE: "Fehlende Abstuetzmittel oder Hebezeuge bei Wartung schwerer Baugruppen", TriggerDE: "Fehlende Abstuetzmittel oder Hebezeuge bei Wartung schwerer Baugruppen",
HarmDE: "Quetschung von Hand oder Fuss, Knochenbrueche", HarmDE: "Quetschung von Hand oder Fuss, Knochenbrueche",
AffectedDE: "Wartungspersonal", ZoneDE: "Maschineninneres, Wartungszugang", AffectedDE: "Wartungspersonal", ZoneDE: "Maschineninneres, Wartungszugang",
@@ -193,7 +193,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
DefaultSeverity: 2, DefaultExposure: 3, DefaultSeverity: 2, DefaultExposure: 3,
}, },
{ {
ID: "HP1433", NameDE: "Unkontrollierte Achsbewegung bei Probelauf nach Wartung", NameEN: "Uncontrolled axis movement during test run after maintenance", ID: "HP1433", NameDE: "Unkontrollierte Achsbewegung nach Probelauf", NameEN: "Uncontrolled axis movement during test run after maintenance",
RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredComponentTags: []string{"moving_part", "programmable"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M212", "M050", "M042"}, SuggestedMeasureIDs: []string{"M212", "M050", "M042"},
@@ -202,7 +202,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
OperationalStates: []string{"manual_operation", "teach_mode"}, OperationalStates: []string{"manual_operation", "teach_mode"},
HumanRoles: []string{"maintenance_tech", "programmer"}, HumanRoles: []string{"maintenance_tech", "programmer"},
StateTransitions: []string{"maintenance→manual_operation"}, StateTransitions: []string{"maintenance→manual_operation"},
ScenarioDE: "Nach Wartung oder Reparatur verfahren Achsen unkontrolliert beim ersten Testlauf", ScenarioDE: "oder Reparatur verfahren Achsen unkontrolliert beim ersten Testlauf",
TriggerDE: "Falsche Parameter nach Wartung, fehlende Referenzfahrt, Endschalter nicht justiert", TriggerDE: "Falsche Parameter nach Wartung, fehlende Referenzfahrt, Endschalter nicht justiert",
HarmDE: "Quetschung, Kollision Werkzeug/Werkstueck", HarmDE: "Quetschung, Kollision Werkzeug/Werkstueck",
AffectedDE: "Wartungspersonal, Einrichter", ZoneDE: "Verfahrbereich, Bearbeitungsraum", AffectedDE: "Wartungspersonal, Einrichter", ZoneDE: "Verfahrbereich, Bearbeitungsraum",
@@ -218,7 +218,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
Priority: 70, MachineTypes: cncTypes, Priority: 70, MachineTypes: cncTypes,
OperationalStates: []string{"maintenance"}, OperationalStates: []string{"maintenance"},
HumanRoles: []string{"maintenance_tech"}, HumanRoles: []string{"maintenance_tech"},
ScenarioDE: "Restkuehlmittel tropft bei Wartung auf Schaltschrank oder Steuerungskomponenten", ScenarioDE: "Restkuehlmittel tropft auf Schaltschrank oder Steuerungskomponenten",
TriggerDE: "Fehlende Auffangwanne oder Abdeckung bei Wartung an KSS-fuehrenden Bauteilen", TriggerDE: "Fehlende Auffangwanne oder Abdeckung bei Wartung an KSS-fuehrenden Bauteilen",
HarmDE: "Kurzschluss, Stromschlag bei Beruehrung nasser Teile", HarmDE: "Kurzschluss, Stromschlag bei Beruehrung nasser Teile",
AffectedDE: "Wartungspersonal", ZoneDE: "Schaltschrank, Steuerungsbereich", AffectedDE: "Wartungspersonal", ZoneDE: "Schaltschrank, Steuerungsbereich",
@@ -11,7 +11,7 @@ func builtinElectricalPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M061", "M062", "M063", "M121"}, SuggestedMeasureIDs: []string{"M061", "M062", "M063", "M121"},
SuggestedEvidenceIDs: []string{"E01", "E04", "E10"}, SuggestedEvidenceIDs: []string{"E01", "E04", "E10"},
Priority: 95, Priority: 95,
ScenarioDE: "Person beruehrt spannungsfuehrende Teile bei Wartung, Stoerungsbeseitigung oder durch defekte Isolation.", ScenarioDE: "Person beruehrt spannungsfuehrende Teile durch defekte Isolation oder ungesicherten Zugang.",
TriggerDE: "Direktes oder indirektes Beruehren spannungsfuehrender Leiter ueber 50 V AC / 120 V DC.", TriggerDE: "Direktes oder indirektes Beruehren spannungsfuehrender Leiter ueber 50 V AC / 120 V DC.",
HarmDE: "Stromschlag, Herzkammerflimmern, Verbrennungen, Todesfolge bei Hochspannung.", HarmDE: "Stromschlag, Herzkammerflimmern, Verbrennungen, Todesfolge bei Hochspannung.",
AffectedDE: "Wartungspersonal, Elektrofachkraefte, Bedienpersonal", AffectedDE: "Wartungspersonal, Elektrofachkraefte, Bedienpersonal",
@@ -66,7 +66,7 @@ func builtinEnvironmentPatterns() []HazardPattern {
DefaultSeverity: 2, DefaultExposure: 5, DefaultSeverity: 2, DefaultExposure: 5,
}, },
{ {
ID: "HP027", NameDE: "Ergonomische Belastung bei Wartung in der Hoehe", NameEN: "Ergonomic risk for work at height", ID: "HP027", NameDE: "Ergonomische Belastung in der Hoehe", NameEN: "Ergonomic risk for work at height",
RequiredComponentTags: []string{"structural_part", "gravity_risk"}, RequiredComponentTags: []string{"structural_part", "gravity_risk"},
RequiredEnergyTags: []string{}, RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"ergonomic", "mechanical_hazard"}, GeneratedHazardCats: []string{"ergonomic", "mechanical_hazard"},
@@ -130,7 +130,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
SuggestedMeasureIDs: []string{"M121", "M131"}, SuggestedMeasureIDs: []string{"M121", "M131"},
SuggestedEvidenceIDs: []string{"E14"}, SuggestedEvidenceIDs: []string{"E14"},
Priority: 90, Priority: 90,
ScenarioDE: "Nach Wartung vergessenes Werkzeug wird beim Anlauf der Maschine zum Geschoss.", ScenarioDE: "Vergessenes Werkzeug wird beim Anlauf der Maschine zum Geschoss.",
TriggerDE: "Werkzeug liegt im Arbeitsraum, Maschine wird ohne Kontrolle gestartet", TriggerDE: "Werkzeug liegt im Arbeitsraum, Maschine wird ohne Kontrolle gestartet",
HarmDE: "Wegschleudern des Werkzeugs, schwere Verletzungen durch Projektil", HarmDE: "Wegschleudern des Werkzeugs, schwere Verletzungen durch Projektil",
AffectedDE: "Bedienpersonal, Personen im Umfeld", AffectedDE: "Bedienpersonal, Personen im Umfeld",
@@ -262,7 +262,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M005"}, SuggestedMeasureIDs: []string{"M003", "M005"},
SuggestedEvidenceIDs: []string{"E08"}, SuggestedEvidenceIDs: []string{"E08"},
Priority: 80, Priority: 80, MachineTypes: []string{"press"},
ScenarioDE: "Exzentrische Belastung des Stoessels fuehrt zu seitlichem Ausbrechen des Werkstuecks.", ScenarioDE: "Exzentrische Belastung des Stoessels fuehrt zu seitlichem Ausbrechen des Werkstuecks.",
TriggerDE: "Werkstueck nicht korrekt positioniert, seitliche Kraftkomponente entsteht", TriggerDE: "Werkstueck nicht korrekt positioniert, seitliche Kraftkomponente entsteht",
HarmDE: "Aufprallverletzung durch geschleudertes Werkstueck, Quetschung", HarmDE: "Aufprallverletzung durch geschleudertes Werkstueck, Quetschung",
@@ -290,7 +290,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
// Roboter/Cobot erweitert (HP151-HP154) // Roboter/Cobot erweitert (HP151-HP154)
// ================================================================ // ================================================================
{ {
ID: "HP151", NameDE: "Kollision bei Teach-In-Betrieb", NameEN: "Collision during teach-in operation", ID: "HP151", NameDE: "Kollision im manuellen Verfahrbetrieb", NameEN: "Collision during teach-in operation",
RequiredComponentTags: []string{"programmable", "moving_part"}, RequiredComponentTags: []string{"programmable", "moving_part"},
RequiredEnergyTags: []string{}, RequiredEnergyTags: []string{},
RequiredLifecycles: []string{"setup"}, RequiredLifecycles: []string{"setup"},
@@ -336,7 +336,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 2, DefaultSeverity: 3, DefaultExposure: 2,
}, },
{ {
ID: "HP154", NameDE: "Kollision zweier Roboter", NameEN: "Collision of two robots", ID: "HP154", MachineTypes: []string{"robotics_cobot"}, NameDE: "Kollision zweier Roboter", NameEN: "Collision of two robots",
RequiredComponentTags: []string{"programmable", "moving_part"}, RequiredComponentTags: []string{"programmable", "moving_part"},
RequiredEnergyTags: []string{"kinetic"}, RequiredEnergyTags: []string{"kinetic"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -361,7 +361,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M051"}, SuggestedMeasureIDs: []string{"M001", "M051"},
SuggestedEvidenceIDs: []string{"E08", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80, Priority: 80, MachineTypes: []string{"conveyor", "packaging"},
ScenarioDE: "Finger oder Kleidung werden an der Bandumlenkstelle eingezogen.", ScenarioDE: "Finger oder Kleidung werden an der Bandumlenkstelle eingezogen.",
TriggerDE: "Eingriff am laufenden Band, lose Kleidung geraet in Umlenkrolle", TriggerDE: "Eingriff am laufenden Band, lose Kleidung geraet in Umlenkrolle",
HarmDE: "Fingeramputation, Armverletzung, Strangulation durch eingezogene Kleidung", HarmDE: "Fingeramputation, Armverletzung, Strangulation durch eingezogene Kleidung",
@@ -595,7 +595,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M051"}, SuggestedMeasureIDs: []string{"M003", "M051"},
SuggestedEvidenceIDs: []string{"E08", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80, Priority: 80, MachineTypes: []string{"rotary_transfer"},
ScenarioDE: "Hand wird zwischen Drehteller und festem Anschlag eingeklemmt bei Taktbewegung.", ScenarioDE: "Hand wird zwischen Drehteller und festem Anschlag eingeklemmt bei Taktbewegung.",
TriggerDE: "Eingriff waehrend der Taktbewegung, fehlende Schutzabdeckung am Drehteller", TriggerDE: "Eingriff waehrend der Taktbewegung, fehlende Schutzabdeckung am Drehteller",
HarmDE: "Quetschung, Fingerfraktur, Amputation von Fingern", HarmDE: "Quetschung, Fingerfraktur, Amputation von Fingern",
@@ -42,7 +42,7 @@ func GetDGUVExtendedPatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001"}, SuggestedMeasureIDs: []string{"M001"},
Priority: 60, Priority: 60,
ScenarioDE: "Reibung an rotierender Welle oder Walze bei Wartung", HarmDE: "Hautabschuerfungen, Verbrennungen durch Reibungswaerme", ScenarioDE: "Reibung an rotierender Welle oder Walze", HarmDE: "Hautabschuerfungen, Verbrennungen durch Reibungswaerme",
TriggerDE: "Beruehrung laufender Teile", AffectedDE: "Wartungspersonal", ZoneDE: "Walzen-/Wellenbereich", DefaultSeverity: 2, DefaultExposure: 3, TriggerDE: "Beruehrung laufender Teile", AffectedDE: "Wartungspersonal", ZoneDE: "Walzen-/Wellenbereich", DefaultSeverity: 2, DefaultExposure: 3,
}, },
{ {
@@ -102,7 +102,7 @@ func GetDGUVExtendedPatterns() []HazardPattern {
RequiredEnergyTags: []string{}, RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M051"}, SuggestedMeasureIDs: []string{"M051"},
Priority: 80, Priority: 80, MachineTypes: []string{"crane", "construction"},
ScenarioDE: "Unkontrolliertes Schwingen einer angehobenen Last", HarmDE: "Quetschung, Erschlagen durch pendelnde Last", ScenarioDE: "Unkontrolliertes Schwingen einer angehobenen Last", HarmDE: "Quetschung, Erschlagen durch pendelnde Last",
TriggerDE: "Schraeger Zug oder ploetzliches Abstoppen", AffectedDE: "Kranfuehrer, Anschlaeger", ZoneDE: "Schwenkbereich des Krans", DefaultSeverity: 4, DefaultExposure: 3, TriggerDE: "Schraeger Zug oder ploetzliches Abstoppen", AffectedDE: "Kranfuehrer, Anschlaeger", ZoneDE: "Schwenkbereich des Krans", DefaultSeverity: 4, DefaultExposure: 3,
}, },
@@ -261,13 +261,13 @@ func GetDGUVExtendedPatterns() []HazardPattern {
TriggerDE: "Hautkontakt mit kontaminiertem Fluid", AffectedDE: "Maschinenbediener, Wartungspersonal", ZoneDE: "Fluidsystem, Tank", DefaultSeverity: 2, DefaultExposure: 3, TriggerDE: "Hautkontakt mit kontaminiertem Fluid", AffectedDE: "Maschinenbediener, Wartungspersonal", ZoneDE: "Fluidsystem, Tank", DefaultSeverity: 2, DefaultExposure: 3,
}, },
{ {
ID: "HP117", NameDE: "Asbest-/Mineralfaserfreisetzung bei Demontage", NameEN: "Asbestos/mineral fiber release during dismantling", ID: "HP117", NameDE: "Asbest-/Mineralfaserfreisetzung", NameEN: "Asbestos/mineral fiber release during dismantling",
RequiredComponentTags: []string{"chemical_risk"}, RequiredComponentTags: []string{"chemical_risk"},
RequiredLifecycles: []string{"decommissioning", "disposal"}, RequiredLifecycles: []string{"decommissioning", "disposal"},
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"}, SuggestedMeasureIDs: []string{"M141"},
Priority: 90, Priority: 90,
ScenarioDE: "Freisetzung von Asbestfasern bei Demontage alter Anlagen", HarmDE: "Asbestose, Mesotheliom (Langzeitfolge)", ScenarioDE: "Freisetzung von Asbestfasern alter Anlagen", HarmDE: "Asbestose, Mesotheliom (Langzeitfolge)",
TriggerDE: "Mechanische Bearbeitung asbesthaltiger Bauteile", AffectedDE: "Demontagepersonal", ZoneDE: "Altanlagen, Isolierung", DefaultSeverity: 5, DefaultExposure: 1, TriggerDE: "Mechanische Bearbeitung asbesthaltiger Bauteile", AffectedDE: "Demontagepersonal", ZoneDE: "Altanlagen, Isolierung", DefaultSeverity: 5, DefaultExposure: 1,
}, },
@@ -428,7 +428,7 @@ func GetFinalPatternsA() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"}, SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"}, SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 78, ScenarioDE: "Finger wird zwischen Kette und Kettenrad eingezogen", Priority: 78, MachineTypes: []string{"conveyor", "forestry"}, ScenarioDE: "Finger wird zwischen Kette und Kettenrad eingezogen",
TriggerDE: "Eingriff in ungeschuetzten Kettenantrieb", HarmDE: "Fingerquetschung, Abriss", TriggerDE: "Eingriff in ungeschuetzten Kettenantrieb", HarmDE: "Fingerquetschung, Abriss",
AffectedDE: "Wartungspersonal", ZoneDE: "Kettenrad, Kettenstrang", AffectedDE: "Wartungspersonal", ZoneDE: "Kettenrad, Kettenstrang",
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
@@ -667,13 +667,13 @@ func GetFinalPatternsA() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 2, DefaultSeverity: 5, DefaultExposure: 2,
}, },
{ {
ID: "HP1054", NameDE: "Herabfallendes Bauteil bei Montage", NameEN: "Falling component during assembly", ID: "HP1054", NameDE: "Herabfallendes Bauteil", NameEN: "Falling component during assembly",
RequiredComponentTags: []string{"gravity_risk", "structural_part"}, RequiredComponentTags: []string{"gravity_risk", "structural_part"},
RequiredEnergyTags: []string{"gravitational"}, RequiredEnergyTags: []string{"gravitational"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M141"}, SuggestedMeasureIDs: []string{"M001", "M141"},
SuggestedEvidenceIDs: []string{"E01"}, SuggestedEvidenceIDs: []string{"E01"},
Priority: 68, ScenarioDE: "Bauteil loest sich bei Montage und faellt", Priority: 68, ScenarioDE: "Bauteil loest sich und faellt",
TriggerDE: "Unzureichende Sicherung waehrend Zusammenbau", HarmDE: "Prellung, Fraktur", TriggerDE: "Unzureichende Sicherung waehrend Zusammenbau", HarmDE: "Prellung, Fraktur",
AffectedDE: "Montagepersonal", ZoneDE: "Montageplatz, Regalbereich", AffectedDE: "Montagepersonal", ZoneDE: "Montageplatz, Regalbereich",
DefaultSeverity: 3, DefaultExposure: 3, DefaultSeverity: 3, DefaultExposure: 3,
@@ -814,7 +814,7 @@ func GetFinalPatternsA() []HazardPattern {
}, },
// === Einklemmen Haare/Kleidung (3) === // === Einklemmen Haare/Kleidung (3) ===
{ {
ID: "HP1066", NameDE: "Haareinzug Drehmaschine", NameEN: "Hair entanglement lathe", ID: "HP1066", MachineTypes: []string{"lathe", "cnc", "metalworking"}, NameDE: "Haareinzug Drehmaschine", NameEN: "Hair entanglement lathe",
RequiredComponentTags: []string{"rotating_part", "entanglement_risk"}, RequiredComponentTags: []string{"rotating_part", "entanglement_risk"},
RequiredEnergyTags: []string{"rotational"}, RequiredEnergyTags: []string{"rotational"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -1027,7 +1027,7 @@ func GetFinalPatternsA() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"}, SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"}, SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 78, ScenarioDE: "Schwere Maschine kippt bei Transport oder Betrieb", Priority: 78, ScenarioDE: "Schwere Maschine kippt oder Betrieb",
TriggerDE: "Unebener Boden, Schwerpunktverlagerung", HarmDE: "Toedliche Quetschung", TriggerDE: "Unebener Boden, Schwerpunktverlagerung", HarmDE: "Toedliche Quetschung",
AffectedDE: "Transportpersonal", ZoneDE: "Kippbereich, Aufstellflaeche", AffectedDE: "Transportpersonal", ZoneDE: "Kippbereich, Aufstellflaeche",
DefaultSeverity: 5, DefaultExposure: 1, DefaultSeverity: 5, DefaultExposure: 1,
@@ -624,7 +624,7 @@ func GetFinalPatternsB() []HazardPattern {
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M124", "M141"}, SuggestedMeasureIDs: []string{"M124", "M141"},
SuggestedEvidenceIDs: []string{"E20"}, SuggestedEvidenceIDs: []string{"E20"},
Priority: 82, ScenarioDE: "Asbestfasern werden bei Demontage/Wartung freigesetzt", Priority: 82, ScenarioDE: "Asbestfasern werden /Wartung freigesetzt",
TriggerDE: "Bohren/Saegen in Asbestmaterial, Abrissarbeiten", HarmDE: "Asbestose, Mesotheliom", TriggerDE: "Bohren/Saegen in Asbestmaterial, Abrissarbeiten", HarmDE: "Asbestose, Mesotheliom",
AffectedDE: "Wartungspersonal, Abbrucharbeiter", ZoneDE: "Altanlage, Dichtungen, Isolierungen", AffectedDE: "Wartungspersonal, Abbrucharbeiter", ZoneDE: "Altanlage, Dichtungen, Isolierungen",
DefaultSeverity: 5, DefaultExposure: 1, DefaultSeverity: 5, DefaultExposure: 1,
@@ -860,7 +860,7 @@ func GetFinalPatternsC() []HazardPattern {
GeneratedHazardCats: []string{"ergonomic_hazard"}, GeneratedHazardCats: []string{"ergonomic_hazard"},
SuggestedMeasureIDs: []string{"M141"}, SuggestedMeasureIDs: []string{"M141"},
SuggestedEvidenceIDs: []string{"E01"}, SuggestedEvidenceIDs: []string{"E01"},
Priority: 52, ScenarioDE: "Haeufiges Knien bei Montage/Wartungsarbeiten", Priority: 52, ScenarioDE: "Haeufiges Knien /Wartungsarbeiten",
TriggerDE: "Bodennahe Arbeiten, fehlende Knieschoner", HarmDE: "Meniskusschaden (BK 2112)", TriggerDE: "Bodennahe Arbeiten, fehlende Knieschoner", HarmDE: "Meniskusschaden (BK 2112)",
AffectedDE: "Wartungspersonal", ZoneDE: "Bodenbereich", AffectedDE: "Wartungspersonal", ZoneDE: "Bodenbereich",
DefaultSeverity: 2, DefaultExposure: 4, DefaultSeverity: 2, DefaultExposure: 4,
@@ -158,7 +158,7 @@ func GetFinalPatternsD() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard", "maintenance_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard", "maintenance_hazard"},
SuggestedMeasureIDs: []string{"M001"}, SuggestedMeasureIDs: []string{"M001"},
SuggestedEvidenceIDs: []string{"E01"}, SuggestedEvidenceIDs: []string{"E01"},
Priority: 72, ScenarioDE: "Verschlissenes Teil versagt im Betrieb", Priority: 72, ScenarioDE: "Verschlissenes Teil versagt",
TriggerDE: "Fehlende Inspektion, ueberschrittene Standzeit", HarmDE: "Funktionsverlust, Bruch", TriggerDE: "Fehlende Inspektion, ueberschrittene Standzeit", HarmDE: "Funktionsverlust, Bruch",
AffectedDE: "Bedienpersonal", ZoneDE: "Verschleissteil, Fuehrung", AffectedDE: "Bedienpersonal", ZoneDE: "Verschleissteil, Fuehrung",
DefaultSeverity: 3, DefaultExposure: 3, DefaultSeverity: 3, DefaultExposure: 3,
@@ -573,7 +573,7 @@ func GetFinalPatternsD() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"}, SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01"}, SuggestedEvidenceIDs: []string{"E01"},
Priority: 72, ScenarioDE: "Schutzeinrichtung nach Einrichten nicht reaktiviert", Priority: 72, ScenarioDE: "Schutzeinrichtung nicht reaktiviert",
TriggerDE: "Vergessen, Bypass noch aktiv", HarmDE: "Produktion ohne Schutz", TriggerDE: "Vergessen, Bypass noch aktiv", HarmDE: "Produktion ohne Schutz",
AffectedDE: "Bedienpersonal", ZoneDE: "Gesamte Maschine", AffectedDE: "Bedienpersonal", ZoneDE: "Gesamte Maschine",
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
@@ -817,7 +817,7 @@ func GetFinalPatternsD() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M001", "M005"}, SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E01", "E08"}, SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 78, ScenarioDE: "Kran schwenkt Last ueber besetzten Arbeitsplatz", Priority: 78, MachineTypes: []string{"crane", "construction"}, ScenarioDE: "Kran schwenkt Last ueber besetzten Arbeitsplatz",
TriggerDE: "Fehlende Endschalter, Unachtsamkeit", HarmDE: "Herabfallende Last", TriggerDE: "Fehlende Endschalter, Unachtsamkeit", HarmDE: "Herabfallende Last",
AffectedDE: "Personen darunter", ZoneDE: "Unter Kranschwenkbereich", AffectedDE: "Personen darunter", ZoneDE: "Unter Kranschwenkbereich",
DefaultSeverity: 5, DefaultExposure: 2, DefaultSeverity: 5, DefaultExposure: 2,
@@ -131,7 +131,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
RequiresExpertCalculation: true, RequiresExpertCalculation: true,
ExpertHintDE: "IP-Schutzklasse muss fuer Nassreinigung (mindestens IPX5) nachgewiesen werden.", ExpertHintDE: "IP-Schutzklasse muss fuer Nassreinigung (mindestens IPX5) nachgewiesen werden.",
ExpertHintEN: "IP rating must be verified for wet cleaning conditions (minimum IPX5).", ExpertHintEN: "IP rating must be verified for wet cleaning conditions (minimum IPX5).",
ScenarioDE: "Wasser dringt beim Reinigen in elektrische Komponenten ein und erzeugt einen Fehlerstrom.", ScenarioDE: "Wasser dringt in elektrische Komponenten ein und erzeugt einen Fehlerstrom.",
TriggerDE: "Unzureichende IP-Schutzklasse, defekte Kabeldurchfuehrungen, beschaedigtes Gehaeuse.", TriggerDE: "Unzureichende IP-Schutzklasse, defekte Kabeldurchfuehrungen, beschaedigtes Gehaeuse.",
HarmDE: "Elektrischer Schlag, Herzkammerflimmern, Tod durch Stromschlag.", HarmDE: "Elektrischer Schlag, Herzkammerflimmern, Tod durch Stromschlag.",
AffectedDE: "Reinigungspersonal, Bedienpersonal bei Nassreinigung.", AffectedDE: "Reinigungspersonal, Bedienpersonal bei Nassreinigung.",
@@ -65,7 +65,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M001", "M005"}, SuggestedMeasureIDs: []string{"M001", "M005"},
SuggestedEvidenceIDs: []string{"E08", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 85, Priority: 85,
ScenarioDE: "Kontakt mit rotierendem Maehwerk bei Wartung oder durch Wegschleudern von Fremdkoerpern.", ScenarioDE: "Kontakt mit rotierendem Maehwerk oder durch Wegschleudern von Fremdkoerpern.",
TriggerDE: "Wartung bei laufendem Maehwerk, fehlende Schutzabdeckung, Steinschleuder", TriggerDE: "Wartung bei laufendem Maehwerk, fehlende Schutzabdeckung, Steinschleuder",
HarmDE: "Amputationsverletzung an Fuessen/Haenden, tiefe Schnittwunden, Augenverletzung durch Steinschlag", HarmDE: "Amputationsverletzung an Fuessen/Haenden, tiefe Schnittwunden, Augenverletzung durch Steinschlag",
AffectedDE: "Maehwerksfahrer, Gartenarbeiter, Umstehende", AffectedDE: "Maehwerksfahrer, Gartenarbeiter, Umstehende",
@@ -311,7 +311,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M052", "M141"}, SuggestedMeasureIDs: []string{"M052", "M141"},
SuggestedEvidenceIDs: []string{"E20"}, SuggestedEvidenceIDs: []string{"E20"},
Priority: 70, Priority: 70,
ScenarioDE: "Person stuerzt von erhoehtem Rollenfoerderer bei Wartung oder Stoerungsbeseitigung.", ScenarioDE: "Person stuerzt von erhoehtem Rollenfoerderer .",
TriggerDE: "Fehlende Absturzsicherung, kein Zugangsweg, improvisiertes Besteigen", TriggerDE: "Fehlende Absturzsicherung, kein Zugangsweg, improvisiertes Besteigen",
HarmDE: "Knochenbrueche, Wirbelsaeulenverletzung, toedlicher Sturz ab 2 m Hoehe", HarmDE: "Knochenbrueche, Wirbelsaeulenverletzung, toedlicher Sturz ab 2 m Hoehe",
AffectedDE: "Wartungspersonal, Bediener bei Stoerung", AffectedDE: "Wartungspersonal, Bediener bei Stoerung",
@@ -16,10 +16,10 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"}, RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"}, GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"},
SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 90, SuggestedMeasureIDs: []string{"M054", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 90,
ScenarioDE: "Gespeicherte Energie entlaedt sich bei Wartung", TriggerDE: "Nicht abgelassener Druckspeicher", ScenarioDE: "Gespeicherte Energie entlaedt sich", TriggerDE: "Nicht abgelassener Druckspeicher",
HarmDE: "Unkontrollierte Bewegung, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Antriebe, Speicher", HarmDE: "Unkontrollierte Bewegung, Quetschung", AffectedDE: "Instandhalter", ZoneDE: "Antriebe, Speicher",
DefaultSeverity: 5, DefaultExposure: 3}, DefaultSeverity: 5, DefaultExposure: 3},
{ID: "HP702", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Falsches Werkzeug bei Wartung", NameEN: "Wrong tool during maintenance", {ID: "HP702", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Falsches Werkzeug fuer Arbeiten an der Maschine", NameEN: "Wrong tool during maintenance",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
@@ -33,11 +33,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Unqualifiziertes Personal an Elektrik", TriggerDE: "Keine Elektrofachkraft", ScenarioDE: "Unqualifiziertes Personal an Elektrik", TriggerDE: "Keine Elektrofachkraft",
HarmDE: "Stromschlag, Fehlverdrahtung", AffectedDE: "Instandhalter", ZoneDE: "Schaltschrank", HarmDE: "Stromschlag, Fehlverdrahtung", AffectedDE: "Instandhalter", ZoneDE: "Schaltschrank",
DefaultSeverity: 4, DefaultExposure: 3}, DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP704", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Herabfallen schwerer Teile bei Demontage", NameEN: "Heavy parts falling during disassembly", {ID: "HP704", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Herabfallen schwerer Teile", NameEN: "Heavy parts falling during disassembly",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 75, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 75,
ScenarioDE: "Schwere Teile fallen bei Demontage herab", TriggerDE: "Fehlende Abstuetzung", ScenarioDE: "Schwere Teile fallen herab", TriggerDE: "Fehlende Abstuetzung",
HarmDE: "Quetschung, Frakturen, Tod", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich", HarmDE: "Quetschung, Frakturen, Tod", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich",
DefaultSeverity: 5, DefaultExposure: 3}, DefaultSeverity: 5, DefaultExposure: 3},
{ID: "HP705", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Vergessenes Werkzeug in Maschine", NameEN: "Forgotten tool in machine", {ID: "HP705", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Vergessenes Werkzeug in Maschine", NameEN: "Forgotten tool in machine",
@@ -54,7 +54,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Scharfe Kanten und Grate verletzen", TriggerDE: "Fehlende Schutzhandschuhe", ScenarioDE: "Scharfe Kanten und Grate verletzen", TriggerDE: "Fehlende Schutzhandschuhe",
HarmDE: "Schnittwunden, Abschuerfungen", AffectedDE: "Instandhalter", ZoneDE: "Blechverkleidungen", HarmDE: "Schnittwunden, Abschuerfungen", AffectedDE: "Instandhalter", ZoneDE: "Blechverkleidungen",
DefaultSeverity: 2, DefaultExposure: 4}, DefaultSeverity: 2, DefaultExposure: 4},
{ID: "HP707", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Verbrennung an heissen Teilen bei Wartung", NameEN: "Burn on hot parts during maintenance", {ID: "HP707", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Verbrennung an heissen Teilen", NameEN: "Burn on hot parts during maintenance",
RequiredComponentTags: []string{"high_temperature"}, RequiredLifecycles: []string{"maintenance"}, RequiredComponentTags: []string{"high_temperature"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"thermal_hazard"}, GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E10"}, Priority: 60, SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E10"}, Priority: 60,
@@ -72,11 +72,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"chemical_risk"}, RequiredLifecycles: []string{"maintenance"}, RequiredComponentTags: []string{"chemical_risk"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50, SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
ScenarioDE: "Verkeimter Kuehlschmierstoff bei Wartung", TriggerDE: "Altes KSS, Biofilme", ScenarioDE: "Verkeimter Kuehlschmierstoff", TriggerDE: "Altes KSS, Biofilme",
HarmDE: "Hautinfektionen, Atemwegsbeschwerden", AffectedDE: "Instandhalter", ZoneDE: "KSS-System", HarmDE: "Hautinfektionen, Atemwegsbeschwerden", AffectedDE: "Instandhalter", ZoneDE: "KSS-System",
DefaultSeverity: 2, DefaultExposure: 3}, DefaultSeverity: 2, DefaultExposure: 3},
// — Einrichten / Umruesten (HP710-HP719) — // — Einrichten / Umruesten (HP710-HP719) —
{ID: "HP710", OperationalStates: []string{"teach_mode"}, HumanRoles: []string{"programmer"}, NameDE: "Falsche Parameter nach Umruestung", NameEN: "Wrong parameters after changeover", {ID: "HP710", OperationalStates: []string{"teach_mode"}, HumanRoles: []string{"programmer"}, NameDE: "Falsche Parameter nach Produktwechsel", NameEN: "Wrong parameters after changeover",
RequiredComponentTags: []string{"programmable"}, RequiredLifecycles: []string{"setup"}, RequiredComponentTags: []string{"programmable"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"safety_function_failure"}, GeneratedHazardCats: []string{"safety_function_failure"},
SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 75, SuggestedMeasureIDs: []string{"M106", "M082"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 75,
@@ -90,7 +90,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Schwere Werkzeuge manuell gewechselt", TriggerDE: "Kein Hebezeug, Finger eingeklemmt", ScenarioDE: "Schwere Werkzeuge manuell gewechselt", TriggerDE: "Kein Hebezeug, Finger eingeklemmt",
HarmDE: "Quetschung, Amputation", AffectedDE: "Einrichter", ZoneDE: "Werkzeugaufnahme", HarmDE: "Quetschung, Amputation", AffectedDE: "Einrichter", ZoneDE: "Werkzeugaufnahme",
DefaultSeverity: 4, DefaultExposure: 4}, DefaultSeverity: 4, DefaultExposure: 4},
{ID: "HP712", OperationalStates: []string{"teach_mode", "manual_operation"}, HumanRoles: []string{"programmer", "maintenance_tech"}, NameDE: "Unkontrollierte Bewegung bei Testlauf", NameEN: "Uncontrolled movement test run", {ID: "HP712", OperationalStates: []string{"teach_mode", "manual_operation"}, HumanRoles: []string{"programmer", "maintenance_tech"}, NameDE: "Unkontrollierte Bewegung nach Probelauf", NameEN: "Uncontrolled movement test run",
RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"setup"}, RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M106", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80, SuggestedMeasureIDs: []string{"M106", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
@@ -129,17 +129,17 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
ScenarioDE: "Falsches Material nach Umruestung", TriggerDE: "Verwechslung, fehlende Kennzeichnung", ScenarioDE: "Falsches Material", TriggerDE: "Verwechslung, fehlende Kennzeichnung",
HarmDE: "Werkzeugbruch, Splitterflug", AffectedDE: "Bedienpersonal", ZoneDE: "Materialzufuhr", HarmDE: "Werkzeugbruch, Splitterflug", AffectedDE: "Bedienpersonal", ZoneDE: "Materialzufuhr",
DefaultSeverity: 3, DefaultExposure: 3}, DefaultSeverity: 3, DefaultExposure: 3},
{ID: "HP718", NameDE: "Absturz bei Einrichtung hoher Maschine", NameEN: "Fall during tall machine setup", {ID: "HP718", NameDE: "Absturz hoher Maschine", NameEN: "Fall during tall machine setup",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
ScenarioDE: "Einrichtarbeiten in Hoehe ohne sicheren Zugang", TriggerDE: "Improvisierte Aufstiegshilfe", ScenarioDE: "Einrichtarbeiten in Hoehe ohne sicheren Zugang", TriggerDE: "Improvisierte Aufstiegshilfe",
HarmDE: "Absturz, Frakturen", AffectedDE: "Einrichter", ZoneDE: "Maschinenoberteil", HarmDE: "Absturz, Frakturen", AffectedDE: "Einrichter", ZoneDE: "Maschinenoberteil",
DefaultSeverity: 4, DefaultExposure: 3}, DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP719", NameDE: "Schutzeinrichtung nach Umruestung defekt", NameEN: "Faulty guard after changeover", {ID: "HP719", NameDE: "Schutzeinrichtung nach Produktwechsel defekt", NameEN: "Faulty guard after changeover",
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"setup"}, RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"safety_function_failure", "mechanical_hazard"}, GeneratedHazardCats: []string{"safety_function_failure", "mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
@@ -218,7 +218,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
HarmDE: "Folgestoerung mit groesserem Schaden", AffectedDE: "Bedienpersonal", ZoneDE: "Steuerung", HarmDE: "Folgestoerung mit groesserem Schaden", AffectedDE: "Bedienpersonal", ZoneDE: "Steuerung",
DefaultSeverity: 4, DefaultExposure: 2}, DefaultSeverity: 4, DefaultExposure: 2},
// — Transport / Montage (HP900-HP907) — // — Transport / Montage (HP900-HP907) —
{ID: "HP900", NameDE: "Kippen der Maschine beim Transport", NameEN: "Machine tipping during transport", {ID: "HP900", NameDE: "Kippen der Maschine", NameEN: "Machine tipping during transport",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"transport"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"transport"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 80, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 80,
@@ -267,7 +267,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Stapler kollidiert mit Personen", TriggerDE: "Eingeschraenkte Sicht, zu schnell", ScenarioDE: "Stapler kollidiert mit Personen", TriggerDE: "Eingeschraenkte Sicht, zu schnell",
HarmDE: "Anfahrunfall, Quetschung", AffectedDE: "Fussgaenger", ZoneDE: "Transportwege", HarmDE: "Anfahrunfall, Quetschung", AffectedDE: "Fussgaenger", ZoneDE: "Transportwege",
DefaultSeverity: 4, DefaultExposure: 3}, DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP907", NameDE: "Verankerungsfehler bei Montage", NameEN: "Anchoring error installation", {ID: "HP907", NameDE: "Verankerungsfehler am Aufstellort", NameEN: "Anchoring error installation",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"transport"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"transport"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
@@ -339,7 +339,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Reinigung ohne Abschaltung der Maschine", TriggerDE: "Zeitdruck", ScenarioDE: "Reinigung ohne Abschaltung der Maschine", TriggerDE: "Zeitdruck",
HarmDE: "Einzug, Quetschung, Aufwickeln", AffectedDE: "Reinigungspersonal", ZoneDE: "Rotierende Teile", HarmDE: "Einzug, Quetschung, Aufwickeln", AffectedDE: "Reinigungspersonal", ZoneDE: "Rotierende Teile",
DefaultSeverity: 5, DefaultExposure: 3}, DefaultSeverity: 5, DefaultExposure: 3},
{ID: "HP917", NameDE: "Nassrutschiger Boden nach Reinigung", NameEN: "Wet slippery floor after cleaning", {ID: "HP917", NameDE: "Nassrutschiger Boden durch Fluessigkeiten", NameEN: "Wet slippery floor after cleaning",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"cleaning"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"cleaning"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 45, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 45,
@@ -410,7 +410,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"}, RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"},
RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"electrical_hazard"}, RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 75, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E09"}, Priority: 75,
ScenarioDE: "Messung unter Spannung bei Fehlersuche", TriggerDE: "Messgeraet rutscht ab", ScenarioDE: "Messung unter Spannung", TriggerDE: "Messgeraet rutscht ab",
HarmDE: "Stromschlag, Lichtbogen", AffectedDE: "Elektrofachkraft", ZoneDE: "Schaltschrank", HarmDE: "Stromschlag, Lichtbogen", AffectedDE: "Elektrofachkraft", ZoneDE: "Schaltschrank",
DefaultSeverity: 4, DefaultExposure: 3}, DefaultSeverity: 4, DefaultExposure: 3},
{ID: "HP927", NameDE: "ZfP mit Strahlenquelle", NameEN: "NDT with radiation source", {ID: "HP927", NameDE: "ZfP mit Strahlenquelle", NameEN: "NDT with radiation source",
@@ -451,7 +451,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
HarmDE: "Vernachlaessigte Sicherheit", AffectedDE: "Alle Gewerke", ZoneDE: "Schnittstellen", HarmDE: "Vernachlaessigte Sicherheit", AffectedDE: "Alle Gewerke", ZoneDE: "Schnittstellen",
DefaultSeverity: 3, DefaultExposure: 3}, DefaultSeverity: 3, DefaultExposure: 3},
// — Notfall (HP932-HP934) — // — Notfall (HP932-HP934) —
{ID: "HP932", NameDE: "Versperrte Fluchtwege bei Wartung", NameEN: "Blocked escape routes maintenance", {ID: "HP932", NameDE: "Versperrte Fluchtwege durch abgestelltes Material", NameEN: "Blocked escape routes maintenance",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70, SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 70,
@@ -465,11 +465,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
ScenarioDE: "Kein Erste-Hilfe-Material am abgelegenen Ort", TriggerDE: "Entfernter Standort", ScenarioDE: "Kein Erste-Hilfe-Material am abgelegenen Ort", TriggerDE: "Entfernter Standort",
HarmDE: "Verzoegerte Erstversorgung", AffectedDE: "Instandhalter", ZoneDE: "Abgelegene Wartungsorte", HarmDE: "Verzoegerte Erstversorgung", AffectedDE: "Instandhalter", ZoneDE: "Abgelegene Wartungsorte",
DefaultSeverity: 3, DefaultExposure: 3}, DefaultSeverity: 3, DefaultExposure: 3},
{ID: "HP934", NameDE: "Brandbekaempfung bei Wartung", NameEN: "Firefighting during maintenance", {ID: "HP934", NameDE: "Erschwerter Zugang zu Loescheinrichtungen", NameEN: "Firefighting during maintenance",
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"}, RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"thermal_hazard"}, GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E10", "E20"}, Priority: 65, SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E10", "E20"}, Priority: 65,
ScenarioDE: "Feuerloescher nicht erreichbar bei Wartung", TriggerDE: "Verstellter Loescher", ScenarioDE: "Feuerloescher nicht erreichbar", TriggerDE: "Verstellter Loescher",
HarmDE: "Brandausbreitung, Verbrennungen", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich", HarmDE: "Brandausbreitung, Verbrennungen", AffectedDE: "Instandhalter", ZoneDE: "Wartungsbereich",
DefaultSeverity: 4, DefaultExposure: 2}, DefaultSeverity: 4, DefaultExposure: 2},
} }
@@ -90,7 +90,7 @@ func builtinMechanicalPatterns() []HazardPattern {
TriggerDE: "Bediener befindet sich im Kraftwirkbereich waehrend des Arbeitshubes oder bei Stoerungsbeseitigung.", TriggerDE: "Bediener befindet sich im Kraftwirkbereich waehrend des Arbeitshubes oder bei Stoerungsbeseitigung.",
HarmDE: "Schwere Quetschung, Fraktur, innere Verletzungen, Todesfolge bei Ganzkompression.", HarmDE: "Schwere Quetschung, Fraktur, innere Verletzungen, Todesfolge bei Ganzkompression.",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal", AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
ZoneDE: "Kraftwirkbereich (Pressenraum, Vorschubachse), Einlegestelle", ZoneDE: "Kraftwirkbereich, Einlegestelle, Vorschubachse",
DefaultSeverity: 5, DefaultExposure: 3, DefaultSeverity: 5, DefaultExposure: 3,
}, },
{ {
@@ -120,7 +120,7 @@ func builtinMechanicalPatterns() []HazardPattern {
TriggerDE: "Versagen einer Halterung, Bruch eines Lastaufnahmemittels oder Abrutschen bei Wartungsarbeiten in der Hoehe.", TriggerDE: "Versagen einer Halterung, Bruch eines Lastaufnahmemittels oder Abrutschen bei Wartungsarbeiten in der Hoehe.",
HarmDE: "Kopfverletzung, Fraktur, Quetschung durch herabfallende Last; Sturzverletung.", HarmDE: "Kopfverletzung, Fraktur, Quetschung durch herabfallende Last; Sturzverletung.",
AffectedDE: "Wartungspersonal, Bedienpersonal, Personen im Gefahrenbereich", AffectedDE: "Wartungspersonal, Bedienpersonal, Personen im Gefahrenbereich",
ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen, Kran-/Hebezeugbereich", ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen",
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
}, },
{ {
@@ -150,7 +150,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
}, },
{ {
ID: "HP075", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit heissen Teilen bei Wartung", NameEN: "Contact with hot parts during maintenance", ID: "HP075", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit heissen Oberflaechen", NameEN: "Contact with hot parts during maintenance",
RequiredComponentTags: []string{"high_temperature"}, RequiredComponentTags: []string{"high_temperature"},
RequiredLifecycles: []string{"maintenance"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"thermal_hazard"}, GeneratedHazardCats: []string{"thermal_hazard"},
@@ -165,7 +165,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 3, DefaultSeverity: 3, DefaultExposure: 3,
}, },
{ {
ID: "HP076", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit Gefahrstoffen bei Wartung", NameEN: "Contact with hazardous substances during maintenance", ID: "HP076", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Kontakt mit Gefahrstoffen", NameEN: "Contact with hazardous substances during maintenance",
RequiredComponentTags: []string{"chemical_risk"}, RequiredComponentTags: []string{"chemical_risk"},
RequiredLifecycles: []string{"maintenance", "cleaning"}, RequiredLifecycles: []string{"maintenance", "cleaning"},
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
@@ -179,7 +179,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 3, DefaultSeverity: 3, DefaultExposure: 3,
}, },
{ {
ID: "HP077", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Elektrischer Schlag bei Wartungsarbeiten", NameEN: "Electric shock during maintenance", ID: "HP077", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Elektrischer Schlag an offenen Baugruppen", NameEN: "Electric shock during maintenance",
RequiredComponentTags: []string{"high_voltage"}, RequiredComponentTags: []string{"high_voltage"},
RequiredLifecycles: []string{"maintenance", "fault_clearing"}, RequiredLifecycles: []string{"maintenance", "fault_clearing"},
GeneratedHazardCats: []string{"electrical_hazard"}, GeneratedHazardCats: []string{"electrical_hazard"},
@@ -195,7 +195,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 3, DefaultSeverity: 5, DefaultExposure: 3,
}, },
{ {
ID: "HP078", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Ergonomische Belastung bei Wartungszugang", NameEN: "Ergonomic strain at maintenance access", ID: "HP078", OperationalStates: []string{"maintenance"}, HumanRoles: []string{"maintenance_tech"}, NameDE: "Ergonomische Belastung durch schwierigen Zugang", NameEN: "Ergonomic strain at maintenance access",
RequiredComponentTags: []string{"structural_part"}, RequiredComponentTags: []string{"structural_part"},
RequiredLifecycles: []string{"maintenance"}, RequiredLifecycles: []string{"maintenance"},
GeneratedHazardCats: []string{"ergonomic"}, GeneratedHazardCats: []string{"ergonomic"},
@@ -273,7 +273,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 3, DefaultSeverity: 4, DefaultExposure: 3,
}, },
{ {
ID: "HP083", NameDE: "Unbeabsichtigter Hub bei Einrichtbetrieb", NameEN: "Unintended stroke in setup mode", ID: "HP083", NameDE: "Unbeabsichtigter Hub im manuellen Betrieb", NameEN: "Unintended stroke in setup mode",
RequiredComponentTags: []string{"moving_part", "crush_point"}, RequiredComponentTags: []string{"moving_part", "crush_point"},
RequiredLifecycles: []string{"setup"}, RequiredLifecycles: []string{"setup"},
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"}, GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
@@ -281,7 +281,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
Priority: 94, Priority: 94,
RequiresExpertCalculation: true, RequiresExpertCalculation: true,
ExpertHintDE: "Einrichtbetrieb nur mit reduzierter Geschwindigkeit und Zweihandschaltung.", ExpertHintDE: "Einrichtbetrieb nur mit reduzierter Geschwindigkeit und Zweihandschaltung.",
ScenarioDE: "Einrichter befindet sich im Werkzeugraum waehrend Testlauf im Einrichtbetrieb", ScenarioDE: "Person befindet sich im Werkzeugraum waehrend Testlauf",
TriggerDE: "Stossel fuehrt vollen Hub statt Tipphub aus wegen Softwarefehler oder Fehlbedienung", TriggerDE: "Stossel fuehrt vollen Hub statt Tipphub aus wegen Softwarefehler oder Fehlbedienung",
HarmDE: "Toedliches Quetschen oder Amputation durch vollen Pressenhub bei Anwesenheit", HarmDE: "Toedliches Quetschen oder Amputation durch vollen Pressenhub bei Anwesenheit",
AffectedDE: "Einrichter", AffectedDE: "Einrichter",
@@ -289,7 +289,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 3, DefaultSeverity: 5, DefaultExposure: 3,
}, },
{ {
ID: "HP084", NameDE: "Falsche Parametereinstellung nach Umruestung", NameEN: "Wrong parameters after changeover", ID: "HP084", NameDE: "Falsche Parametereinstellung nach Produktwechsel", NameEN: "Wrong parameters after changeover",
RequiredComponentTags: []string{"programmable"}, RequiredComponentTags: []string{"programmable"},
RequiredLifecycles: []string{"changeover", "setup"}, RequiredLifecycles: []string{"changeover", "setup"},
GeneratedHazardCats: []string{"safety_function_failure"}, GeneratedHazardCats: []string{"safety_function_failure"},
@@ -323,7 +323,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
// Transport / Montage / Demontage (HP086-HP090) // Transport / Montage / Demontage (HP086-HP090)
// ================================================================ // ================================================================
{ {
ID: "HP086", NameDE: "Kippen der Maschine beim Transport", NameEN: "Machine tipping during transport", ID: "HP086", NameDE: "Kippen der Maschine", NameEN: "Machine tipping during transport",
RequiredComponentTags: []string{"structural_part"}, RequiredComponentTags: []string{"structural_part"},
RequiredLifecycles: []string{"transport"}, RequiredLifecycles: []string{"transport"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -337,7 +337,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 2, DefaultSeverity: 5, DefaultExposure: 2,
}, },
{ {
ID: "HP087", NameDE: "Quetschen bei Montage/Aufstellung", NameEN: "Crushing during installation", ID: "HP087", NameDE: "Quetschen/Aufstellung", NameEN: "Crushing during installation",
RequiredComponentTags: []string{"high_force", "gravity_risk"}, RequiredComponentTags: []string{"high_force", "gravity_risk"},
RequiredLifecycles: []string{"assembly"}, RequiredLifecycles: []string{"assembly"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -351,7 +351,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
}, },
{ {
ID: "HP088", NameDE: "Unkontrollierte Bewegung bei Inbetriebnahme", NameEN: "Uncontrolled movement during commissioning", ID: "HP088", NameDE: "Unkontrollierte Bewegung beim Erststart", NameEN: "Uncontrolled movement during commissioning",
RequiredComponentTags: []string{"moving_part", "programmable"}, RequiredComponentTags: []string{"moving_part", "programmable"},
RequiredLifecycles: []string{"commissioning"}, RequiredLifecycles: []string{"commissioning"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -365,7 +365,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 4, DefaultExposure: 2, DefaultSeverity: 4, DefaultExposure: 2,
}, },
{ {
ID: "HP089", NameDE: "Restmedien bei Demontage (Oel, Gas, Druck)", NameEN: "Residual media during dismantling", ID: "HP089", NameDE: "Restmedien (Oel, Gas, Druck)", NameEN: "Residual media during dismantling",
RequiredComponentTags: []string{"hydraulic_part"}, RequiredComponentTags: []string{"hydraulic_part"},
RequiredLifecycles: []string{"decommissioning", "disposal"}, RequiredLifecycles: []string{"decommissioning", "disposal"},
GeneratedHazardCats: []string{"material_environmental", "pneumatic_hydraulic"}, GeneratedHazardCats: []string{"material_environmental", "pneumatic_hydraulic"},
@@ -379,7 +379,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 2, DefaultSeverity: 3, DefaultExposure: 2,
}, },
{ {
ID: "HP090", NameDE: "Scharfe Kanten bei Demontage", NameEN: "Sharp edges during dismantling", ID: "HP090", NameDE: "Scharfe Kanten an demontierten Teilen", NameEN: "Sharp edges during dismantling",
RequiredComponentTags: []string{"cutting_part"}, RequiredComponentTags: []string{"cutting_part"},
RequiredLifecycles: []string{"decommissioning", "disposal"}, RequiredLifecycles: []string{"decommissioning", "disposal"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -411,7 +411,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 2, DefaultExposure: 4, DefaultSeverity: 2, DefaultExposure: 4,
}, },
{ {
ID: "HP092", NameDE: "Chemische Exposition bei Reinigung", NameEN: "Chemical exposure during cleaning", ID: "HP092", NameDE: "Chemische Exposition durch Reinigungsmittel", NameEN: "Chemical exposure during cleaning",
RequiredComponentTags: []string{"chemical_risk"}, RequiredComponentTags: []string{"chemical_risk"},
RequiredLifecycles: []string{"cleaning"}, RequiredLifecycles: []string{"cleaning"},
GeneratedHazardCats: []string{"material_environmental"}, GeneratedHazardCats: []string{"material_environmental"},
@@ -425,7 +425,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 3, DefaultSeverity: 3, DefaultExposure: 3,
}, },
{ {
ID: "HP093", NameDE: "Einziehen in rotierende Teile bei Reinigung", NameEN: "Draw-in by rotating parts during cleaning", ID: "HP093", NameDE: "Einziehen in rotierende Teile bei laufender Maschine", NameEN: "Draw-in by rotating parts during cleaning",
RequiredComponentTags: []string{"rotating_part"}, RequiredComponentTags: []string{"rotating_part"},
RequiredLifecycles: []string{"cleaning"}, RequiredLifecycles: []string{"cleaning"},
ExcludedComponentTags: []string{"interlocked"}, ExcludedComponentTags: []string{"interlocked"},
@@ -262,7 +262,7 @@ func GetPlasticsMetalPatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"}, SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"}, SuggestedEvidenceIDs: []string{"E08", "E09"},
Priority: 95, Priority: 95, MachineTypes: []string{"lathe", "cnc", "metalworking"},
ScenarioDE: "Offene Haare, Krawatten, Aermel oder Handschuhe werden vom rotierenden Werkstueck oder Spannfutter erfasst.", ScenarioDE: "Offene Haare, Krawatten, Aermel oder Handschuhe werden vom rotierenden Werkstueck oder Spannfutter erfasst.",
TriggerDE: "Tragen von Handschuhen an der Drehmaschine, offene Haare, lose Kleidung", TriggerDE: "Tragen von Handschuhen an der Drehmaschine, offene Haare, lose Kleidung",
HarmDE: "Skalpierung, Armfraktur, Strangulation, toedliche Aufwickelverletzung", HarmDE: "Skalpierung, Armfraktur, Strangulation, toedliche Aufwickelverletzung",
@@ -124,7 +124,7 @@ func GetPressHazardPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M051", "M131"}, SuggestedMeasureIDs: []string{"M051", "M131"},
SuggestedEvidenceIDs: []string{"E01", "E08"}, SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 92, Priority: 92,
ScenarioDE: "Hydraulikspeicher entlaedt sich schlagartig bei Wartungsarbeiten oder Leitungsbruch.", ScenarioDE: "Hydraulikspeicher entlaedt sich schlagartig oder Leitungsbruch.",
TriggerDE: "Oeffnen einer Leitung ohne vorherige Druckentlastung, Berstversagen des Speichers.", TriggerDE: "Oeffnen einer Leitung ohne vorherige Druckentlastung, Berstversagen des Speichers.",
HarmDE: "Schwere Schnittverletzungen durch Oelstrahl, Augenverletzungen, Verbrennungen.", HarmDE: "Schwere Schnittverletzungen durch Oelstrahl, Augenverletzungen, Verbrennungen.",
AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.", AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.",
@@ -0,0 +1,359 @@
package iace
// GetRobotCellPatterns returns hazard patterns for industrial robot cells
// (non-collaborative) with safety fence, conveyors, and CNC machine tools.
// Based on typical ISO 10218-2 risk assessment scope for integrated robot systems.
//
// FORMULIERUNGSREGEL: Gefährdung und Szenario NEUTRAL formulieren — keine
// Lebensphasen im Text. Lebensphasen stehen in ApplicableLifecycles.
// HP1600-HP1649
func GetRobotCellPatterns() []HazardPattern {
return []HazardPattern{
// ================================================================
// Roboterarm — Quetschen/Einklemmen von Personen
// ================================================================
{
ID: "HP1600", NameDE: "Einklemmen zwischen Roboterarm und Anlage", NameEN: "Crushing between robot arm and fixed structure",
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M062", "M054"},
Priority: 99, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"normal_operation", "setup", "teach_mode", "cleaning", "maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person befindet sich im Bewegungsbereich des Roboterarms und wird zwischen Roboterarm und feststehenden Anlagenteilen eingeklemmt.",
TriggerDE: "Roboterarm bewegt sich waehrend Person im Gefahrenbereich steht.",
HarmDE: "Quetschungen, Knochenbrueche, innere Verletzungen durch Einklemmen von Koerperteilen.",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal, Reinigungspersonal",
ZoneDE: "Roboterarm, feststehende Anlagenteile innerhalb der Roboterzelle",
DefaultSeverity: 4, DefaultExposure: 3,
},
{
ID: "HP1602", NameDE: "Durchgreifen durch Schutzzaun zum Roboter", NameEN: "Reaching through safety fence to robot",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift ueber oder durch den Schutzzaun und erreicht den Bewegungsbereich des Roboterarms.",
TriggerDE: "Unzureichender Sicherheitsabstand zwischen Schutzzaun-Oberkante und Roboter-Schwenkbereich.",
HarmDE: "Quetschung von Hand oder Arm zwischen Roboterarm und feststehenden Teilen.",
AffectedDE: "Bedienpersonal, Reinigungspersonal",
ZoneDE: "Schutzzaun-Oberkante, Roboterarm",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1603", NameDE: "Eingeschlossen in Roboterzelle", NameEN: "Trapped inside robot cell",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M054", "M141"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person befindet sich in der Roboterzelle, Schutztuer wird geschlossen und Roboter startet. Person kann den Gefahrenbereich nicht rechtzeitig verlassen.",
TriggerDE: "Schutztuer schliesst waehrend Person im Innenraum. Wiederanlauf des Roboters ohne Quittierung.",
HarmDE: "Quetschungen, Stoss durch anlaufenden Roboter.",
AffectedDE: "Wartungspersonal, Einrichter, Reinigungspersonal",
ZoneDE: "Inneres der Roboterzelle",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1604", NameDE: "Roboterarm durchschlaegt Schutzzaun", NameEN: "Robot arm penetrates safety fence",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M002"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Roboterarm ueberschreitet Bewegungsbereich und trifft Schutzzaun. Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
TriggerDE: "Fehler in der Bahnplanung oder Ausfall der Achsbegrenzung.",
HarmDE: "Teile des Schutzzauns werden herausgeschleudert, Person ausserhalb wird getroffen.",
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1605", NameDE: "Stoss durch Werkzeug/Greifer im Einrichtbetrieb", NameEN: "Impact by tool/gripper during setup",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054"},
Priority: 98, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"teach_mode", "setup", "changeover", "fault_clearing"},
ScenarioDE: "Person steht im Bewegungsbereich des Roboterarms und wird von bewegtem Werkzeug oder Greifer getroffen. Geschwindigkeitsreduzierung im Einrichtbetrieb reicht nicht aus.",
TriggerDE: "Roboter bewegt Werkzeug/Greifer mit unerwartet hoher Geschwindigkeit oder in unerwartete Richtung.",
HarmDE: "Prellungen, Quetschungen durch Kontakt mit Werkzeug/Greifer am Roboterarm.",
AffectedDE: "Einrichter, Programmierer, Wartungspersonal",
ZoneDE: "Inneres der Roboterzelle, Schwenkbereich Werkzeug/Greifer",
DefaultSeverity: 3, DefaultExposure: 3,
},
// ================================================================
// Greifer / Werkstueck
// ================================================================
{
ID: "HP1610", NameDE: "Quetschen im Greiferbereich", NameEN: "Crushing in gripper area",
RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 99, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Person greift in den Bereich des Greifers. Hand wird zwischen Greifbacken und Werkstueck eingeklemmt.",
TriggerDE: "Greiferbacken schliessen waehrend Koerperteil im Greifbereich ist.",
HarmDE: "Quetschung oder Amputation von Fingern durch Greifkraft.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Greifer des Roboterarms, Werkstueckaufnahme",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1611", NameDE: "Werkstueck faellt aus Greifer herab", NameEN: "Workpiece falls from gripper",
RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M007", "M141"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Greifer verliert das Werkstueck waehrend des Transports. Werkstueck faellt herab und trifft Person unterhalb des Roboterarms.",
TriggerDE: "Werkstueck faellt aus Greifer und trifft Person unterhalb des Roboterarms.",
HarmDE: "Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht und Fallhoehe.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bereich unterhalb des Greifer/Roboterarms",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1612", NameDE: "Werkstueck durchschlaegt Einhausung", NameEN: "Workpiece penetrates enclosure",
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M141"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Greifer versagt und Werkstueck wird in Richtung Schutzzaun geschleudert. Person ausserhalb der Zelle wird von durchschlagendem Werkstueck getroffen.",
TriggerDE: "Werkstueck wird durch Roboterbewegung weggeschleudert und durchschlaegt die Schutzeinrichtung.",
HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Werkstueck getroffen.",
AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle",
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
// ================================================================
// Foerderbaender / Werkstueckzu-/-auslauf
// ================================================================
{
ID: "HP1620", NameDE: "Quetschen an Foerderband-Einlauf", NameEN: "Crushing at conveyor infeed",
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061", "M003"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift an Foerderband und wird zwischen beweglichen und feststehenden Teilen eingeklemmt.",
TriggerDE: "Hand oder Finger geraten zwischen Band und Umlenkrolle oder zwischen Werkstueck und Tunnelrahmen.",
HarmDE: "Quetschung von Fingern, Einzug von Kleidung oder Haaren.",
AffectedDE: "Bedienpersonal, Reinigungspersonal",
ZoneDE: "Foerderbaender, Bandein- und -auslauf",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP1621", NameDE: "Durchgreifen durch Foerderband-Oeffnung in Schutzzaun", NameEN: "Reaching through conveyor opening in fence",
RequiredComponentTags: []string{"entanglement_risk", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"},
ScenarioDE: "Person greift durch die Oeffnung im Schutzzaun fuer die Foerderbaender in den Gefahrenbereich des Roboters.",
TriggerDE: "Oeffnung ist zu gross oder Sicherheitsabstand zum Roboter-Schwenkbereich ist zu gering.",
HarmDE: "Quetschung von Hand oder Arm durch Roboterarm oder bewegte Maschinenteile.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Oeffnung der Foerderbaender im Schutzzaun, Roboterbereich dahinter",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1622", NameDE: "Herunterfallen von Werkstueck am Bandende", NameEN: "Workpiece falling off conveyor end",
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M008"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup"},
ScenarioDE: "Werkstueck faehrt ueber das Ende des Transportbandes hinaus, faellt herab und trifft Person am Be-/Entladeplatz.",
TriggerDE: "Mechanischer Anschlag fehlt oder ist beschaedigt.",
HarmDE: "Prellungen, Quetschung von Fuessen durch herabfallendes Werkstueck.",
AffectedDE: "Bedienpersonal am Be-/Entladeplatz",
ZoneDE: "Ende der Transportbaender, Be-/Entladeplatz",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// Scharfe Kanten / Allgemein
// ================================================================
{
ID: "HP1625", NameDE: "Schneiden an scharfen Kanten der Einhausung", NameEN: "Cutting on sharp enclosure edges",
RequiredComponentTags: []string{"guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person schneidet sich an nicht entgrateten oder scharfkantigen Blechen der Einhausung oder Verkleidung.",
TriggerDE: "Zugaengliche Kanten sind nicht gerundet oder gebrochen.",
HarmDE: "Schnittwunden an Haenden und Armen.",
AffectedDE: "Bedienpersonal, Wartungspersonal, Reinigungspersonal",
ZoneDE: "Zugaengliche Kanten der Maschine und Einhausung",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// Pneumatik / Druckluft
// ================================================================
{
ID: "HP1630", NameDE: "Pneumatikschlauch springt unter Druck ab", NameEN: "Pressurized hose comes loose",
RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Pneumatikschlauch der Automation springt unter Druck ab und trifft eine Person (Peitscheneffekt).",
TriggerDE: "Befestigung loest sich, Verschraubung wird undicht, Materialermuedung des Schlauchs.",
HarmDE: "Prellungen, Augenverletzungen durch abspringenden Schlauch.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Pneumatikschlaeuche der Automation",
DefaultSeverity: 2, DefaultExposure: 2,
},
{
ID: "HP1631", NameDE: "Restdruck in Pneumatik nach Abschaltung", NameEN: "Residual pressure in pneumatics after shutdown",
RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480", "M141"},
Priority: 97,
ApplicableLifecycles: []string{"maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person loest druckbeaufschlagte Pneumatik-Komponenten die nach Abschaltung noch unter Druck stehen. Teile fliegen unkontrolliert weg und treffen die Person.",
TriggerDE: "Fehlende Druckentlastung. Gesperrte Rueckschlagventile halten Druck.",
HarmDE: "Person wird von wegfliegenden Teilen oder unkontrolliert loesenden Verbindungen getroffen. Prellungen, Schnittverletzungen.",
AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Pneumatikschlaeuche und -komponenten",
DefaultSeverity: 2, DefaultExposure: 2,
},
// ================================================================
// Kuehlschmierstoff (KSS)
// ================================================================
{
ID: "HP1606", NameDE: "Quetschen/Scheren durch Greifer im Einrichtbetrieb", NameEN: "Crushing/shearing by gripper during setup",
RequiredComponentTags: []string{"clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054"},
Priority: 98, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
ApplicableLifecycles: []string{"teach_mode", "setup", "changeover", "fault_clearing"},
ScenarioDE: "Einrichter steht im Schwenkbereich des Roboterarms und wird von bewegtem Greifer oder daran befestigtem Werkzeug verletzt.",
TriggerDE: "Reduzierte Geschwindigkeit im Einrichtbetrieb reicht nicht aus oder wird nicht aktiviert.",
HarmDE: "Quetschung, Schnittverletzung durch Greiferkanten oder Werkzeug am Roboter.",
AffectedDE: "Einrichter, Programmierer",
ZoneDE: "Inneres der Roboterzelle, Greifer/Werkzeug am Roboterarm",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1634", NameDE: "KSS-Pumpe spritzt bei geoeffneter Schutztuer", NameEN: "Coolant pump sprays with open guard door",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 96, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Niederdruck-Pumpe fuer Bettspuelung laeuft an waehrend Schutztuer geoeffnet ist. Person bekommt KSS-Spritzer ins Auge oder Gesicht.",
TriggerDE: "Pumpe startet automatisch, kein Verriegelungssignal von Schutztuer zur KSS-Pumpe.",
HarmDE: "Augenverletzung durch KSS-Spritzer, Hautreizung.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszelle, Austrittsduesen der Bettspuelung",
DefaultSeverity: 1, DefaultExposure: 3,
},
{
ID: "HP1633", NameDE: "KSS-Versorgungsschlauch platzt oder reisst ab", NameEN: "Coolant supply hose bursts or tears off",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "maintenance", "fault_clearing"},
ScenarioDE: "KSS-Versorgungsschlauch reisst ab oder platzt. Person in der Naehe wird von abspringendem Schlauch oder KSS-Strahl unter Druck getroffen.",
TriggerDE: "Materialermuedung, mechanische Beschaedigung, fehlerhafte Befestigung des Schlauchs.",
HarmDE: "Person wird von KSS-Strahl getroffen. Einstichverletzung, Hautreizung, Rutschgefahr.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems, Verbindungsstellen",
DefaultSeverity: 2, DefaultExposure: 2,
},
{
ID: "HP1635", NameDE: "Ausrutschen durch KSS-Leckage", NameEN: "Slipping due to coolant leakage",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M420"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Kuehlschmierstoff tritt aus und bildet rutschigen Belag auf dem Boden. Person rutscht aus und stuerzt.",
TriggerDE: "Leckage an Schlauchverbindung, Dichtungsversagen.",
HarmDE: "Ausrutschen und Sturz, Prellungen, Knochenbrueche.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Boden um Bearbeitungszentrum und Kuehlschmierstoffanlage",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP1636", NameDE: "Hautkontakt mit Kuehlschmierstoff", NameEN: "Skin contact with coolant",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person kommt bei Arbeiten am Bearbeitungszentrum oder der Roboterzelle mit Kuehlschmierstoff in Beruehrung.",
TriggerDE: "Hautkontakt beim Reinigen, Werkzeugwechsel oder Beseitigung von Stoerungen.",
HarmDE: "Hautirritationen, allergische Reaktionen, bei laengerer Exposition Ekzeme.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszentrum, Roboterzelle im Bereich der Beladetuer",
DefaultSeverity: 1, DefaultExposure: 3,
},
{
ID: "HP1637", NameDE: "Einatmen von KSS-Aerosolen", NameEN: "Inhalation of coolant aerosols",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"material_environmental"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
ScenarioDE: "Person oeffnet Schutztuer der Bearbeitungszelle und atmet freigesetzte KSS-Aerosole ein.",
TriggerDE: "Oeffnen der Schutztuer nach Bearbeitungsvorgang, unzureichende Absaugung.",
HarmDE: "Person atmet KSS-Aerosole ein. Atembeschwerden, Reizung der Atemwege, bei chronischer Exposition Atemwegserkrankungen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer",
DefaultSeverity: 1, DefaultExposure: 3,
},
// ================================================================
// Elektrisch (Roboterzelle-spezifisch)
// ================================================================
{
ID: "HP1640", NameDE: "Direktes Beruehren spannungsfuehrender Teile", NameEN: "Direct contact with live parts",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M265", "M089", "M088", "M139", "M475"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Person beruehrt spannungsfuehrende Teile der Anlage die nicht ausreichend isoliert oder abgedeckt sind.",
TriggerDE: "Beschaedigte Isolation, fehlende Abdeckung, ungesicherter Schaltschrank.",
HarmDE: "Person erleidet elektrischen Schlag. Herzkammerflimmern, Verbrennungen, bei Hochspannung Todesfolge.",
AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Zugaengliche Kabel, Klemmen, Schaltschrank",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1641", NameDE: "Gefaehrliche Beruehrungsspannung durch Schutzleiterfehler", NameEN: "Dangerous touch voltage due to PE failure",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{"electrical"},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M475", "M476"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Schutzleiter ist unterbrochen. Person beruehrt das Maschinengehaeuse und erleidet elektrischen Schlag durch gefaehrliche Beruehrungsspannung.",
TriggerDE: "Schutzleiterunterbrechung durch mechanische Beschaedigung oder fehlerhafte Installation.",
HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder leitfaehiger Oberflaechen.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Beruehrbare leitfaehige Oberflaechen der Anlage",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1642", NameDE: "Kabelbrand durch Ueberlast oder Kurzschluss", NameEN: "Cable fire from overload or short circuit",
RequiredComponentTags: []string{},
RequiredEnergyTags: []string{"electrical"},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
ScenarioDE: "Kabel ueberhitzt und entzuendet sich durch Ueberlast oder fehlenden Ueberstromschutz. Person wird durch Brand oder toxische Gase verletzt.",
TriggerDE: "Dauerhafter Betrieb nahe der Belastungsgrenze, falsch dimensionierte Sicherung.",
HarmDE: "Brand, Rauchentwicklung, Verletzung durch Feuer oder toxische Gase.",
AffectedDE: "Alle Personen im Bereich der Anlage",
ZoneDE: "Kabel und Leitungen der Anlage",
DefaultSeverity: 3, DefaultExposure: 2,
},
}
}
@@ -0,0 +1,465 @@
package iace
// GetRobotCellPatternsExt returns additional hazard patterns for robot cells.
// These cover specific scenarios identified through GT benchmark gaps.
// HP1650-HP1699
func GetRobotCellPatternsExt() []HazardPattern {
return []HazardPattern{
// ================================================================
// Roboterarm — Spezifische Szenarien (GT-Gaps)
// ================================================================
{
ID: "HP1650", NameDE: "Roboterarm durchschlaegt Bewegungsbegrenzung", NameEN: "Robot arm exceeds motion limit",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061", "M054"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
ScenarioDE: "Roboterarm ueberschreitet Bewegungsbegrenzung und trifft Schutzzaun. Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
TriggerDE: "Softwareendschalter versagt, Achsbegrenzung (DCS) fehlerhaft konfiguriert.",
HarmDE: "Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1651", NameDE: "Wiederanlauf Roboter waehrend Person in Zelle", NameEN: "Robot restart while person inside cell",
RequiredComponentTags: []string{"moving_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061", "M141"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing", "changeover"},
ScenarioDE: "Person befindet sich in der Roboterzelle. Schutztuer wird geschlossen und Roboter startet ohne dass sichergestellt ist, dass niemand im Gefahrenbereich ist.",
TriggerDE: "Fehlende Quittierungspflicht, kein Personenscanner, Schutztuer ohne Sicherheitszuhaltung.",
HarmDE: "Schwere Quetschungen, Knochenbrueche durch anlaufenden Roboter.",
AffectedDE: "Wartungspersonal, Einrichter, Reinigungspersonal",
ZoneDE: "Inneres der Roboterzelle, Roboterarm",
DefaultSeverity: 4, DefaultExposure: 3,
},
{
ID: "HP1652", NameDE: "Quetschen durch Werkzeug/Greifer am Roboter im Betrieb", NameEN: "Crushing by tool/gripper during operation",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning"},
ScenarioDE: "Person wird von bewegtem Werkzeug oder Greifer am Roboterarm getroffen oder zwischen Werkzeug und feststehenden Teilen eingeklemmt.",
TriggerDE: "Roboter bewegt Werkzeug/Greifer waehrend Person im Schwenkbereich.",
HarmDE: "Quetschungen, Schnittverletzungen, Prellungen durch Werkzeug/Greifer.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Inneres der Roboterzelle, Greifer/Werkzeug des Roboterarms",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1653", NameDE: "Quetschen durch Werkstück am Robotergreifer", NameEN: "Crushing by workpiece on robot gripper",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Person wird von sich bewegendem Werkstueck am Robotergreifer getroffen oder zwischen Werkstueck und feststehenden Anlagenteilen eingeklemmt.",
TriggerDE: "Roboter transportiert Werkstueck, Person steht im Schwenkbereich.",
HarmDE: "Quetschungen, Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Inneres der Roboterzelle, Greifer des Roboterarms",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1654", NameDE: "Werkstück/Werkzeug durchschlaegt Schutzzaun", NameEN: "Workpiece/tool penetrates safety fence",
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Greifer versagt und Werkstueck/Werkzeug wird Richtung Schutzzaun geschleudert. Person ausserhalb wird getroffen.",
TriggerDE: "Greifkraftverlust, Druckausfall, oelige Oberflaeche des Werkstuecks.",
HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Teil getroffen.",
AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle",
ZoneDE: "Schutzzaun, Bereich ausserhalb der Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1655", NameDE: "Durchgreifen ueber Schutzzaun zum Greifer/Werkstueck", NameEN: "Reaching over fence to gripper/workpiece",
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift ueber den Schutzzaun und erreicht den Greifer oder das Werkstueck am Roboterarm.",
TriggerDE: "Sicherheitsabstand zwischen Zaun-Oberkante und Greifer/Werkstueck zu gering.",
HarmDE: "Quetschung von Hand oder Arm zwischen Greifer/Werkstueck und feststehenden Teilen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Schutzzaun-Oberkante, Greifer/Werkstueck am Roboterarm",
DefaultSeverity: 3, DefaultExposure: 2,
},
// ================================================================
// Zentriergreifer an Förderbändern
// ================================================================
{
ID: "HP1660", NameDE: "Quetschen am Zentriergreifer von aussen", NameEN: "Crushing at centering gripper from outside",
RequiredComponentTags: []string{"clamping_part", "entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person befindet sich ausserhalb der Roboterzelle und greift an die Zentriereinheit (fest montierter Greifer am Foerderband).",
TriggerDE: "Zentriergreifer schliesst waehrend Hand im Greifbereich. Unzureichender Abstand zwischen Greifer und Schutzzaun-Oeffnung.",
HarmDE: "Quetschung von Fingern oder Hand zwischen Greifbacken und Werkstueck.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Zentriereinheit an Foerderbaendern, Schutzzaun-Oeffnung",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP1661", NameDE: "Quetschen am Zentriergreifer von innen", NameEN: "Crushing at centering gripper from inside cell",
RequiredComponentTags: []string{"clamping_part", "entanglement_risk", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "cleaning", "fault_clearing"},
ScenarioDE: "Person befindet sich innerhalb der Roboterzelle und greift an die Zentriereinheit am Foerderband.",
TriggerDE: "Schutztuer geoeffnet, aber Zentriergreifer wird nicht automatisch stillgesetzt.",
HarmDE: "Quetschung von Fingern oder Hand zwischen Greifbacken und Werkstueck.",
AffectedDE: "Wartungspersonal, Reinigungspersonal",
ZoneDE: "Zentriereinheit an Foerderbaendern innerhalb der Roboterzelle",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// Bearbeitungszentrum (Robodrill/WZM) innerhalb Roboterzelle
// ================================================================
{
ID: "HP1665", NameDE: "Quetschen an Beladetuer der Werkzeugmaschine", NameEN: "Crushing at machine tool loading door",
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive", "robotics_cobot"},
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift durch die Beladetuer der Werkzeugmaschine. Beladetuer schliesst sich oder bewegliche Teile im Innenraum starten.",
TriggerDE: "Tuerpositionsschalter nicht in Robotersteuerung eingebunden, fehlende Verriegelung.",
HarmDE: "Quetschung von Hand/Arm an Beladetuer oder durch bewegliche Teile im Bearbeitungsraum.",
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
ZoneDE: "Beladetuer der Werkzeugmaschine, Bearbeitungsraum",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1666", NameDE: "Quetschen/Scheren im Bearbeitungsraum der WZM", NameEN: "Crushing/shearing inside machine tool workspace",
RequiredComponentTags: []string{"moving_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054"},
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive", "robotics_cobot"},
ApplicableLifecycles: []string{"setup", "maintenance", "fault_clearing"},
ScenarioDE: "Person greift in den Bearbeitungsraum der Werkzeugmaschine und wird von beweglichen Achsen, Werkzeug oder Spannvorrichtung verletzt.",
TriggerDE: "Bewegliche Teile starten waehrend Hand im Bearbeitungsraum (Einrichtbetrieb, Stoerungsbeseitigung).",
HarmDE: "Quetschungen, Schnittverletzungen durch rotierende Werkzeuge, Scheren an Achsbewegungen.",
AffectedDE: "Einrichter, Wartungspersonal",
ZoneDE: "Bearbeitungsraum der Werkzeugmaschine, Achsen, Werkzeug, Spannvorrichtung",
DefaultSeverity: 3, DefaultExposure: 3,
},
// ================================================================
// KSS-Spritzer / Druckluft in Bearbeitungszelle
// ================================================================
{
ID: "HP1670", NameDE: "KSS-Spritzer in Augen/Gesicht", NameEN: "Coolant splash to eyes/face",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person bekommt Kuehlschmierstoff-Spritzer ins Auge oder Gesicht beim Oeffnen der Bearbeitungszelle oder bei laufender Bettspuelung.",
TriggerDE: "KSS-Pumpe laeuft waehrend Schutztuer geoeffnet ist, Austrittsduese nicht korrekt gerichtet.",
HarmDE: "Augenverletzung, Reizung der Bindehaut, bei Hochdruck-KSS ernsthafte Augenschaeden.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer, Austrittsduesen",
DefaultSeverity: 2, DefaultExposure: 3,
},
{
ID: "HP1671", NameDE: "Druckluft-Verletzung in Bearbeitungszelle", NameEN: "Compressed air injury in machining cell",
RequiredComponentTags: []string{"pinch_point"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Person wird von ausstroemender Druckluft oder aufgewirbelten Bearbeitungsrueckstaenden in der Bearbeitungszelle verletzt.",
TriggerDE: "Druckluftreinigungsduese aktiv waehrend Schutztuer geoeffnet, Spaene oder Partikel werden aufgewirbelt.",
HarmDE: "Augenverletzung durch Spaene, Hautverletzung durch Druckluftstoss.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bearbeitungszelle, Druckluftreinigungsduesen",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// KSS-Schläuche unter Druck
// ================================================================
{
ID: "HP1675", NameDE: "KSS-Schlauch bersten oder abspringen", NameEN: "Coolant hose burst or detachment",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Schlauch der Kuehlschmierstoffversorgung zwischen Aufbereitungsanlage und Bearbeitungszentrum platzt oder springt unter Druck ab.",
TriggerDE: "Materialermuedung, Ueberdruck, fehlerhafte Befestigung, mechanische Beschaedigung des Schlauchs.",
HarmDE: "Person wird von abspringendem Schlauch getroffen (Peitscheneffekt). KSS-Spritzer unter Druck verletzen Haut und Augen. Rutschgefahr durch austretenden KSS.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems",
DefaultSeverity: 2, DefaultExposure: 2,
},
// ================================================================
// Quetschen am Förderband — Werkstück/Tunnel
// ================================================================
{
ID: "HP1680", NameDE: "Quetschen zwischen Werkstueck und Tunnel am Foerderband", NameEN: "Crushing between workpiece and conveyor tunnel",
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M002", "M003"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"},
ScenarioDE: "Person greift an den Tunnel/Rahmen des Foerderbandes und wird von einem darauf bewegten Werkstueck eingequetscht.",
TriggerDE: "Zu geringer Abstand zwischen Werkstueck und Tunnel/Rahmen, scharfe Kanten an Tunneleingang.",
HarmDE: "Quetschung von Fingern zwischen Werkstueck und Rahmen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Foerderband-Tunnel, Werkstück auf dem Band",
DefaultSeverity: 2, DefaultExposure: 3,
},
// ================================================================
// Elektrisch — Spezifische Szenarien
// ================================================================
{
ID: "HP1685", NameDE: "Indirektes Beruehren durch Schutzleiterunterbrechung", NameEN: "Indirect contact due to PE interruption",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M475", "M476"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "Schutzleiter ist unterbrochen. Person beruehrt leitfaehige Maschinenteile und erleidet elektrischen Schlag.",
TriggerDE: "Mechanische Beschaedigung des Schutzleiters, korrodierte Verbindung, fehlerhafte Installation.",
HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder anderer leitfaehiger Teile.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Beruehrbare leitfaehige Oberflaechen der Anlage",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1686", NameDE: "Direktes Beruehren im Schaltschrank", NameEN: "Direct contact inside control cabinet",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009"},
Priority: 98,
ApplicableLifecycles: []string{"maintenance", "fault_clearing", "commissioning"},
ScenarioDE: "Person beruehrt spannungsfuehrende Teile bei geoeffnetem Schaltschrank. Leiter um Bedienelemente sind nicht fingersicher geschuetzt.",
TriggerDE: "Schaltschranktuer geoeffnet fuer Wartung oder Fehlersuche, unzureichender Beruehrungsschutz.",
HarmDE: "Person erleidet elektrischen Schlag. Herzkammerflimmern, Verbrennungen, bei Hochspannung Todesfolge.",
AffectedDE: "Wartungspersonal, Elektrofachkraefte",
ZoneDE: "Schaltschrank-Innenraum, Klemmen, Sammelschienen",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1687", NameDE: "Brand durch eindringende Fluessigkeit", NameEN: "Fire from liquid ingress causing short circuit",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M009"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "cleaning"},
ScenarioDE: "Fluessigkeit dringt in elektrische Komponenten ein und verursacht Kurzschluss. Person wird durch Brand oder Rauchentwicklung gefaehrdet.",
TriggerDE: "Reinigung mit Wasser, KSS-Leckage tropft auf Schaltschrank oder Steuerungskomponenten.",
HarmDE: "Person wird durch Brand, Flammen oder toxische Rauchgase verletzt. Verbrennungen, Rauchvergiftung.",
AffectedDE: "Bedienpersonal, Reinigungspersonal",
ZoneDE: "Schaltgeraetekombinationen, elektrische Komponenten unterhalb von Rohrleitungen",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1688", NameDE: "Gefaehrliche Beruehrungsspannung durch Potentialunterschiede", NameEN: "Dangerous touch voltage from potential differences",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M475", "M477", "M138", "M329"},
Priority: 96,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Person beruehrt gleichzeitig Anlagenteile mit unterschiedlichem Potential und erleidet elektrischen Schlag.",
TriggerDE: "Fehlender Potentialausgleich zwischen Anlagenteilen verschiedener Hersteller.",
HarmDE: "Elektrischer Schlag bei gleichzeitigem Beruehren von Teilen unterschiedlichen Potentials.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Elektrisch leitfaehige Oberflaechen verschiedener Anlagenteile",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1689", NameDE: "Fehlerstromschutz an Steckdosenstromkreisen", NameEN: "RCD protection at socket circuits",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M475"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Defektes Geraet wird an Steckdose der Maschine angeschlossen. Fehlerstrom fliesst ueber den Koerper der beruerenden Person.",
TriggerDE: "Fehlende Fehlerstrom-Schutzeinrichtung (RCD) an Steckdosenstromkreisen der Maschine.",
HarmDE: "Person erleidet elektrischen Schlag durch Fehlerstrom. Herzkammerflimmern, potentiell toedlich.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Steckdosen der Maschine, angeschlossene Betriebsmittel",
DefaultSeverity: 4, DefaultExposure: 2,
},
// ================================================================
// Ergonomie
// ================================================================
{
ID: "HP1690", NameDE: "Ergonomisch unguenstige Einlegeposition", NameEN: "Unfavorable ergonomic loading position",
RequiredComponentTags: []string{"entanglement_risk"},
GeneratedHazardCats: []string{"ergonomic_hazard"},
SuggestedMeasureIDs: []string{},
Priority: 85,
ApplicableLifecycles: []string{"normal_operation"},
ScenarioDE: "Person muss Werkstuecke in ergonomisch unguenstiger Hoehe oder Reichweite auf das Foerderband auflegen oder entnehmen.",
TriggerDE: "Bandhoehe nicht auf ergonomische Handhabung ausgelegt, schwere Werkstuecke.",
HarmDE: "Person erleidet Rueckenbeschwerden und Schulterbelastung durch wiederholte Fehlhaltung. Langfristig Muskel-Skelett-Erkrankungen.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Beladebereich der Foerderbaender",
DefaultSeverity: 2, DefaultExposure: 4,
},
{
ID: "HP1691", NameDE: "Unergonomische Position der Bedienelemente", NameEN: "Unfavorable ergonomic position of controls",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"ergonomic_hazard"},
SuggestedMeasureIDs: []string{},
Priority: 85,
ApplicableLifecycles: []string{"normal_operation", "setup"},
ScenarioDE: "Person bedient Anlage in ergonomisch unguenstiger Position ueber laengere Zeit.",
TriggerDE: "Bedienfeld zu hoch, zu niedrig oder seitlich versetzt montiert.",
HarmDE: "Person erleidet Nacken- und Schulterbelastung durch unguenstige Bedienposition. Langfristig Haltungsschaeden.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Bedienfeld, HMI, Betriebsartenwahlschalter",
DefaultSeverity: 2, DefaultExposure: 4,
},
// ================================================================
// Thermisch / Verbrennung
// ================================================================
{
ID: "HP1695", NameDE: "Verbrennung an heissen Werkstuecken", NameEN: "Burn from hot workpieces",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"thermal_hazard"},
SuggestedMeasureIDs: []string{"M141"},
Priority: 88, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Person beruehrt heisse Werkstuecke die durch die Bearbeitung erwaermt wurden.",
TriggerDE: "Manuelle Entnahme von Werkstuecken ohne Wartezeit oder Schutzhandschuhe.",
HarmDE: "Verbrennungen an Haenden und Fingern.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Werkstueckausgabe, Entnahmeplatz",
DefaultSeverity: 1, DefaultExposure: 3,
},
// ================================================================
// Tragfähigkeit / Aufstellung
// ================================================================
{
ID: "HP1697", NameDE: "Anlage bricht durch unzureichenden Untergrund ein", NameEN: "Machine collapses through insufficient floor",
RequiredComponentTags: []string{"high_force"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{},
Priority: 88,
ApplicableLifecycles: []string{"normal_operation", "setup", "commissioning"},
ScenarioDE: "Untergrund bricht unter dem Maschinengewicht ein. Personen im Umfeld werden von kippender oder absackender Anlage eingeklemmt.",
TriggerDE: "Boden nicht auf maximale statische und dynamische Lasten der Maschine ausgelegt.",
HarmDE: "Anlage bricht ein, Quetschung von Personen im Umfeld.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Bereich um die Maschine, Aufstellflaeche",
DefaultSeverity: 4, DefaultExposure: 1,
},
// ================================================================
// Elektrisch — Kriechstrecken + EMV
// ================================================================
{
ID: "HP1698", NameDE: "Kurzschluss durch unzureichende Luft-/Kriechstrecken", NameEN: "Short circuit from insufficient creepage/clearance",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M477"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
ScenarioDE: "Unzureichende Luft-/Kriechstrecken fuehren bei Verschmutzung zu Kriechstroemen. Person beruehrt betroffene Teile und erleidet elektrischen Schlag.",
TriggerDE: "Verschmutzungsgrad hoeher als bei der Dimensionierung angenommen, Feuchtigkeit, alterungsbedingte Veraenderung.",
HarmDE: "Gefaehrliche Beruehrungsspannung an beruehrbaren Teilen, Kurzschluss, Brand.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Schaltgeraetekombinationen, elektrische Anschluesse",
DefaultSeverity: 4, DefaultExposure: 2,
},
{
ID: "HP1699", NameDE: "EMV-Stoereinfluss auf Sicherheitsfunktionen", NameEN: "EMC interference with safety functions",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"radiation_hazard"},
SuggestedMeasureIDs: []string{"M478", "M479"},
Priority: 97,
ApplicableLifecycles: []string{"normal_operation", "setup"},
ScenarioDE: "EMV-Stoerungen verursachen unerwartete Maschinenbewegungen. Person im Gefahrenbereich wird von unkontrolliert bewegten Teilen getroffen.",
TriggerDE: "Unzureichende EMV-Schirmung, nicht-fachgerechte Verkabelung, externe Stoerquellen.",
HarmDE: "Unkontrollierte Bewegung von Achsen, Werkzeug oder Roboterarm durch Steuerungsfehler.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Bearbeitungsbereich, sicherheitsrelevante Steuerungen",
DefaultSeverity: 3, DefaultExposure: 2,
},
// ================================================================
// Differenzierte Patterns (GT-Benchmark: gleiche Zone, anderes Szenario)
// ================================================================
{
ID: "HP1700", NameDE: "Getroffen von bewegtem Werkzeug/Greifer am Roboter", NameEN: "Struck by moving tool/gripper on robot",
RequiredComponentTags: []string{"moving_part", "clamping_part"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M054", "M061"},
Priority: 99,
ApplicableLifecycles: []string{"normal_operation", "setup", "teach_mode", "cleaning"},
ScenarioDE: "Person steht im Bewegungsbereich des Roboterarms und wird von bewegtem Werkzeug oder Greifer getroffen.",
TriggerDE: "Roboter schwenkt mit Werkzeug/Greifer in Richtung Person.",
HarmDE: "Prellungen, Schnittverletzungen durch Werkzeugkanten, Knochenbrueche.",
AffectedDE: "Bedienpersonal, Einrichter",
ZoneDE: "Inneres der Roboterzelle, Schwenkbereich Werkzeug/Greifer",
DefaultSeverity: 3, DefaultExposure: 3,
},
{
ID: "HP1701", NameDE: "Greifer/Werkzeug durchschlaegt Schutzzaun", NameEN: "Gripper/tool penetrates safety fence",
RequiredComponentTags: []string{"clamping_part", "guard"},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 98,
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
ScenarioDE: "Greifer oder Werkzeug am Roboterarm durchschlaegt den Schutzzaun und trifft Person ausserhalb der Zelle.",
TriggerDE: "Bewegungsbegrenzung versagt, Schutzzaun nicht auf Aufprallenergie ausgelegt.",
HarmDE: "Person ausserhalb wird von Greifer/Werkzeug oder Zaunteilen getroffen.",
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
ZoneDE: "Bereich um Roboterarm ausserhalb der Roboterzelle",
DefaultSeverity: 3, DefaultExposure: 2,
},
{
ID: "HP1702", NameDE: "KSS-Schlauch platzt unter Druck", NameEN: "Coolant hose bursts under pressure",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M480"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "maintenance", "fault_clearing"},
ScenarioDE: "KSS-Schlauch platzt und spritzt Kuehlschmierstoff unter Druck. Person in der Naehe wird von KSS-Strahl getroffen.",
TriggerDE: "Alterung, Beschaedigung oder Ueberdruck fuehrt zum Versagen des Schlauchs.",
HarmDE: "Einstichverletzung durch KSS-Strahl unter Druck, Augenverletzung, Rutschgefahr.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems",
DefaultSeverity: 2, DefaultExposure: 2,
},
{
ID: "HP1703", NameDE: "KSS-Bettspuelung bei geoeffneter Schutztuer", NameEN: "Coolant bed wash with open guard door",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M061"},
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
ScenarioDE: "KSS-Pumpe laeuft bei geoeffneter Schutztuer. Person vor der Bearbeitungszelle bekommt KSS-Spritzer ins Auge oder Gesicht.",
TriggerDE: "Kein automatisches Abschalten der KSS-Pumpe bei geoeffneter Tuer.",
HarmDE: "KSS-Spritzer in Augen oder Gesicht, Rutschgefahr durch austretenden KSS.",
AffectedDE: "Bedienpersonal, Wartungspersonal",
ZoneDE: "Inneres des Bearbeitungszentrums, Bereich vor der Schutztuer",
DefaultSeverity: 1, DefaultExposure: 3,
},
{
ID: "HP1704", NameDE: "Brand durch KSS-Leckage auf elektrische Komponenten", NameEN: "Fire from coolant leakage on electrical components",
RequiredComponentTags: []string{},
GeneratedHazardCats: []string{"electrical_hazard"},
SuggestedMeasureIDs: []string{"M480", "M009"},
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive"},
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance"},
ScenarioDE: "KSS-Leckage tropft auf elektrische Komponenten und verursacht Kurzschluss. Person wird durch Brand oder Rauchentwicklung gefaehrdet.",
TriggerDE: "KSS-Leitung undicht oberhalb elektrischer Komponenten, tropft auf Klemmen oder Leiterplatten.",
HarmDE: "Person wird durch Brand, Flammen oder toxische Rauchgase verletzt. Verbrennungen, Rauchvergiftung.",
AffectedDE: "Bedienpersonal",
ZoneDE: "Spannungsfuehrende Teile unterhalb/angrenzend von KSS-Leitungen",
DefaultSeverity: 3, DefaultExposure: 2,
},
}
}
@@ -42,7 +42,7 @@ func builtinSoftwarePatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M145", "M146", "M121"}, SuggestedMeasureIDs: []string{"M145", "M146", "M121"},
SuggestedEvidenceIDs: []string{"E01", "E14"}, SuggestedEvidenceIDs: []string{"E01", "E14"},
Priority: 70, Priority: 70,
ScenarioDE: "Falsche Parametrierung von Achsgrenzen, Geschwindigkeiten oder Sicherheitsgrenzen nach Umruestung.", ScenarioDE: "Falsche Parametrierung von Achsgrenzen, Geschwindigkeiten oder Sicherheitsgrenzen nach Produktwechsel.",
TriggerDE: "Bediener oder Einrichter aendert Parameter ohne Validierung oder nutzt falsches Rezept/Programm.", TriggerDE: "Bediener oder Einrichter aendert Parameter ohne Validierung oder nutzt falsches Rezept/Programm.",
HarmDE: "Ueberfahren mechanischer Anschlaege, zu hohe Kraefte/Geschwindigkeiten, Kollision.", HarmDE: "Ueberfahren mechanischer Anschlaege, zu hohe Kraefte/Geschwindigkeiten, Kollision.",
AffectedDE: "Bedienpersonal, Einrichter", AffectedDE: "Bedienpersonal, Einrichter",
@@ -252,7 +252,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 90, Priority: 90, MachineTypes: []string{"wind_turbine"},
ScenarioDE: "Rotorblatt einer Windturbine bricht durch Materialermuedung oder Blitzschlag und wird Hunderte Meter weit geschleudert.", ScenarioDE: "Rotorblatt einer Windturbine bricht durch Materialermuedung oder Blitzschlag und wird Hunderte Meter weit geschleudert.",
TriggerDE: "Materialermuedung, Blitzschaden, Vereisung mit Unwucht, fehlende Inspektionen", TriggerDE: "Materialermuedung, Blitzschaden, Vereisung mit Unwucht, fehlende Inspektionen",
HarmDE: "Toedliche Verletzung durch Blattstuecke, Sachschaeden im weiten Umkreis", HarmDE: "Toedliche Verletzung durch Blattstuecke, Sachschaeden im weiten Umkreis",
@@ -261,7 +261,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
DefaultSeverity: 5, DefaultExposure: 1, DefaultSeverity: 5, DefaultExposure: 1,
}, },
{ {
ID: "HP746", NameDE: "Absturz bei Wartung der Gondel", NameEN: "Fall during nacelle maintenance", ID: "HP746", NameDE: "Absturz der Gondel", NameEN: "Fall during nacelle maintenance",
RequiredComponentTags: []string{"structural_part", "gravity_risk"}, RequiredComponentTags: []string{"structural_part", "gravity_risk"},
RequiredEnergyTags: []string{"gravitational"}, RequiredEnergyTags: []string{"gravitational"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -297,7 +297,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M141"}, SuggestedMeasureIDs: []string{"M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 80, Priority: 80, MachineTypes: []string{"wind_turbine"},
ScenarioDE: "Bei Vereisung loesen sich Eisstuecke von den Rotorblaettern und werden durch die Fliehkraft weit geschleudert.", ScenarioDE: "Bei Vereisung loesen sich Eisstuecke von den Rotorblaettern und werden durch die Fliehkraft weit geschleudert.",
TriggerDE: "Vereisung im Winter, fehlende Eiserkennungssysteme, Weiterbetrieb bei Eisansatz", TriggerDE: "Vereisung im Winter, fehlende Eiserkennungssysteme, Weiterbetrieb bei Eisansatz",
HarmDE: "Verletzung durch Eisschlag, Sachschaeden an Fahrzeugen und Gebaeuden", HarmDE: "Verletzung durch Eisschlag, Sachschaeden an Fahrzeugen und Gebaeuden",
@@ -30,7 +30,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E08", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E20"},
Priority: 80, Priority: 80, MachineTypes: []string{"escalator"},
ScenarioDE: "Finger oder Handteile werden am Einzugspunkt des Handlaufs in die Verkleidung gezogen.", ScenarioDE: "Finger oder Handteile werden am Einzugspunkt des Handlaufs in die Verkleidung gezogen.",
TriggerDE: "Kinderhand am Handlauf nahe der Verkleidung, fehlende Einlaufschutzbuegel", TriggerDE: "Kinderhand am Handlauf nahe der Verkleidung, fehlende Einlaufschutzbuegel",
HarmDE: "Fingerquetschung, Hautabschuerfungen, bei Kindern Armverletzung", HarmDE: "Fingerquetschung, Hautabschuerfungen, bei Kindern Armverletzung",
@@ -39,7 +39,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
DefaultSeverity: 3, DefaultExposure: 4, DefaultSeverity: 3, DefaultExposure: 4,
}, },
{ {
ID: "HP758", NameDE: "Sturz bei Notbremsung der Fahrtreppe", NameEN: "Fall during emergency stop of escalator", ID: "HP758", MachineTypes: []string{"escalator", "elevator"}, NameDE: "Sturz bei Notbremsung der Fahrtreppe", NameEN: "Fall during emergency stop of escalator",
RequiredComponentTags: []string{"moving_part"}, RequiredComponentTags: []string{"moving_part"},
RequiredEnergyTags: []string{"kinetic"}, RequiredEnergyTags: []string{"kinetic"},
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
@@ -75,7 +75,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E08", "E09", "E20"}, SuggestedEvidenceIDs: []string{"E08", "E09", "E20"},
Priority: 85, Priority: 85, MachineTypes: []string{"escalator", "elevator"},
ScenarioDE: "Bruch einer Trittstufe oder der Kammplatte fuehrt zum Einsacken oder Einzug in die Mechanik.", ScenarioDE: "Bruch einer Trittstufe oder der Kammplatte fuehrt zum Einsacken oder Einzug in die Mechanik.",
TriggerDE: "Materialermuedung, Korrosion, fehlende Inspektionen, Vandalismus", TriggerDE: "Materialermuedung, Korrosion, fehlende Inspektionen, Vandalismus",
HarmDE: "Einzug in Mechanik, Beinverletzungen, Sturz in Maschinenkammer", HarmDE: "Einzug in Mechanik, Beinverletzungen, Sturz in Maschinenkammer",
@@ -173,7 +173,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 95, Priority: 95, MachineTypes: []string{"playground"},
ScenarioDE: "Kind steckt Kopf durch Oeffnung im Spielgeraet und bleibt haengen (Kopf-Entrapment-Gefahr bei 89-230 mm).", ScenarioDE: "Kind steckt Kopf durch Oeffnung im Spielgeraet und bleibt haengen (Kopf-Entrapment-Gefahr bei 89-230 mm).",
TriggerDE: "Oeffnungen im kritischen Bereich 89-230 mm, V-foermige Spalte, Gelaendersprosse mit Kopffangmass", TriggerDE: "Oeffnungen im kritischen Bereich 89-230 mm, V-foermige Spalte, Gelaendersprosse mit Kopffangmass",
HarmDE: "Strangulation, Erstickung, toedliche Verletzung", HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
@@ -233,7 +233,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M141"}, SuggestedMeasureIDs: []string{"M003", "M141"},
SuggestedEvidenceIDs: []string{"E01", "E20"}, SuggestedEvidenceIDs: []string{"E01", "E20"},
Priority: 95, Priority: 95, MachineTypes: []string{"playground"},
ScenarioDE: "Kind verfaengt sich mit Kapuzenkordel, Schal oder Halskette in Seilen oder Netzen des Spielgeraets.", ScenarioDE: "Kind verfaengt sich mit Kapuzenkordel, Schal oder Halskette in Seilen oder Netzen des Spielgeraets.",
TriggerDE: "Kleidung mit Kordeln am Hals, zu grosse Maschenweite, lose Seilenden", TriggerDE: "Kleidung mit Kordeln am Hals, zu grosse Maschenweite, lose Seilenden",
HarmDE: "Strangulation, Erstickung, toedliche Verletzung", HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
@@ -361,7 +361,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"}, SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"}, SuggestedEvidenceIDs: []string{"E08", "E09"},
Priority: 85, Priority: 85, MachineTypes: []string{"laundry"},
ScenarioDE: "Person greift in die drehende Trommel der Industriewaschmaschine und wird eingezogen.", ScenarioDE: "Person greift in die drehende Trommel der Industriewaschmaschine und wird eingezogen.",
TriggerDE: "Defekte Tuerverriegelung, Oeffnen waehrend Nachlauf, Bedienfehler", TriggerDE: "Defekte Tuerverriegelung, Oeffnen waehrend Nachlauf, Bedienfehler",
HarmDE: "Schwere Quetschverletzung, Armeinzug, Strangulation durch Waeschestuecke", HarmDE: "Schwere Quetschverletzung, Armeinzug, Strangulation durch Waeschestuecke",
@@ -411,7 +411,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
SuggestedMeasureIDs: []string{"M005", "M141"}, SuggestedMeasureIDs: []string{"M005", "M141"},
SuggestedEvidenceIDs: []string{"E20"}, SuggestedEvidenceIDs: []string{"E20"},
Priority: 80, Priority: 80,
ScenarioDE: "Grosse Glasscheibe zerbricht beim Transport oder bei der Montage und trifft umstehende Personen.", ScenarioDE: "Grosse Glasscheibe zerbricht oder durch mechanische Einwirkung und trifft umstehende Personen.",
TriggerDE: "Thermische Spannungen, mechanische Beschaedigung, fehlerhafter Saugnapp, Windlast", TriggerDE: "Thermische Spannungen, mechanische Beschaedigung, fehlerhafter Saugnapp, Windlast",
HarmDE: "Tiefe Schnittwunden, Amputationsgefahr, toedliche Verletzung bei grossen Scheiben", HarmDE: "Tiefe Schnittwunden, Amputationsgefahr, toedliche Verletzung bei grossen Scheiben",
AffectedDE: "Transportpersonal, Monteure, Passanten", AffectedDE: "Transportpersonal, Monteure, Passanten",
@@ -22,7 +22,7 @@ func GetTextileAgriPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M452", "M061"}, SuggestedEvidenceIDs: []string{"E01"}, SuggestedMeasureIDs: []string{"M452", "M061"}, SuggestedEvidenceIDs: []string{"E01"},
Priority: 78, MachineTypes: []string{"textile", "knitting"}, Priority: 78, MachineTypes: []string{"textile", "knitting"},
OperationalStates: []string{"automatic_operation", "maintenance"}, HumanRoles: []string{"operator", "maintenance_tech"}, OperationalStates: []string{"automatic_operation", "maintenance"}, HumanRoles: []string{"operator", "maintenance_tech"},
ScenarioDE: "Kontakt mit schnell bewegenden Nadeln bei Wartung oder Fadenwechsel", ScenarioDE: "Kontakt mit schnell bewegenden Nadeln oder Fadenwechsel",
TriggerDE: "Eingriff in Nadelbereich bei laufender Maschine", HarmDE: "Stichverletzung, Schnittwunde", TriggerDE: "Eingriff in Nadelbereich bei laufender Maschine", HarmDE: "Stichverletzung, Schnittwunde",
AffectedDE: "Bedienpersonal", ZoneDE: "Nadelbett", AffectedDE: "Bedienpersonal", ZoneDE: "Nadelbett",
DefaultSeverity: 3, DefaultExposure: 4}, DefaultSeverity: 3, DefaultExposure: 4},
@@ -123,7 +123,7 @@ func GetTextileAgriPatterns() []HazardPattern {
SuggestedMeasureIDs: []string{"M461", "M465"}, SuggestedEvidenceIDs: []string{"E01", "E08"}, SuggestedMeasureIDs: []string{"M461", "M465"}, SuggestedEvidenceIDs: []string{"E01", "E08"},
Priority: 94, MachineTypes: []string{"agricultural", "harvester", "combine"}, Priority: 94, MachineTypes: []string{"agricultural", "harvester", "combine"},
OperationalStates: []string{"automatic_operation", "maintenance"}, HumanRoles: []string{"operator", "maintenance_tech"}, OperationalStates: []string{"automatic_operation", "maintenance"}, HumanRoles: []string{"operator", "maintenance_tech"},
ScenarioDE: "Kontakt mit rotierendem Schneidwerk bei Wartung oder Blockierungsbeseitigung", ScenarioDE: "Kontakt mit rotierendem Schneidwerk oder Blockierungsbeseitigung",
TriggerDE: "Maschine nicht abgestellt, hydraulischer Nachlauf", TriggerDE: "Maschine nicht abgestellt, hydraulischer Nachlauf",
HarmDE: "Amputation, schwere Schnittverletzungen", AffectedDE: "Bediener, Wartungspersonal", ZoneDE: "Schneidwerksbereich", HarmDE: "Amputation, schwere Schnittverletzungen", AffectedDE: "Bediener, Wartungspersonal", ZoneDE: "Schneidwerksbereich",
DefaultSeverity: 5, DefaultExposure: 3}, DefaultSeverity: 5, DefaultExposure: 3},
@@ -42,7 +42,7 @@ func builtinThermalPatterns() []HazardPattern {
SuggestedEvidenceIDs: []string{"E01"}, SuggestedEvidenceIDs: []string{"E01"},
Priority: 75, Priority: 75,
ScenarioDE: "Aktuatoren (Servomotoren, Linearantriebe) erwaermen sich im Dauerbetrieb ueber die Beruehrtemperaturgrenze.", ScenarioDE: "Aktuatoren (Servomotoren, Linearantriebe) erwaermen sich im Dauerbetrieb ueber die Beruehrtemperaturgrenze.",
TriggerDE: "Beruehren heisser Motorgehaeuse bei Wartung oder Stoerungsbeseitigung ohne ausreichende Abkuehlzeit.", TriggerDE: "Beruehren heisser Motorgehaeuse ohne ausreichende Abkuehlzeit.",
HarmDE: "Kontaktverbrennung, Blasenbildung an Haenden.", HarmDE: "Kontaktverbrennung, Blasenbildung an Haenden.",
AffectedDE: "Wartungspersonal, Einrichter", AffectedDE: "Wartungspersonal, Einrichter",
ZoneDE: "Motorgehaeuse, Getriebegehaeuse, Linearantrieb", ZoneDE: "Motorgehaeuse, Getriebegehaeuse, Linearantrieb",
@@ -230,7 +230,7 @@ func GetWeldingGlassTextilePatterns() []HazardPattern {
GeneratedHazardCats: []string{"mechanical_hazard"}, GeneratedHazardCats: []string{"mechanical_hazard"},
SuggestedMeasureIDs: []string{"M003", "M004", "M082"}, SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
SuggestedEvidenceIDs: []string{"E08", "E09"}, SuggestedEvidenceIDs: []string{"E08", "E09"},
Priority: 80, Priority: 80, MachineTypes: []string{"glass_washing"},
ScenarioDE: "Transportwalzen der Glaswaschmaschine erfassen Finger oder Kleidung beim manuellen Einlegen der Scheiben.", ScenarioDE: "Transportwalzen der Glaswaschmaschine erfassen Finger oder Kleidung beim manuellen Einlegen der Scheiben.",
TriggerDE: "Manuelles Nachjustieren bei laufenden Walzen, fehlender Schutz am Einlaufbereich", TriggerDE: "Manuelles Nachjustieren bei laufenden Walzen, fehlender Schutz am Einlaufbereich",
HarmDE: "Fingerquetschung, Einzug der Hand, Hautabschaelungen", HarmDE: "Fingerquetschung, Einzug der Hand, Hautabschaelungen",
@@ -71,21 +71,21 @@ func getSupplementaryMeasures() []ProtectiveMeasureEntry {
// Elektrische Sicherheit — Potentialausgleich & Ableitstroeme // Elektrische Sicherheit — Potentialausgleich & Ableitstroeme
// Gap: GT-Benchmark 2.12 (Potentialausgleich), 2.4 (Ableitstroeme) // Gap: GT-Benchmark 2.12 (Potentialausgleich), 2.4 (Ableitstroeme)
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
{ID: "M410", ReductionType: "design", SubType: "electrical_safety", Name: "Potentialausgleich zwischen Anlagenteilen", Description: "Alle leitfaehigen Anlagenteile mit unterschiedlicher Energieversorgung werden ueber einen Potentialausgleichsleiter verbunden um gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Potentialausgleich zwischen Roboterzelle und Werkzeugmaschine", "Potentialausgleichsschiene im Schaltschrank"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2", "IEC 61439-1"}}, {ID: "M475", ReductionType: "design", SubType: "electrical_safety", Name: "Potentialausgleich zwischen Anlagenteilen", Description: "Alle leitfaehigen Anlagenteile mit unterschiedlicher Energieversorgung werden ueber einen Potentialausgleichsleiter verbunden um gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Potentialausgleich zwischen Roboterzelle und Werkzeugmaschine", "Potentialausgleichsschiene im Schaltschrank"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2", "IEC 61439-1"}},
{ID: "M411", ReductionType: "design", SubType: "electrical_safety", Name: "Schutz bei erhoehten Ableitstroemen", Description: "Bei Ableitstroemen ueber 10 mA wird der Schutzleiter mechanisch geschuetzt oder ein zusaetzlicher Schutzleiter verlegt und die Verbindung ueberwacht.", HazardCategory: "electrical", Examples: []string{"Schutzrohr fuer Schutzleiter an Frequenzumrichter", "Doppelter Schutzleiter mit Ueberwachung"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2.6"}}, {ID: "M476", ReductionType: "design", SubType: "electrical_safety", Name: "Schutz bei erhoehten Ableitstroemen", Description: "Bei Ableitstroemen ueber 10 mA wird der Schutzleiter mechanisch geschuetzt oder ein zusaetzlicher Schutzleiter verlegt und die Verbindung ueberwacht.", HazardCategory: "electrical", Examples: []string{"Schutzrohr fuer Schutzleiter an Frequenzumrichter", "Doppelter Schutzleiter mit Ueberwachung"}, NormReferences: []string{"IEC 60204-1 Ziff. 8.2.6"}},
{ID: "M412", ReductionType: "design", SubType: "electrical_safety", Name: "Dimensionierung von Luft- und Kriechstrecken", Description: "Luft- und Kriechstrecken werden entsprechend der elektrischen Beanspruchung und Verschmutzungsgrad dimensioniert um Kurzschluesse und gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Mindestabstaende in Schaltgeraetekombinationen einhalten", "Isolationsueberwachung installieren"}, NormReferences: []string{"IEC 60204-1 Ziff. 6.2", "IEC 61439-1"}}, {ID: "M477", ReductionType: "design", SubType: "electrical_safety", Name: "Dimensionierung von Luft- und Kriechstrecken", Description: "Luft- und Kriechstrecken werden entsprechend der elektrischen Beanspruchung und Verschmutzungsgrad dimensioniert um Kurzschluesse und gefaehrliche Beruehrungsspannungen zu vermeiden.", HazardCategory: "electrical", Examples: []string{"Mindestabstaende in Schaltgeraetekombinationen einhalten", "Isolationsueberwachung installieren"}, NormReferences: []string{"IEC 60204-1 Ziff. 6.2", "IEC 61439-1"}},
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
// EMV-Sicherheit // EMV-Sicherheit
// Gap: GT-Benchmark 6.1 (EMV-Stoereinfluss auf Sicherheitsfunktionen) // Gap: GT-Benchmark 6.1 (EMV-Stoereinfluss auf Sicherheitsfunktionen)
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
{ID: "M415", ReductionType: "design", SubType: "emc_safety", Name: "EMV-konforme Installation und Verkabelung", Description: "Alle sicherheitsrelevanten Komponenten und Sub-Systeme werden nach EMV-Richtlinien installiert und verkabelt um Stoereinfluss auf Sicherheitsfunktionen zu verhindern.", HazardCategory: "electrical", Examples: []string{"Geschirmte Steuerleitungen verwenden", "Getrennte Kabelkanaele fuer Leistungs- und Signalleitungen"}, NormReferences: []string{"IEC 61000-6-2", "EN 16090-1 Ziff. 5.8.7"}}, {ID: "M478", ReductionType: "design", SubType: "emc_safety", Name: "EMV-konforme Installation und Verkabelung", Description: "Alle sicherheitsrelevanten Komponenten und Sub-Systeme werden nach EMV-Richtlinien installiert und verkabelt um Stoereinfluss auf Sicherheitsfunktionen zu verhindern.", HazardCategory: "electrical", Examples: []string{"Geschirmte Steuerleitungen verwenden", "Getrennte Kabelkanaele fuer Leistungs- und Signalleitungen"}, NormReferences: []string{"IEC 61000-6-2", "EN 16090-1 Ziff. 5.8.7"}},
{ID: "M416", ReductionType: "design", SubType: "emc_safety", Name: "EMV-Pruefung sicherheitsrelevanter Systeme", Description: "Sicherheitsrelevante Steuerungen und Antriebe werden auf Stoerfestigkeit gegenueber elektromagnetischen Einflussgroessen geprueft.", HazardCategory: "electrical", Examples: []string{"Burst/Surge-Pruefung nach IEC 61000-4", "Stoerfestigkeitspruefung der Sicherheits-SPS"}, NormReferences: []string{"IEC 61000-4-4", "IEC 61000-4-5", "IEC 62061"}}, {ID: "M479", ReductionType: "design", SubType: "emc_safety", Name: "EMV-Pruefung sicherheitsrelevanter Systeme", Description: "Sicherheitsrelevante Steuerungen und Antriebe werden auf Stoerfestigkeit gegenueber elektromagnetischen Einflussgroessen geprueft.", HazardCategory: "electrical", Examples: []string{"Burst/Surge-Pruefung nach IEC 61000-4", "Stoerfestigkeitspruefung der Sicherheits-SPS"}, NormReferences: []string{"IEC 61000-4-4", "IEC 61000-4-5", "IEC 62061"}},
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
// Kuehlschmierstoff-Leitungssicherheit // Kuehlschmierstoff-Leitungssicherheit
// Gap: GT-Benchmark 2.10 (KSS-Leckage fuehrt zu Brand) // Gap: GT-Benchmark 2.10 (KSS-Leckage fuehrt zu Brand)
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
{ID: "M420", ReductionType: "design", SubType: "fluid_safety", Name: "Druckfeste Auslegung von KSS-Leitungen", Description: "Schlaeuche, Dichtungen, Verbindungsstuecke und Befestigungen des Kuehlschmierstoffsystems werden auf den Nenndruck der jeweiligen Komponente ausgelegt und gegen Abspringen gesichert.", HazardCategory: "mechanical", Examples: []string{"Druckschlaeuche auf maximalen Betriebsdruck dimensionieren", "Schlauchbruchsicherungen an kritischen Verbindungen"}, NormReferences: []string{"IEC 60204-1 Ziff. 11.3", "EN ISO 4414"}}, {ID: "M480", ReductionType: "design", SubType: "fluid_safety", Name: "Druckfeste Auslegung von KSS-Leitungen", Description: "Schlaeuche, Dichtungen, Verbindungsstuecke und Befestigungen des Kuehlschmierstoffsystems werden auf den Nenndruck der jeweiligen Komponente ausgelegt und gegen Abspringen gesichert.", HazardCategory: "mechanical", Examples: []string{"Druckschlaeuche auf maximalen Betriebsdruck dimensionieren", "Schlauchbruchsicherungen an kritischen Verbindungen"}, NormReferences: []string{"IEC 60204-1 Ziff. 11.3", "EN ISO 4414"}},
} }
} }
@@ -0,0 +1,84 @@
package iace
import "testing"
// TestHP1640_ResolvesToContactProtection pins the GT-2.2 fix: the "direct
// contact with live parts" pattern must resolve to electrical-contact-protection
// measures (basic protection, double insulation, earthing, equipotential
// bonding), not to mechanical fallbacks like chip extraction.
func TestHP1640_ResolvesToContactProtection(t *testing.T) {
measureByID := make(map[string]ProtectiveMeasureEntry)
for _, m := range GetProtectiveMeasureLibrary() {
measureByID[m.ID] = m
}
patterns := GetRobotCellPatterns()
var hp1640 *HazardPattern
for i := range patterns {
if patterns[i].ID == "HP1640" {
hp1640 = &patterns[i]
break
}
}
if hp1640 == nil {
t.Fatal("HP1640 not found in robot cell patterns")
}
if len(hp1640.SuggestedMeasureIDs) < 3 {
t.Errorf("HP1640 should suggest at least 3 measures, got %d", len(hp1640.SuggestedMeasureIDs))
}
for _, mid := range hp1640.SuggestedMeasureIDs {
m, ok := measureByID[mid]
if !ok {
t.Errorf("HP1640 references non-existent measure %s", mid)
continue
}
if m.HazardCategory != "electrical" {
t.Errorf("HP1640 measure %s (%q) has HazardCategory=%s, expected electrical",
mid, m.Name, m.HazardCategory)
}
}
}
// TestHP1688_M475IsPotentialausgleich pins the M475 rename: HP1688 (touch
// voltage from potential differences) must resolve M475 to the equipotential
// bonding measure, not to the metalworking chip extraction that previously
// occupied M410 and overwrote the electrical definition.
func TestHP1688_M475IsPotentialausgleich(t *testing.T) {
measureByID := make(map[string]ProtectiveMeasureEntry)
for _, m := range GetProtectiveMeasureLibrary() {
measureByID[m.ID] = m
}
m, ok := measureByID["M475"]
if !ok {
t.Fatal("M475 not defined — supplementary rename did not land")
}
if m.HazardCategory != "electrical" {
t.Errorf("M475 must be HazardCategory=electrical, got %s (%q)", m.HazardCategory, m.Name)
}
patterns := GetRobotCellPatternsExt()
var hp1688 *HazardPattern
for i := range patterns {
if patterns[i].ID == "HP1688" {
hp1688 = &patterns[i]
break
}
}
if hp1688 == nil {
t.Fatal("HP1688 not found in robot cell ext patterns")
}
found := false
for _, mid := range hp1688.SuggestedMeasureIDs {
if mid == "M475" {
found = true
break
}
}
if !found {
t.Errorf("HP1688 must reference M475 (Potentialausgleich), got %v", hp1688.SuggestedMeasureIDs)
}
}
@@ -66,6 +66,8 @@ type PatternMatch struct {
HumanRoles []string `json:"human_roles,omitempty"` HumanRoles []string `json:"human_roles,omitempty"`
GeneratedHazardType string `json:"generated_hazard_type,omitempty"` GeneratedHazardType string `json:"generated_hazard_type,omitempty"`
MatchedFailureModes []string `json:"matched_failure_modes,omitempty"` MatchedFailureModes []string `json:"matched_failure_modes,omitempty"`
ApplicableLifecycles []string `json:"applicable_lifecycles,omitempty"`
SuggestedMeasureIDs []string `json:"suggested_measure_ids,omitempty"`
} }
// HazardSuggestion is a suggested hazard from pattern matching. // HazardSuggestion is a suggested hazard from pattern matching.
@@ -94,44 +96,11 @@ type PatternEngine struct {
} }
// NewPatternEngine creates a PatternEngine with all pattern sources and resolver. // NewPatternEngine creates a PatternEngine with all pattern sources and resolver.
// Pattern registration is in pattern_registry.go (collectAllPatterns).
func NewPatternEngine() *PatternEngine { func NewPatternEngine() *PatternEngine {
// Combine all pattern sources
patterns := GetBuiltinHazardPatterns() // HP001-HP044
patterns = append(patterns, GetExtendedHazardPatterns()...) // HP045+ from rule library
patterns = append(patterns, GetPressHazardPatterns()...) // HP045-HP058 press-specific
patterns = append(patterns, GetCobotHazardPatterns()...) // HP059-HP065 cobot-specific
patterns = append(patterns, GetOperationalHazardPatterns()...) // HP066-HP093 operational states
patterns = append(patterns, GetDGUVExtendedPatterns()...) // HP094-HP133 DGUV themes
patterns = append(patterns, GetExtendedHazardPatterns2()...) // HP134-HP173 additional hazards
patterns = append(patterns, GetElevatorPatterns()...) // HP174-HP198 elevator/lift
patterns = append(patterns, GetAGVAgriPatterns()...) // HP199-HP228 AGV + agricultural
patterns = append(patterns, GetFoodProcessingPatterns()...) // HP300-HP319 food processing
patterns = append(patterns, GetPackagingPatterns()...) // HP320-HP334 packaging machines
patterns = append(patterns, GetLaserPatterns()...) // HP335-HP349 laser machines
patterns = append(patterns, GetMedicalDevicePatterns()...) // HP350-HP364 medical devices (IEC 60601)
patterns = append(patterns, GetPressureEquipmentPatterns()...) // HP365-HP374 pressure equipment
patterns = append(patterns, GetConstructionPatterns()...) // HP400-HP419 construction/crane
patterns = append(patterns, GetForestryConveyorPatterns()...) // HP420-HP450 forestry/conveyor
patterns = append(patterns, GetPlasticsMetalPatterns()...) // HP500-HP529 plastics + metalworking
patterns = append(patterns, GetWeldingGlassTextilePatterns()...) // HP530-HP559 welding + glass + textile
patterns = append(patterns, GetSpecificMachinePatterns()...) // HP730-HP755 pressure/wind/solar/battery
patterns = append(patterns, GetSpecificMachinePatterns2()...) // HP756-HP784 escalator/pool/playground/fitness/laundry/glass
patterns = append(patterns, GetCyberExtendedPatterns()...) // HP800-HP829 software faults/cyber-security
patterns = append(patterns, GetCyberExtendedPatterns2()...) // HP830-HP844 AI-ML specific
patterns = append(patterns, GetCyberExtendedPatterns3()...) // HP845-HP864 network/communication + HMI
patterns = append(patterns, GetWorkshopPatterns()...) // HP600-HP664 cross-machine workshop
patterns = append(patterns, GetMaintenanceExtPatterns()...) // HP700-HP729,HP900-HP934 maintenance lifecycle
patterns = append(patterns, GetFinalPatternsA()...) // HP1000-HP1084 mechanical body-part variants
patterns = append(patterns, GetFinalPatternsB()...) // HP1085-HP1169 electrical/thermal/chemical/bio/radiation
patterns = append(patterns, GetFinalPatternsC()...) // HP1170-HP1254 software/control/org/ergonomic/fire
patterns = append(patterns, GetFinalPatternsD()...) // HP1255-HP1335 lifecycle/special situations
patterns = append(patterns, GetCNCHazardPatterns()...) // HP1400-HP1419 CNC/metalworking part 1 (Phase 3)
patterns = append(patterns, GetCNCHazardPatternsExt()...) // HP1420-HP1434 CNC/metalworking part 2 (Phase 3)
patterns = append(patterns, GetVDMAIndustryPatterns()...) // HP1500-HP1549 VDMA sectors (Phase 3)
patterns = append(patterns, GetTextileAgriPatterns()...) // HP1550-HP1584 Textile + Agri (Phase 5)
return &PatternEngine{ return &PatternEngine{
resolver: NewTagResolver(), resolver: NewTagResolver(),
patterns: patterns, patterns: collectAllPatterns(),
} }
} }
@@ -250,7 +219,9 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
StateTransitions: p.StateTransitions, StateTransitions: p.StateTransitions,
HumanRoles: p.HumanRoles, HumanRoles: p.HumanRoles,
GeneratedHazardType: p.GeneratedHazardType, GeneratedHazardType: p.GeneratedHazardType,
MatchedFailureModes: matchedFMs, MatchedFailureModes: matchedFMs,
ApplicableLifecycles: p.ApplicableLifecycles,
SuggestedMeasureIDs: p.SuggestedMeasureIDs,
}) })
for _, cat := range p.GeneratedHazardCats { for _, cat := range p.GeneratedHazardCats {
@@ -0,0 +1,43 @@
package iace
// collectAllPatterns gathers hazard patterns from all registered sources.
// This function is called by NewPatternEngine() to build the complete pattern set.
// New pattern sources are registered here.
func collectAllPatterns() []HazardPattern {
patterns := GetBuiltinHazardPatterns() // HP001-HP044
patterns = append(patterns, GetExtendedHazardPatterns()...) // HP045+ from rule library
patterns = append(patterns, GetPressHazardPatterns()...) // HP045-HP058 press-specific
patterns = append(patterns, GetCobotHazardPatterns()...) // HP059-HP065 cobot-specific
patterns = append(patterns, GetOperationalHazardPatterns()...) // HP066-HP093 operational states
patterns = append(patterns, GetDGUVExtendedPatterns()...) // HP094-HP133 DGUV themes
patterns = append(patterns, GetExtendedHazardPatterns2()...) // HP134-HP173 additional hazards
patterns = append(patterns, GetElevatorPatterns()...) // HP174-HP198 elevator/lift
patterns = append(patterns, GetAGVAgriPatterns()...) // HP199-HP228 AGV + agricultural
patterns = append(patterns, GetFoodProcessingPatterns()...) // HP300-HP319 food processing
patterns = append(patterns, GetPackagingPatterns()...) // HP320-HP334 packaging machines
patterns = append(patterns, GetLaserPatterns()...) // HP335-HP349 laser machines
patterns = append(patterns, GetMedicalDevicePatterns()...) // HP350-HP364 medical devices (IEC 60601)
patterns = append(patterns, GetPressureEquipmentPatterns()...) // HP365-HP374 pressure equipment
patterns = append(patterns, GetConstructionPatterns()...) // HP400-HP419 construction/crane
patterns = append(patterns, GetForestryConveyorPatterns()...) // HP420-HP450 forestry/conveyor
patterns = append(patterns, GetPlasticsMetalPatterns()...) // HP500-HP529 plastics + metalworking
patterns = append(patterns, GetWeldingGlassTextilePatterns()...) // HP530-HP559 welding + glass + textile
patterns = append(patterns, GetSpecificMachinePatterns()...) // HP730-HP755 pressure/wind/solar/battery
patterns = append(patterns, GetSpecificMachinePatterns2()...) // HP756-HP784 escalator/pool/playground/fitness/laundry/glass
patterns = append(patterns, GetCyberExtendedPatterns()...) // HP800-HP829 software faults/cyber-security
patterns = append(patterns, GetCyberExtendedPatterns2()...) // HP830-HP844 AI-ML specific
patterns = append(patterns, GetCyberExtendedPatterns3()...) // HP845-HP864 network/communication + HMI
patterns = append(patterns, GetWorkshopPatterns()...) // HP600-HP664 cross-machine workshop
patterns = append(patterns, GetMaintenanceExtPatterns()...) // HP700-HP729,HP900-HP934 maintenance lifecycle
patterns = append(patterns, GetFinalPatternsA()...) // HP1000-HP1084 mechanical body-part variants
patterns = append(patterns, GetFinalPatternsB()...) // HP1085-HP1169 electrical/thermal/chemical/bio/radiation
patterns = append(patterns, GetFinalPatternsC()...) // HP1170-HP1254 software/control/org/ergonomic/fire
patterns = append(patterns, GetFinalPatternsD()...) // HP1255-HP1335 lifecycle/special situations
patterns = append(patterns, GetCNCHazardPatterns()...) // HP1400-HP1419 CNC/metalworking part 1 (Phase 3)
patterns = append(patterns, GetCNCHazardPatternsExt()...) // HP1420-HP1434 CNC/metalworking part 2 (Phase 3)
patterns = append(patterns, GetVDMAIndustryPatterns()...) // HP1500-HP1549 VDMA sectors (Phase 3)
patterns = append(patterns, GetTextileAgriPatterns()...) // HP1550-HP1584 Textile + Agri (Phase 5)
patterns = append(patterns, GetRobotCellPatterns()...) // HP1600-HP1649 Robot cell (GT benchmark)
patterns = append(patterns, GetRobotCellPatternsExt()...) // HP1650-HP1699 Robot cell extended (GT gaps)
return patterns
}
@@ -64,12 +64,16 @@ class ComplianceCheckStatusResponse(BaseModel):
@router.post("/extract-text") @router.post("/extract-text")
async def extract_text(req: ExtractTextRequest): async def extract_text(req: ExtractTextRequest):
"""Extract text from a URL via consent-tester DSI discovery.""" """Extract text from a URL via consent-tester DSI discovery.
Merges all documents found on the page (sub-pages, accordions, etc.)
"""
try: try:
async with httpx.AsyncClient(timeout=90.0) as client: async with httpx.AsyncClient(timeout=300.0) as client:
resp = await client.post( resp = await client.post(
f"{CONSENT_TESTER_URL}/dsi-discovery", f"{CONSENT_TESTER_URL}/dsi-discovery",
json={"url": req.url, "max_documents": 1}, json={"url": req.url, "max_documents": 5},
timeout=300.0,
) )
if resp.status_code != 200: if resp.status_code != 200:
return { return {
@@ -86,10 +90,15 @@ async def extract_text(req: ExtractTextRequest):
"error": "Kein Text extrahierbar", "error": "Kein Text extrahierbar",
} }
doc = docs[0] # Merge all documents (handles multi-page DSIs like BMW)
text = doc.get("full_text", "") or doc.get("text_preview", "") or doc.get("text", "") texts = []
title = doc.get("title", "") or doc.get("doc_type", "") for doc in docs:
word_count = doc.get("word_count", 0) or len(text.split()) t = doc.get("full_text", "") or doc.get("text_preview", "") or ""
if t and len(t) > 50:
texts.append(t)
text = "\n\n".join(texts) if texts else ""
title = docs[0].get("title", "") or docs[0].get("doc_type", "")
word_count = len(text.split())
return { return {
"text": text, "text": text,
@@ -178,11 +187,16 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
# 1. Same URL used for multiple doc_types → split by heading # 1. Same URL used for multiple doc_types → split by heading
# 2. DSI text contains Cookie/Social-Media sections → auto-fill empty rows # 2. DSI text contains Cookie/Social-Media sections → auto-fill empty rows
from compliance.services.section_splitter import ( from compliance.services.section_splitter import (
split_shared_texts, auto_fill_from_dsi, split_shared_texts, auto_fill_from_dsi, cross_search_documents,
) )
split_shared_texts(doc_entries, url_text_cache) split_shared_texts(doc_entries, url_text_cache)
auto_fill_from_dsi(doc_entries) auto_fill_from_dsi(doc_entries)
# Refresh doc_texts after splitting
# Step 1c: Cross-document search — find doc_types in wrong documents
_update(check_id, "Dokumente werden uebergreifend durchsucht...")
placement_findings = cross_search_documents(doc_entries)
# Refresh doc_texts after all splitting/searching
for entry in doc_entries: for entry in doc_entries:
if entry.get("text"): if entry.get("text"):
doc_texts[entry["doc_type"]] = entry["text"] doc_texts[entry["doc_type"]] = entry["text"]
@@ -232,6 +246,16 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
# Apply profile context filter # Apply profile context filter
result = _apply_profile_filter(result, profile, doc_type) result = _apply_profile_filter(result, profile, doc_type)
# Add placement findings — but only if the regex checks confirm
# the text doesn't match. If completeness >= 50%, the text IS the
# right doc_type despite missing cross-search keywords.
if result.completeness_pct < 50:
for pf in placement_findings:
if pf.get("doc_type") == doc_type:
result.checks.insert(0, CheckItem(**{
k: v for k, v in pf.items() if k != "doc_type"
}))
results.append(result) results.append(result)
total_findings += result.findings_count total_findings += result.findings_count
@@ -302,17 +326,24 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
else: else:
r.scenario = "import" r.scenario = "import"
# Step 5: Build report # Step 5: Build report with management summary
_update(check_id, "Report wird erstellt...") _update(check_id, "Report wird erstellt...")
from .agent_doc_check_report import build_management_summary
summary_html = build_management_summary(results)
report_html = build_html_report(results, None) report_html = build_html_report(results, None)
profile_html = _build_profile_html(profile) profile_html = _build_profile_html(profile)
full_html = profile_html + report_html full_html = summary_html + profile_html + report_html
# Step 6: Send email # Step 6: Send email — include website/company name in subject
doc_count = len([r for r in results if not r.error]) doc_count = len([r for r in results if not r.error])
site_name = (
extracted_profile.get("company_profile", {}).get("companyName")
or _extract_domain(doc_entries)
or "Unbekannt"
)
email_result = send_email( email_result = send_email(
recipient=req.recipient, recipient=req.recipient,
subject=f"[COMPLIANCE-CHECK] {doc_count} Dokumente geprueft", subject=f"[COMPLIANCE-CHECK] {site_name}{doc_count} Dokumente geprueft",
body_html=full_html, body_html=full_html,
) )
@@ -349,23 +380,55 @@ def _update(check_id: str, msg: str):
async def _fetch_text(url: str) -> str: async def _fetch_text(url: str) -> str:
"""Fetch text from URL via consent-tester.""" """Fetch text from URL via consent-tester, with HTTP fallback.
1. Try consent-tester (Playwright) handles JS-heavy SPAs
2. Fallback: direct HTTP fetch + HTML strip fast, works for SSR pages
"""
# 1. Consent-tester (Playwright-based, full JS rendering)
try: try:
async with httpx.AsyncClient(timeout=90.0) as client: async with httpx.AsyncClient(timeout=60.0) as client:
resp = await client.post( resp = await client.post(
f"{CONSENT_TESTER_URL}/dsi-discovery", f"{CONSENT_TESTER_URL}/dsi-discovery",
json={"url": url, "max_documents": 1}, json={"url": url, "max_documents": 3},
timeout=60.0,
) )
if resp.status_code != 200: if resp.status_code == 200:
return "" docs = resp.json().get("documents", [])
docs = resp.json().get("documents", []) if docs:
if not docs: texts = []
return "" for doc in docs:
doc = docs[0] t = doc.get("full_text", "") or doc.get("text_preview", "") or ""
return doc.get("full_text", "") or doc.get("text_preview", "") or "" if t and len(t) > 50:
texts.append(t)
merged = "\n\n".join(texts)
if merged and len(merged.split()) > 100:
if len(texts) > 1:
logger.info("Merged %d docs from %s (%d words)",
len(texts), url, len(merged.split()))
return merged
except Exception as e: except Exception as e:
logger.warning("Text fetch failed for %s: %s", url, e) logger.warning("Consent-tester fetch failed for %s: %s", url, e)
return ""
# 2. Fallback: direct HTTP fetch (works for SSR pages like BMW)
try:
import re as _re
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
resp = await client.get(url)
if resp.status_code == 200 and "text/html" in resp.headers.get("content-type", ""):
html = resp.text
# Strip HTML tags, decode entities
text = _re.sub(r"<script[^>]*>.*?</script>", " ", html, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"<style[^>]*>.*?</style>", " ", text, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"<[^>]+>", " ", text)
text = _re.sub(r"\s+", " ", text).strip()
if len(text.split()) > 100:
logger.info("HTTP fallback for %s: %d words", url, len(text.split()))
return text
except Exception as e:
logger.warning("HTTP fallback failed for %s: %s", url, e)
return ""
async def _check_single( async def _check_single(
@@ -440,6 +503,17 @@ async def _check_single(
) )
def _extract_domain(doc_entries: list[dict]) -> str | None:
"""Extract domain name from first URL for email subject."""
for entry in doc_entries:
url = entry.get("url", "")
if url and "://" in url:
from urllib.parse import urlparse
host = urlparse(url).netloc
return host.replace("www.", "") if host else None
return None
def _get_skip_types(profile) -> dict[str, str]: def _get_skip_types(profile) -> dict[str, str]:
"""Doc_types to skip entirely. Currently empty — we check everything """Doc_types to skip entirely. Currently empty — we check everything
and flag irrelevant items as INFO instead of skipping.""" and flag irrelevant items as INFO instead of skipping."""
@@ -40,6 +40,121 @@ def _hint_box(hint: str) -> str:
) )
def build_management_summary(results: list[DocCheckResult]) -> str:
"""Build a plain-language management summary for the CEO/GF.
No legal jargon concrete actions that can be delegated to staff,
lawyers, or the DPO.
"""
ok = [r for r in results if r.completeness_pct == 100 and not r.error]
fixable = [r for r in results if 0 < r.completeness_pct < 100 and not r.error]
critical = [r for r in results if r.completeness_pct == 0 and not r.error]
errors = [r for r in results if r.error]
html = [
'<div style="font-family:-apple-system,BlinkMacSystemFont,sans-serif;'
'max-width:700px;margin:0 auto 20px;padding:16px 20px;'
'background:#f8fafc;border:1px solid #e2e8f0;border-radius:12px">',
'<h2 style="margin:0 0 12px;font-size:18px;color:#1e293b">'
'Zusammenfassung fuer die Geschaeftsfuehrung</h2>',
]
# Overall status
total = len(results) - len(errors)
if total == 0:
html.append('<p>Keine Dokumente geprueft.</p></div>')
return "\n".join(html)
if len(ok) == total:
html.append(
'<p style="color:#16a34a;font-weight:600;font-size:15px">'
'Alle Dokumente sind vollstaendig. Keine dringenden Massnahmen noetig.</p>'
)
else:
html.append(
f'<p style="font-size:14px;color:#475569">'
f'{len(ok)} von {total} Dokumenten sind vollstaendig. '
f'{len(fixable)} brauchen Korrekturen'
f'{f", {len(critical)} fehlen oder sind unbrauchbar" if critical else ""}.</p>'
)
# Concrete actions
actions: list[str] = []
for r in results:
if r.error or r.completeness_pct == 100:
continue
failed_checks = [
c for c in r.checks
if c.level == 1 and not c.passed and not c.skipped
and c.severity != "INFO"
]
for c in failed_checks[:3]: # Max 3 per document
action = _check_to_action(r.label, c.label, c.hint)
if action:
actions.append(action)
if actions:
html.append(
'<h3 style="font-size:14px;color:#334155;margin:16px 0 8px">'
'Konkrete Aufgaben:</h3>'
'<ol style="font-size:13px;color:#475569;padding-left:20px;margin:0">'
)
for a in actions[:10]: # Max 10 actions
html.append(f'<li style="margin-bottom:6px">{a}</li>')
html.append('</ol>')
html.append('</div>')
return "\n".join(html)
def _check_to_action(doc_label: str, check_label: str, hint: str) -> str:
"""Convert a failed check into a plain-language action item."""
# Map technical check labels to business-language actions
label_lower = check_label.lower()
if "datenschutzbeauftragter" in label_lower or "dsb" in label_lower:
return (f"<strong>{doc_label}:</strong> Ihren Datenschutzbeauftragten "
f"mit Kontaktdaten erwaehnen. Pflicht ab 20 Mitarbeitern.")
if "beschwerderecht" in label_lower or "art. 77" in label_lower:
return (f"<strong>{doc_label}:</strong> Hinweis auf das Beschwerderecht "
f"bei der Aufsichtsbehoerde ergaenzen (Name + Kontakt der Behoerde).")
if "betroffenenrechte" in label_lower:
return (f"<strong>{doc_label}:</strong> Alle Betroffenenrechte "
f"(Auskunft, Berichtigung, Loeschung, etc.) einzeln auffuehren.")
if "verantwortlicher" in label_lower:
return (f"<strong>{doc_label}:</strong> Vollstaendige Firmenbezeichnung "
f"mit Rechtsform, Adresse, E-Mail und Telefon eintragen.")
if "interessenabwaegung" in label_lower:
return (f"<strong>{doc_label}:</strong> Bei 'berechtigtem Interesse' "
f"die Abwaegung dokumentieren. Aufgabe fuer den DSB/Rechtsanwalt.")
if "widerrufsbelehrung" in label_lower or "widerruf" in label_lower:
return (f"<strong>{doc_label}:</strong> Gesetzliche Widerrufsbelehrung "
f"mit 14-Tage-Frist und Musterformular bereitstellen.")
if "loeschkonzept" in label_lower:
return (f"<strong>{doc_label}:</strong> Loeschfristen und -prozess "
f"dokumentieren. Aufgabe fuer den DSB.")
if "profiling" in label_lower or "art. 22" in label_lower:
return (f"<strong>{doc_label}:</strong> Hinweis ergaenzen ob "
f"automatisierte Entscheidungen stattfinden oder nicht.")
if "nicht im eingereichten text" in label_lower:
return (f"<strong>{doc_label}:</strong> Das eingereichte Dokument "
f"enthaelt nicht den erwarteten Inhalt. Bitte korrekte URL pruefen.")
# Generic fallback
if hint and len(hint) < 150:
return f"<strong>{doc_label}:</strong> {hint[:120]}"
return f"<strong>{doc_label}:</strong> '{check_label}' muss ergaenzt werden."
def build_html_report( def build_html_report(
results: list[DocCheckResult], results: list[DocCheckResult],
cookie_result: dict | None, cookie_result: dict | None,
@@ -163,10 +163,16 @@ async def detect_business_profile(documents: dict[str, str]) -> BusinessProfile:
full_text = "\n".join(documents.values()).lower() full_text = "\n".join(documents.values()).lower()
full_text = full_text.replace("\xad", "") # strip soft hyphens full_text = full_text.replace("\xad", "") # strip soft hyphens
# ── Tracking services ──────────────────────────────────────── # ── Tracking services (use full service detector) ──────────
for pattern, label in _TRACKING_SERVICES.items(): try:
if pattern in full_text: from compliance.services.service_detector import detect_services_in_text
profile.detected_services.append(label) detected = detect_services_in_text(full_text)
profile.detected_services = [s["name"] for s in detected]
except Exception:
# Fallback to simple keyword list
for pattern, label in _TRACKING_SERVICES.items():
if pattern in full_text:
profile.detected_services.append(label)
# ── Online shop ────────────────────────────────────────────── # ── Online shop ──────────────────────────────────────────────
shop_hits = _count_hits(full_text, _ONLINE_SHOP_KEYWORDS) shop_hits = _count_hits(full_text, _ONLINE_SHOP_KEYWORDS)
@@ -177,8 +183,20 @@ async def detect_business_profile(documents: dict[str, str]) -> BusinessProfile:
profile.has_editorial_content = editorial_hits >= 2 profile.has_editorial_content = editorial_hits >= 2
# ── Regulated profession ───────────────────────────────────── # ── Regulated profession ─────────────────────────────────────
# Only check impressum text (not full text) — keywords like "rechtsanwalt"
# appear as contact persons in DSI texts (e.g. Spiegel's "Rechtsanwalt Kruse")
# but that doesn't mean the company IS a law firm.
impressum_text = documents.get("impressum", "").lower().replace("\xad", "")
if not impressum_text:
impressum_text = full_text[:2000] # Fallback: first 2000 chars
for keyword, prof_type in _REGULATED_PROFESSIONS.items(): for keyword, prof_type in _REGULATED_PROFESSIONS.items():
if keyword in full_text: if keyword in impressum_text:
# Extra guard: "rechtsanwalt" must appear near the company description,
# not just as a contact person name
if keyword in ("rechtsanwalt", "rechtsanwaeltin", "rechtsanwältin"):
# Check if it's in the first 500 chars (company description area)
if keyword not in impressum_text[:500]:
continue
profile.is_regulated_profession = True profile.is_regulated_profession = True
profile.regulated_profession_type = prof_type profile.regulated_profession_type = prof_type
break break
@@ -64,6 +64,22 @@ def extract_profile_from_documents(
"regulated_profession_type", "" "regulated_profession_type", ""
) )
# ── Detected services (full list with metadata) ────────────
try:
from compliance.services.service_detector import detect_services_in_text
detected = detect_services_in_text(all_text)
result["detected_services"] = detected
# Add non-EU services as scope hint
non_eu = [s for s in detected if not s.get("eu_adequate")]
if non_eu:
result["compliance_scope_hints"].append({
"field": "hasThirdCountryTransfer",
"value": True,
"source": f"{len(non_eu)} Dienste ausserhalb EWR erkannt ({', '.join(s['name'] for s in non_eu[:5])}...)",
})
except Exception as e:
logger.warning("Service detection failed: %s", e)
# ── Scope hints from document content ──────────────────────── # ── Scope hints from document content ────────────────────────
_extract_scope_hints(all_text, result) _extract_scope_hints(all_text, result)
@@ -213,3 +213,197 @@ def auto_fill_from_dsi(doc_entries: list[dict]) -> None:
"Auto-filled %d empty rows from DSI sections: %s", "Auto-filled %d empty rows from DSI sections: %s",
len(filled), ", ".join(filled), len(filled), ", ".join(filled),
) )
# ── Cross-Document Search ────────────────────────────────────────────
# Keywords that indicate a doc_type is present in text (case-insensitive)
_DOC_TYPE_KEYWORDS = {
"widerruf": [
"widerrufsrecht", "widerrufsbelehrung", "widerrufsfrist",
"binnen 14 tagen", "widerruf erklaeren", "muster-widerrufsformular",
],
"cookie": [
"cookie-richtlinie", "cookie-tabelle", "cookiebot", "consent-tool",
"arten der cookies", "session-cookie", "tracking-cookie",
],
"social_media": [
"gemeinsam verantwortlich", "art. 26 dsgvo", "fanpage",
"social media plugin", "facebook-seite", "instagram-profil",
],
"impressum": [
"angaben gemaess", "angaben gemäß", "§ 5 tmg", "§5 tmg",
"telemediengesetz", "impressum",
],
"agb": [
"allgemeine geschaeftsbedingungen", "allgemeine geschäftsbedingungen",
"geltungsbereich", "vertragsschluss", "§305 bgb",
],
"dsb": [
"datenschutzbeauftragte", "dsb@", "dpo@",
"datenschutzbeauftragten",
],
}
def cross_search_documents(doc_entries: list[dict]) -> list[dict]:
"""Search ALL texts for ALL doc_types and fill missing entries.
For each empty doc_type row, search through all other documents'
texts to find the content. If found in the wrong document, extract
it, assign it, and create a finding about incorrect placement.
Returns list of findings (misplacement warnings).
"""
findings: list[dict] = []
# Collect all available texts with their source doc_type
all_texts: list[tuple[str, str, str]] = [] # (doc_type, url, text)
for entry in doc_entries:
if entry.get("text") and len(entry["text"]) > 100:
all_texts.append((entry["doc_type"], entry.get("url", ""), entry["text"]))
if not all_texts:
return findings
# For each entry, check if:
# a) It's empty → search other texts
# b) It has text but the text doesn't match the doc_type → search other texts
for entry in doc_entries:
target_type = entry["doc_type"]
keywords = _DOC_TYPE_KEYWORDS.get(target_type, [])
if not keywords:
continue
has_text = entry.get("text") and len(entry["text"].split()) > 50
text_matches = False
if has_text:
# Check if the current text actually contains this doc_type's content
entry_lower = entry["text"].lower()
match_score = sum(1 for kw in keywords if kw in entry_lower)
text_matches = match_score >= 2
if has_text and text_matches:
continue # Text present AND matches doc_type → skip
# Search all other texts for this doc_type's keywords
best_match: dict | None = None
best_score = 0
for source_type, source_url, source_text in all_texts:
if source_type == target_type:
continue
text_lower = source_text.lower()
score = sum(1 for kw in keywords if kw in text_lower)
if score >= 2 and score > best_score:
best_score = score
# Extract the relevant section
section = _extract_section_by_keywords(source_text, keywords)
if section and len(section.split()) >= 30:
best_match = {
"source_type": source_type,
"source_url": source_url,
"section_text": section,
"keyword_hits": score,
}
if best_match:
entry["text"] = best_match["section_text"]
entry["word_count"] = len(best_match["section_text"].split())
source_label = best_match["source_type"].upper()
entry["url"] = f"(gefunden in {source_label})"
findings.append({
"id": f"placement-{target_type}",
"label": f"{_type_label(target_type)} in falschem Dokument",
"passed": False,
"severity": "MEDIUM",
"level": 1,
"parent": None,
"skipped": False,
"matched_text": "",
"hint": (
f"Die {_type_label(target_type)} wurde nicht als eigenes "
f"Dokument gefunden, sondern in der/den {source_label} "
f"({best_match['source_url']}). Gemaess Art. 246a EGBGB / "
f"§312d BGB muss die {_type_label(target_type)} leicht "
f"auffindbar und klar erkennbar sein. Empfehlung: Als "
f"eigenen Link im Footer oder als separates Dokument "
f"bereitstellen."
),
"source": "cross_document_search",
"doc_type": target_type,
})
logger.info(
"Cross-doc: Found %s in %s (%d keywords, %d words)",
target_type, best_match["source_type"],
best_match["keyword_hits"],
entry["word_count"],
)
elif has_text and not text_matches:
# Text present but doesn't match — wrong text assigned
findings.append({
"id": f"wrong-text-{target_type}",
"label": f"{_type_label(target_type)} nicht im eingereichten Text",
"passed": False,
"severity": "HIGH",
"level": 1,
"parent": None,
"skipped": False,
"matched_text": "",
"hint": (
f"Der eingereichte Text enthaelt keine "
f"{_type_label(target_type)}. Moeglicherweise wurde "
f"die falsche URL eingegeben. Das System konnte die "
f"{_type_label(target_type)} auch in keinem anderen "
f"eingereichten Dokument finden."
),
"source": "cross_document_search",
"doc_type": target_type,
})
logger.info("Cross-doc: %s text doesn't match doc_type, not found elsewhere", target_type)
return findings
def _extract_section_by_keywords(
text: str, keywords: list[str],
) -> str | None:
"""Extract the section of text around the keyword matches."""
text_lower = text.lower()
lines = text.split("\n")
# Find first and last line containing any keyword
first_line = len(lines)
last_line = 0
for i, line in enumerate(lines):
line_lower = line.lower()
if any(kw in line_lower for kw in keywords):
first_line = min(first_line, i)
last_line = max(last_line, i)
if first_line >= last_line:
return None
# Expand to include context (5 lines before first, 10 after last)
start = max(0, first_line - 5)
end = min(len(lines), last_line + 10)
section = "\n".join(lines[start:end])
return section if len(section.split()) >= 30 else None
def _type_label(doc_type: str) -> str:
labels = {
"widerruf": "Widerrufsbelehrung",
"cookie": "Cookie-Richtlinie",
"social_media": "Social-Media-Datenschutz",
"impressum": "Impressum",
"agb": "AGB",
"dsb": "DSB-Kontakt",
"dse": "Datenschutzerklaerung",
}
return labels.get(doc_type, doc_type)
@@ -0,0 +1,128 @@
"""
Service Detector find all third-party services mentioned in legal texts.
Uses the service_registry (88+ services) as detection source.
Works on lowercased text with simple keyword matching.
Returns structured list of detected services with metadata.
"""
import logging
import re
from compliance.services.service_registry import SERVICE_REGISTRY
logger = logging.getLogger(__name__)
# Build a simple name→metadata lookup from the registry
_SERVICE_BY_NAME: dict[str, dict] = {}
_NAME_PATTERNS: list[tuple[str, dict]] = []
for _pattern, _meta in SERVICE_REGISTRY.items():
name = _meta["name"]
_SERVICE_BY_NAME[name.lower()] = _meta
# Also build lowercase search keywords from the name
_NAME_PATTERNS.append((name.lower(), _meta))
# Additional text-based patterns (services often mentioned by name in DSI,
# not by script URL pattern)
_EXTRA_TEXT_PATTERNS: dict[str, dict] = {
"adobe": {"id": "adobe", "name": "Adobe", "category": "tracking",
"provider": "Adobe Inc.", "country": "US", "eu_adequate": False},
"sourcepoint": {"id": "sourcepoint", "name": "Sourcepoint", "category": "cmp",
"provider": "Sourcepoint Technologies", "country": "US", "eu_adequate": False},
"salesforce": {"id": "salesforce", "name": "Salesforce", "category": "crm",
"provider": "Salesforce Inc.", "country": "US", "eu_adequate": False},
"qualtrics": {"id": "qualtrics", "name": "Qualtrics", "category": "survey",
"provider": "Qualtrics LLC", "country": "US", "eu_adequate": False},
"jw player": {"id": "jw_player", "name": "JW Player", "category": "video",
"provider": "Longtail Ad Solutions", "country": "US", "eu_adequate": False},
"omnystudio": {"id": "omnystudio", "name": "Omnystudio", "category": "audio",
"provider": "Triton Digital", "country": "CA", "eu_adequate": False},
"storifyme": {"id": "storifyme", "name": "Storifyme", "category": "content",
"provider": "Storifyme GmbH", "country": "DE", "eu_adequate": True},
"iqd": {"id": "iqd", "name": "IQD", "category": "marketing",
"provider": "IQ Digital Media Marketing", "country": "DE", "eu_adequate": True},
"id5": {"id": "id5", "name": "ID5", "category": "identity",
"provider": "ID5 Technology Ltd", "country": "GB", "eu_adequate": True},
"utiq": {"id": "utiq", "name": "Utiq", "category": "tracking",
"provider": "Utiq SA/NV", "country": "BE", "eu_adequate": True},
"mapbox": {"id": "mapbox", "name": "Mapbox", "category": "maps",
"provider": "Mapbox Inc.", "country": "US", "eu_adequate": False},
"tiktok": {"id": "tiktok", "name": "TikTok", "category": "social",
"provider": "TikTok Technology Limited", "country": "IE", "eu_adequate": True},
"spotify": {"id": "spotify", "name": "Spotify", "category": "audio",
"provider": "Spotify AB", "country": "SE", "eu_adequate": True},
"reddit": {"id": "reddit", "name": "Reddit", "category": "social",
"provider": "Reddit Inc.", "country": "US", "eu_adequate": False},
"bluesky": {"id": "bluesky", "name": "Bluesky", "category": "social",
"provider": "Bluesky PBLLC", "country": "US", "eu_adequate": False},
"giphy": {"id": "giphy", "name": "Giphy", "category": "content",
"provider": "Meta Platforms", "country": "US", "eu_adequate": False},
"imgur": {"id": "imgur", "name": "Imgur", "category": "content",
"provider": "Imgur Inc.", "country": "US", "eu_adequate": False},
"instagram": {"id": "instagram", "name": "Instagram", "category": "social",
"provider": "Meta Platforms", "country": "US", "eu_adequate": False},
"facebook": {"id": "facebook", "name": "Facebook", "category": "social",
"provider": "Meta Platforms", "country": "US", "eu_adequate": False},
"meta platforms": {"id": "meta_platforms", "name": "Meta Platforms", "category": "social",
"provider": "Meta Platforms Inc.", "country": "US", "eu_adequate": False},
"linkedin": {"id": "linkedin", "name": "LinkedIn", "category": "marketing",
"provider": "LinkedIn Corp.", "country": "US", "eu_adequate": False},
"twitter": {"id": "twitter", "name": "X/Twitter", "category": "social",
"provider": "X Corp.", "country": "US", "eu_adequate": False},
"x.com": {"id": "x_com", "name": "X/Twitter", "category": "social",
"provider": "X Corp.", "country": "US", "eu_adequate": False},
"recaptcha": {"id": "recaptcha", "name": "Google reCAPTCHA", "category": "security",
"provider": "Google LLC", "country": "US", "eu_adequate": False},
"xandr": {"id": "xandr", "name": "Xandr", "category": "marketing",
"provider": "Microsoft/Xandr", "country": "US", "eu_adequate": False},
"criteo": {"id": "criteo", "name": "Criteo", "category": "marketing",
"provider": "Criteo SA", "country": "FR", "eu_adequate": True},
"outbrain": {"id": "outbrain", "name": "Outbrain", "category": "marketing",
"provider": "Outbrain Inc.", "country": "US", "eu_adequate": False},
"taboola": {"id": "taboola", "name": "Taboola", "category": "marketing",
"provider": "Taboola Inc.", "country": "US", "eu_adequate": False},
"piano": {"id": "piano", "name": "Piano", "category": "paywall",
"provider": "Piano Software Inc.", "country": "US", "eu_adequate": False},
"microsoft": {"id": "microsoft", "name": "Microsoft", "category": "cloud",
"provider": "Microsoft Corp.", "country": "US", "eu_adequate": False},
"amazon web services": {"id": "aws", "name": "AWS", "category": "cloud",
"provider": "Amazon Web Services", "country": "US", "eu_adequate": False},
}
def detect_services_in_text(text: str) -> list[dict]:
"""Detect all third-party services mentioned in a legal document text.
Searches for:
1. Service names from service_registry (88+ entries)
2. Additional common service names from _EXTRA_TEXT_PATTERNS
3. Generic "Auftragsverarbeiter" / provider patterns
Returns list of detected service dicts with name, category, country, etc.
"""
text_lower = text.lower()
found: dict[str, dict] = {} # id -> metadata (dedup)
# 1. Registry services (by name)
for name_lower, meta in _NAME_PATTERNS:
# Search for service name as word (not substring)
if name_lower in text_lower:
sid = meta.get("id", name_lower)
if sid not in found:
found[sid] = {**meta, "source": "registry"}
# 2. Extra text patterns
for keyword, meta in _EXTRA_TEXT_PATTERNS.items():
if keyword in text_lower:
sid = meta["id"]
if sid not in found:
found[sid] = {**meta, "source": "text_pattern"}
# 3. Dedup: x.com and twitter are the same
if "x_com" in found and "twitter" in found:
del found["x_com"]
logger.info("Detected %d services in text (%d words)",
len(found), len(text.split()))
return list(found.values())
+1 -1
View File
@@ -335,7 +335,7 @@ async def dsi_discovery(req: DSIDiscoveryRequest):
doc_type=d.doc_type, doc_type=d.doc_type,
word_count=d.word_count, word_count=d.word_count,
text_preview=d.text[:500] if d.text else "", text_preview=d.text[:500] if d.text else "",
full_text=d.text[:50000] if d.text else "", full_text=d.text[:200000] if d.text else "",
) )
for d in result.documents for d in result.documents
], ],
+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:"):] text_pattern = selector[len("shadow-click:"):]
return await _click_in_shadow_dom(page, text_pattern) return await _click_in_shadow_dom(page, text_pattern)
# 1. Try main document
try: try:
locator = page.locator(selector).first locator = page.locator(selector).first
await locator.wait_for(state="visible", timeout=timeout) await locator.wait_for(state="visible", timeout=timeout)
await locator.click() await locator.click()
return True return True
except Exception: except Exception:
# Fallback: try Shadow DOM click with selector text pass
# Extract button text from selector like 'button:has-text("Accept all")'
if ':has-text("' in selector: # 2. Fallback: try inside iframes (Sourcepoint, Quantcast, etc.)
text = selector.split(':has-text("')[1].rstrip('")') try:
return await _click_in_shadow_dom(page, text) for iframe_sel in [
return False "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
+34 -10
View File
@@ -417,7 +417,7 @@ async def discover_dsi_documents(
title=title, url=href, source_url=url, title=title, url=href, source_url=url,
language=lang, language=lang,
doc_type="cross_domain" if not _is_allowed_domain(href, base_domain) else "html_page", doc_type="cross_domain" if not _is_allowed_domain(href, base_domain) else "html_page",
text=text[:50000], word_count=len(text.split()), text=text[:200000], word_count=len(text.split()),
)) ))
# Recursive: search THIS page for more DSI links # Recursive: search THIS page for more DSI links
@@ -532,19 +532,43 @@ async def _find_dsi_links(page: Page, base_domain: str) -> list[dict]:
return [] return []
async def _expand_all_interactive(page: Page) -> None: async def _expand_all_interactive(page: Page) -> None:
"""Expand all accordions, tabs, details, dropdowns on the page.""" """Expand all accordions, tabs, details, dropdowns on the page.
IMPORTANT: Only expand CLOSED elements. Never click elements that
are already expanded (aria-expanded="true") that would close them.
BMW, for example, has accordions open by default.
"""
try: try:
await page.evaluate("""() => { await page.evaluate("""() => {
// 1. Open all <details> that are closed
document.querySelectorAll('details:not([open])').forEach(d => d.open = true); document.querySelectorAll('details:not([open])').forEach(d => d.open = true);
const sels = ['button[aria-expanded="false"]','[data-toggle="collapse"]',
'[data-bs-toggle="collapse"]','[class*="accordion"] > button', // 2. Click buttons that are explicitly CLOSED (aria-expanded="false")
'[class*="collapse"] > button','.panel-heading a']; document.querySelectorAll('button[aria-expanded="false"]').forEach(b => {
sels.forEach(s => document.querySelectorAll(s).forEach(e => { try{e.click()}catch{} })); try { b.click(); } catch {}
document.querySelectorAll('button,a').forEach(b => { });
if (/^(mehr|more|weiterlesen|read more|show more|anzeigen|alle anzeigen)/i.test((b.textContent||'').trim()))
try{b.click()}catch{} // 3. Bootstrap/jQuery collapse triggers (only closed ones)
document.querySelectorAll('[data-toggle="collapse"].collapsed').forEach(e => {
try { e.click(); } catch {}
});
document.querySelectorAll('[data-bs-toggle="collapse"].collapsed').forEach(e => {
try { e.click(); } catch {}
});
// 4. "Show more" / "Mehr anzeigen" buttons
document.querySelectorAll('button,a').forEach(b => {
const t = (b.textContent || '').trim();
if (/^(mehr|more|weiterlesen|read more|show more|anzeigen|alle anzeigen)/i.test(t))
try { b.click(); } catch {}
});
// 5. Tabs click each to make content visible, then go back
// (don't click, just make tab panels visible)
document.querySelectorAll('[role="tabpanel"][hidden]').forEach(p => {
p.removeAttribute('hidden');
p.style.display = '';
}); });
document.querySelectorAll('[role="tab"]').forEach(t => { try{t.click()}catch{} });
}""") }""")
except Exception: except Exception:
pass pass
+24 -12
View File
@@ -81,20 +81,32 @@ async def try_dismiss_consent_banner(page: Page) -> bool:
except Exception: except Exception:
continue 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: try:
sp_div = await page.query_selector("div[id^='sp_message']") for frame in page.frames:
if sp_div: if frame == page.main_frame:
# Sourcepoint renders in an iframe inside sp_message_container continue
sp_iframe = page.frame_locator("iframe[id^='sp_message']") try:
accept_btn = sp_iframe.locator(".sp_choice_type_11").first # Sourcepoint accept button
if await accept_btn.count() > 0: sp_btn = frame.locator(".sp_choice_type_11").first
await accept_btn.click(timeout=5000) if await sp_btn.count() > 0 and await sp_btn.is_visible():
logger.info("Dismissed Sourcepoint consent banner (iframe)") await sp_btn.click(timeout=5000)
await page.wait_for_timeout(3000) logger.info("Dismissed Sourcepoint consent (iframe: %s)", frame.url[:80])
return True 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: 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 # 4) Use banner_detector CMP selectors as fallback
try: try:
+1
View File
@@ -54,6 +54,7 @@ is_excluded() {
*.md|*.json|*.yaml|*.yml|*.lock|*.sum|*.mod|*.toml|*.cfg|*.ini) return 0 ;; *.md|*.json|*.yaml|*.yml|*.lock|*.sum|*.mod|*.toml|*.cfg|*.ini) return 0 ;;
*.html|*.html.j2|*.jinja|*.jinja2) return 0 ;; *.html|*.html.j2|*.jinja|*.jinja2) return 0 ;;
*.svg|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.woff|*.woff2|*.ttf) return 0 ;; *.svg|*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.woff|*.woff2|*.ttf) return 0 ;;
*.xls|*.xlsx|*.xlsm|*.docx|*.pptx|*.zip|*.tar|*.gz) return 0 ;;
*.generated.*|*.gen.*|*_pb.go|*_pb2.py|*.pb.go) return 0 ;; *.generated.*|*.gen.*|*_pb.go|*_pb2.py|*.pb.go) return 0 ;;
esac esac
return 1 return 1
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# Emit per-service + aggregate change flags for the CI / build workflows.
#
# Reads:
# BASE_SHA — diff base. Empty / unreachable → emit everything as true.
# HEAD_SHA — diff target. Defaults to HEAD.
#
# Writes key=value lines to $GITHUB_OUTPUT (defaults to /dev/stdout for local runs).
#
# Keys emitted:
# admin, backend, sdk, portal, tts, crawler, dsms_gateway, dsms_node
# any_python, any_node, any
set -euo pipefail
BASE_SHA="${BASE_SHA:-}"
HEAD_SHA="${HEAD_SHA:-HEAD}"
OUT="${GITHUB_OUTPUT:-/dev/stdout}"
ALL_KEYS=(admin backend sdk portal tts crawler dsms_gateway dsms_node any_python any_node any)
emit() {
echo "$1=$2" >> "$OUT"
}
emit_all_true() {
reason=$1
echo "→ rebuild all: $reason"
for k in "${ALL_KEYS[@]}"; do
emit "$k" true
done
}
if [ -z "$BASE_SHA" ]; then
emit_all_true "no BASE_SHA provided"
exit 0
fi
if ! git rev-parse --verify "${BASE_SHA}^{commit}" >/dev/null 2>&1; then
emit_all_true "BASE_SHA ${BASE_SHA} unreachable"
exit 0
fi
changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true)
echo "Changed files since ${BASE_SHA}:"
echo "${changed:-(none)}"
echo "---"
check() {
key=$1
path=$2
if echo "$changed" | grep -q "^${path}/"; then
emit "$key" true
echo " ${key} (${path}/): true"
else
emit "$key" false
echo " ${key} (${path}/): false"
fi
}
check admin admin-compliance
check backend backend-compliance
check sdk ai-compliance-sdk
check portal developer-portal
check tts compliance-tts-service
check crawler document-crawler
check dsms_gateway dsms-gateway
check dsms_node dsms-node
# Aggregate flags
if echo "$changed" | grep -qE "^(backend-compliance|document-crawler|dsms-gateway|compliance-tts-service)/"; then
emit any_python true
echo " any_python: true"
else
emit any_python false
echo " any_python: false"
fi
if echo "$changed" | grep -qE "^(admin-compliance|developer-portal)/"; then
emit any_node true
echo " any_node: true"
else
emit any_node false
echo " any_node: false"
fi
if [ -n "$changed" ]; then
emit any true
echo " any: true"
else
emit any false
echo " any: false"
fi
+66 -70
View File
@@ -2,8 +2,8 @@
**URL:** https://www.bmw.de **URL:** https://www.bmw.de
**Typ:** Konzern / B2C Automobil **Typ:** Konzern / B2C Automobil
**Datum:** 2026-05-12 **Datum:** 2026-05-15 (URLs + Inhalte verifiziert)
**Batch-Test:** 8/9 L1, 10/21 L2 (Mangelhaft, 48%) **Vorheriger Batch-Test:** 8/9 L1, 10/21 L2 (Mangelhaft, 48%) — VERALTET, URLs waren alle 404
--- ---
@@ -20,83 +20,88 @@
--- ---
## Dokumente ## Dokumente (URLs verifiziert 2026-05-15)
| Dokumenttyp | Vorhanden | URL | **ACHTUNG: BMW verteilt Rechtstexte ueber 3 Domains!**
|-------------|-----------|-----|
| DSI | Ja | https://www.bmw.de/de/footer/metanavigation/datenschutz.html | | Dokumenttyp | Domain | URL |
| Impressum | Ja | https://www.bmw.de/de/footer/metanavigation/impressum.html | |-------------|--------|-----|
| Cookie-Richtlinie | Ja (separate Seite) | https://www.bmw.de/de/footer/metanavigation/cookie-policy.html | | DSI | bmw.de | https://www.bmw.de/de/footer/metanavigation/data-privacy.html |
| AGB | Ja | TODO: URL verifizieren | | Impressum | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-notice-pool/imprint.html |
| Widerruf | Ggf. in AGB | — | | Cookie-Richtlinie | bmw.de | https://www.bmw.de/de/footer/footer-section/cookie-policy.html |
| Social Media DSE | Nein | — | | Legal Disclaimer / NB | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-disclaimer-pool/legal-disclaimer.html |
| Nutzungsbedingungen | Ja | TODO: URL verifizieren | | Konzern-Datenschutz + Widerruf | **bmwgroup.com** | https://www.bmwgroup.com/de/general/data_privacy.html |
| DSB-Kontakt | In DSI | — | | Social Media Privacy | **bmwgroup.jobs** | https://www.bmwgroup.jobs/de/de/services/social-media-privacy-policy.html |
| DSB-Kontakt | bmw.de (in DSI) | datenschutz@bmw.de |
| AGB | **Nicht gefunden** | Kein oeffentlich verlinktes AGB-Dokument |
**Finding: Rechtstexte ueber 3 Domains verteilt (bmw.de, bmwgroup.com, bmwgroup.jobs). Fuer Betroffene schwer auffindbar. Social Media Policy nur ueber Karriere-Portal erreichbar.**
**DSI hat Sub-Pages:** data-category.html, privacy-subpage-weblink-a/c/d/e.html
---
## Verifizierte Inhalte: Impressum
| Feld | Wert |
|------|------|
| Firma | Bayerische Motoren Werke Aktiengesellschaft (BMW AG) |
| Anschrift | Petuelring 130, 80809 Muenchen |
| Vorstand | Milan Nedeljkovic (Vorsitzender), 6 weitere |
| Aufsichtsrat | Nicolas Peter (Vorsitzender) |
| Registergericht | AG Muenchen, HRB 42243 |
| USt-IdNr | DE129273398 |
| Kontakt | kundenbetreuung@bmw.de |
| Versicherungsvermittler | §34d Abs. 6 GewO, IHK Muenchen |
## Verifizierte Inhalte: DSB
| Feld | Wert |
|------|------|
| Titel | Data Protection Officer BMW AG |
| Anschrift | Petuelring 130, 80788 Muenchen |
| E-Mail | datenschutz@bmw.de |
## Verifizierte Inhalte: Social Media
Impressum gilt auch fuer:
- Facebook: facebook.com/BMWDeutschland
- Instagram: instagram.com/bmwdeutschland
- YouTube: youtube.com/user/BMWDeutschland
- Pinterest: de.pinterest.com/bmwdeutschland
--- ---
## Erwartete Ergebnisse: DSI (Art. 13 DSGVO) ## Erwartete Ergebnisse: DSI (Art. 13 DSGVO)
### L1 Checks (8/9) ### L1 Checks
| Check | Erwartet | Begruendung | | Check | Erwartet | Begruendung |
|-------|----------|-------------| |-------|----------|-------------|
| Verantwortlicher | PASS | BMW AG, Muenchen | | Verantwortlicher | PASS | BMW AG, Petuelring 130, 80809 Muenchen |
| DSB | PASS | DSB erwaehnt | | DSB | PASS | datenschutz@bmw.de, Petuelring 130 |
| Zwecke | PASS | Ausfuehrlich | | Zwecke | PASS | Ausfuehrlich (Sub-Pages) |
| Rechtsgrundlage | PASS | Art. 6 Referenzen | | Rechtsgrundlage | PASS | Art. 6 Referenzen |
| Empfaenger | PASS | Kategorien aufgezaehlt | | Empfaenger | PASS | Kategorien aufgezaehlt |
| Drittlandtransfer | PASS | USA-Transfer erwaehnt | | Drittlandtransfer | PASS | USA-Transfer erwaehnt |
| Speicherdauer | PASS | Zeitangaben vorhanden | | Speicherdauer | PASS | Zeitangaben vorhanden |
| Betroffenenrechte | **FAIL** | Rechte ohne Art.-Referenzen | | Betroffenenrechte | Zu pruefen | Art. 15-21 in DSI? |
| Beschwerderecht | **FAIL** | Art. 77 nicht explizit erwaehnt | | Beschwerderecht | Zu pruefen | Art. 77 in DSI? |
### L2 Checks (10/21 — verifizierte True Positives)
| Check | Erwartet | TP/FP |
|-------|----------|-------|
| Anschrift | PASS | — |
| E-Mail | **FAIL** | **TP** — Keine direkte E-Mail-Adresse fuer DSB angegeben |
| Telefon | PASS | — |
| DSB Kontakt | PASS | — |
| Art. 6(1)(a) | PASS | — |
| Art. 6(1)(b) | PASS | — |
| Art. 6(1)(f) | PASS | — |
| Interessenabwaegung | **FAIL** | **TP** — Keine dokumentierte Abwaegung |
| Transfermechanismus | **FAIL** | **TP** — Kein SCC/DPF benannt |
| Art. 15-18,20,21 | **FAIL** | **TP** — Rechte ohne Artikel-Referenzen aufgezaehlt |
| Art. 22 Profiling | **FAIL** | **TP** — Kein Profiling-Hinweis trotz Konfigurator/Personalisierung |
| Aufsichtsbehoerde | **FAIL** | **TP** — Keine konkrete Behoerde benannt |
| Loeschkonzept | **FAIL** | **TP** — Kein Loeschkonzept referenziert |
**Verifiziert: BMW hat tatsaechlich eine lueckenhafte DSI. Die Findings sind True Positives.**
--- ---
## Erwartete Ergebnisse: Impressum ## Erwartete Ergebnisse: Impressum
| Check | Erwartet | Begruendung |
|-------|----------|-------------|
| Firmenname | PASS | BMW AG |
| Anschrift | PASS | Petuelring 130, 80809 Muenchen |
| Vertretung | PASS | Vorstand benannt |
| Registergericht | PASS | AG Muenchen, HRB 42243 |
| USt-IdNr | PASS | DE 129 273 987 |
| V.i.S.d.P. | PASS | Hat redaktionelle Inhalte |
| Streitbeilegung | AKTIV | B2C mit Online-Angebot → ODR relevant |
---
## Erwartete Ergebnisse: Cookie-Richtlinie
| Check | Erwartet | | Check | Erwartet |
|-------|----------| |-------|----------|
| Cookie-Arten | PASS (Essential, Analytics, Marketing) | | Firmenname | PASS (BMW AG) |
| Cookie-Zwecke | PASS | | Anschrift | PASS (Petuelring 130, 80809 Muenchen) |
| Speicherdauern | TODO: verifizieren | | Vertretung | PASS (Vorstand benannt) |
| Drittanbieter | PASS (Google, Meta etc.) | | USt-IdNr | PASS (DE129273398) |
| Rechtsgrundlage | TODO: §25 TDDDG? | | Registergericht | PASS (HRB 42243) |
| Consent-Tool | PASS (OneTrust o.ae.) | | V.i.S.d.P. | PASS (Medienunternehmen mit Blog) |
| Streitbeilegung | AKTIV (B2C) |
| Versicherungsvermittler | PASS (§34d GewO, IHK Muenchen) |
--- ---
@@ -106,16 +111,7 @@
|------|----------| |------|----------|
| banner_detected | true | | banner_detected | true |
| provider | OneTrust oder aehnlich | | provider | OneTrust oder aehnlich |
| violations | Mehrere (grosser Konzern mit viel Tracking) | | violations | Zu pruefen |
---
## Cross-Check Banner vs Cookie
| Finding | Erwartet |
|---------|----------|
| Dienste fehlen in Cookie-RL | Moeglich (viele Third-Party-Tracker) |
| Tracking vor Consent | Moeglich (Pre-Consent Analytics) |
--- ---
@@ -124,6 +120,6 @@
| Check | Filter | Begruendung | | Check | Filter | Begruendung |
|-------|--------|-------------| |-------|--------|-------------|
| ODR | AKTIV | B2C mit Online-Angebot | | ODR | AKTIV | B2C mit Online-Angebot |
| Widerruf | AKTIV | B2C | | Widerruf | In DSI | Marketing-Consent widerrufbar |
| Berufsrecht | SKIP | Kein regulierter Beruf | | Berufsrecht | SKIP | Kein regulierter Beruf |
| V.i.S.d.P. | AKTIV | Hat Magazine/Blog | | V.i.S.d.P. | AKTIV | Hat Magazine/Blog |
@@ -0,0 +1,677 @@
Datenschutzerklärung
So gehen wir mit Ihren Daten um
X.com
Facebook
E-Mail
Link kopieren
switch to english version
Bereich
Inhaltsverzeichnis
aufklappen
Datenschutz bei uns: Transparent, fair und sicher
Verantwortliche Stelle und Gemeinsame Verantwortlichkeit innerhalb der SPIEGEL-Gruppe
Möglichkeiten zur Nutzung unserer Webseite: "Werbefrei-lesen-Option"
Adobe-basiertes Nutzungs- und Marketing-Kampagnen-Tracking (auch: Werbe-Tracking)
Funktionsfähigkeit des Angebots
Vertragsbeziehungen
Werbe-Tracking
Eigene Produkt- und Vertriebsentwicklung
Einbinden von Drittinhalten
Verlagsspezifische Verarbeitungen
Ihre Rechte
Anforderungen des Data Act
Bereich
Datenschutz bei uns: Transparent, fair und sicher
aufklappen
Vielen Dank für Ihren Besuch auf unserer Webseite! Wir möchten, dass Sie sich bei uns wohlfühlen und das beginnt mit einem verantwortungsvollen Umgang mit Ihren persönlichen Daten. Ob Sie sich einfach nur informieren, mit uns in Kontakt treten oder unsere Angebote nutzen: Transparenz und Sicherheit stehen für uns an erster Stelle. In dieser Datenschutzerklärung erfahren Sie, welche Informationen wir erfassen, warum wir das tun und wie wir Ihre Daten schützen. Wir kümmern uns darum, dass Ihre Daten bei uns in guten Händen sind.
Die folgenden Verlinkungen sollen Ihnen helfen, Ihren Auswahlmöglichkeiten möglichst komfortabel nachgehen zu können:
Hier gelangen Sie in unser Privacy Center:
Zu den Einstellungen
Zustimmung widerrufen
Hier können Sie die von uns gesetzten Cookies in Ihrem Browser löschen:
Cookies löschen
Bereich
1. Verantwortliche Stelle und Gemeinsame Verantwortlichkeit innerhalb der SPIEGEL-Gruppe
aufklappen
Datenschutzrechtlich verantwortlich ist die DER SPIEGEL GmbH & Co. KG. Dessen Datenschutzteam erreichen Sie unterdatenschutz@spiegelgruppe.de  oder unter dem Kennwort Datenschutz, Ericusspitze 1, 20459 Hamburg, Deutschland.
Wir möchten Sie darüber informieren, dass innerhalb der SPIEGEL-Gruppe eine gemeinsame Verantwortlichkeit nach Art. 26 Datenschutzgrundverordnung (DSGVO) für die Verarbeitung Ihrer personenbezogenen Daten besteht. Die gemeinsame datenschutzrechtliche Verantwortlichkeit ermöglicht es uns, Ihnen unsere Dienstleistungen effizient anzubieten, indem die Verantwortlichen gemeinsam über Mittel und Zwecke der Datenverarbeitung entscheiden und wesentliche Zuständigkeiten sinnvoll verteilt werden können. Konkret handelt es sich bei den Verantwortlichen um die SPIEGEL-Verlag Rudolf Augstein GmbH & Co. KG, die DER SPIEGEL GmbH & Co. KG, die manager magazin Verlagsgesellschaft mbH, die manager magazin new media GmbH & Co. KG, die 11FREUNDE Verlag GmbH & Co. KG, die QS Quality Service GmbH, die SPIEGEL Tech Lab GmbH und andere Gesellschaften der SPIEGEL-Gruppe mit geringer operativer Relevanz.
Diese zur SPIEGEL-Gruppe gehörenden Unternehmen haben eine Vereinbarung getroffen, die die jeweiligen Aufgaben und Verantwortlichkeiten bei der Verarbeitung Ihrer Daten regelt. Unabhängig davon, welches Unternehmen der SPIEGEL-Gruppe Ihre Daten konkret verarbeitet, können Sie Ihre Rechte als betroffene Person gegenüber jedem Unternehmen der Gruppe geltend machen. Nähere Informationen zu Ihren Ansprechpartnern erhalten Sie in Ziffer 9 dieser Datenschutzerklärung. Weitere Details zu unserer Vereinbarung über die gemeinsame Verantwortlichkeit stellen wir Ihnen auf Anfrage zur Verfügung.
Bereich
2. Möglichkeiten zur Nutzung unserer Webseite: „Werbefrei-lesen-Option“
aufklappen
Wir bieten Ihnen die Möglichkeit, unsere Webseite auf zwei Arten zu nutzen:
a) Kostenlos mit Werbung und Tracking: Bei dieser Option zeigen wir Ihnen personalisierte Werbung an. Dafür verwenden wir Cookies und ähnliche Technologien, um Ihr Nutzungsverhalten zu analysieren und auf dieser Basis interessen- und bedürfnisgerechte Werbemaßnahmen schalten zu können. Wenn Sie unser Angebot kostenfrei nutzen möchten, finanzieren wir somit unseren Journalismus mit Ihrer dann obligatorischen Einwilligung zur personalisierten Werbung.
b) Werbefrei-lesen-Option (kostenpflichtig): Gegen eine monatliche Gebühr können Sie unsere Inhalte weitgehend werbefrei genießen. In der Werbefrei-lesen-Option sehen Sie keine personalisierte Werbung, sammeln wir deutlich weniger Daten über Ihr Nutzungsverhalten und haben Sie Zugriff auf alle Inhalte, genau wie bei der kostenlosen Version.
Die Werbefrei-lesen-Option ist keine Bezahlschranke für exklusive Inhalte, sondern eine Option für werbefreies Surfen mit erhöhtem Datenschutz. Sie erhalten die gleichen Inhalte wie bei der kostenlosen Nutzung unabhängig von einem bestehenden Abonnement aber ohne Werbung und mit deutlich reduzierter Datenerfassung. Wir bieten Ihnen mit der Werbefrei-lesen-Option daher zu einem marktüblichen Entgelt eine gleichwertige Alternative zu unserem mit personalisierter Werbung kofinanzierten Angebot.
Bereich
3. Adobe-basiertes Nutzungs- und Marketing-Kampagnen-Tracking (auch: Werbe-Tracking)
aufklappen
Wir nutzen auf unseren Angeboten Technologien für nutzungs- und kampagnenbezogene Messung und Aussteuerung ("Nutzungs-Tracking, "Webtracking", "Kampagnentracking") die auf Lösungen von »Adobe« (Adobe Systems Software Ireland Limited, 4-6 Riverwalk, Citywest Business Campus, Saggart, Dublin 24, Ireland) basieren. Damit können wir nachvollziehen, ob und wie Nutzer:innen mit unseren Inhalten und Werbemitteln für eigene Verlagsangebote interagieren (z.B. Aufrufe, Klicks, Conversion-/Erfolgsmessung), Reichweiten und Kampagnenleistungen auswerten sowie Werbung für Verlagsangebote zielgruppenorientiert ausspielen und die Werbeauslieferung steuern.
Hierzu werden abhängig von der konkreten Ausgestaltung Informationen auf Ihrem Endgerät gespeichert und ausgelesen (z.B. Cookies, anderer Browser-Storage, Online-Kennungen) und mit Nutzungsdaten verarbeitet. Dabei können insbesondere technische Informationen (z.B. Geräte-/ Browserdaten, IP-Adresse), Interaktions- und Ereignisdaten (z.B. Seitenaufrufe, Klickpfade, Zeitstempel, Leseverhalten), grobe Standortinformationen sowie Werbe- und Kampagnendaten unserer Verlagsangebote verarbeitet und zu pseudonymen Nutzungsprofilen zusammengeführt werden.
Ein unmittelbarer Personenbezug kann dabei grds. nur dann hergestellt werden, wenn ein Nutzerkonto besteht und Sie sich in unserem Angebot einloggen; nur in diesem Fall können ggf. Informationen aus dem Nutzerkontext berücksichtigt werden. Ohne Login liegen uns keine Klardaten zu Ihrer Person vor, eine Zusammenführung von Nutzungsdaten aus dem Webtracking mit den Daten aus dem Nutzerkontext Ihres Kontos kann bei Verwendung desselben Endgerätes auch noch nach dem Logout geschehen. Die IP-Adresse wird durch Kürzung anonymisiert, sodass hieraus regelmäßig kein unmittelbarer Personenbezug ableitbar ist. Eine Zusammenführung mit Daten, die Sie unmittelbar identifizieren, erfolgt nur, sofern dies ausdrücklich beschrieben ist und die datenschutzrechtlichen Voraussetzungen vorliegen.
Wichtig: Das Adobe-basierte Nutzungs- und Marketing-Kampagnen-Tracking (Werbe-Tracking) setzen wir nicht "pauschal" ein, sondern ausschließlich in Bezug auf konkrete Einzelzwecke, über die wir Sie in dieser Datenschutzerklärung umfassend informieren. In diesem Zusammenhang holen wir soweit rechtlich erforderlich auch Ihre datenschutzrechtliche Einwilligung (einschließlich ggf. der Einwilligung in das Speichern/Auslesen von Informationen auf Ihrem Endgerät) über unsere Consent Management Plattform ein; dort finden Sie auch Informationen zu Widerruf bzw. Änderungsmöglichkeiten Ihrer Auswahl.
Bereich
4. Funktionsfähigkeit des Angebots
aufklappen
Webseiten generell und Telemedien mit ihren dafür erforderlichen technischen Funktionalitäten im Besonderen können nicht ohne Verarbeitung personenbezogener bzw. personenbeziehbarer Daten angeboten werden. Nachfolgend möchten wir Ihnen erläutern, welche Verarbeitungen unbedingt für die technische Funktionsfähigkeit unseres Angebots durchgeführt werden müssen und auch bei der Nutzung der "Werbefrei-lesen-Option" eingesetzt werden.
A) Technisch
Logfiles
Bei jedem Aufruf unserer Webseite erfassen die Server und Applikationen automatisiert Daten und Informationen vom Computersystem des aufrufenden Rechners. Folgende Daten werden vorübergehend erhoben:
IP-Adresse
Datum und Uhrzeit des Zugriffs
Name und URL der abgerufenen Datei
Webseite, von der aus der Zugriff erfolgt (Referrer-URL)
User-Agent (enthält in der Regel Informationen über den verwendeten Browser, das Betriebssystem des Rechners, den Hersteller und die Typenbezeichnung des mobilen Endgeräts)
übertragene Datenmenge
Meldung, ob der Zugriff / Abruf erfolgreich war (http-Statuscode)
Die Datenverarbeitung ist erforderlich, um die Funktionsfähigkeit der Webseite, die Stabilität und Sicherheit unserer Systeme zu gewährleisten, um Missbrauch (z.B. Hacking oder DDos-Attacken) zu erkennen und Schutz vor einem solchen bieten zu können, sowie für Zwecke der Fehleranalyse und -behebung. Die Rechtsgrundlage ist unser berechtigtes Interesse an der Datenverarbeitung gem. Art. 6 Abs. 1 S. 1 lit. f DSGVO. Die Informationen werden in einer Protokolldatei für 30 Tage gespeichert und anschließend automatisch gelöscht. Da die Datenverarbeitung für die Gewährleistung der Sicherheit unserer informationstechnischen Systeme zwingend erforderlich ist, können Sie die Verarbeitung nur vermeiden, indem Sie unseren Dienst nicht nutzen. Speziell hinsichtlich des Erkennens und der der Abwehr von DDos-Attacken arbeiten wir mit dem Anbieter Link11 (Link11 GmbH, Lindleystr. 12, 60314 Frankfurt am Main, www.link11.com ) zusammen. Wir haben mit Link11 einen Auftragsdatenverarbeitungsvertrag nach Art. 28 Abs. 3 DSGVO geschlossen, der die Einhaltung Ihrer Rechte bei der Verarbeitung personenbezogener Daten gewährleistet.
Einwilligungsverwaltung
Im Rahmen der Einwilligungsverwaltung überprüfen wir, welche Inhalte von Kooperationspartnern (insb. Werbeunternehmen und soziale Netzwerke) wir ausspielen dürfen, gewährleisten wir die Funktionssicherung von standardmäßig deaktivierten Integrationen und realisieren wir in diesem Zusammenhang die Einholung und Verwaltung Ihrer datenschutzrechtlichen Einwilligungen. Wir verarbeiten Datum und Uhrzeit Ihres Besuchs, Geräte- und Browserinformationen, Ihre anonymisierte IP-Adresse, Ihr Einwilligungsprofil und eine zufallsgenerierte Identifikationsnummer zur Zuordnung des Endgeräts zum Einwilligungsprofil. Dafür setzt die Consent Management Plattform (CMP) ein technisch erforderliches Cookie, um den Consent-Status abfragen und damit entsprechende Inhalte ausspielen zu können. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. c iVm. Art. 7 Abs. 1 DSGVO, § 25 Abs. 2 Nr. 2 Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz (TDDDG). Wir arbeiten für diese Zwecke mit dem Anbieter »Sourcepoint« (Sourcepoint Technologies, Inc., 228 Park Avenue South, #87903, New York, NY 10003-1502, USA) zusammen, mit dem wir einen Auftragsverarbeitungsvertrag gem. Art. 28 Abs. 3 DSGVO geschlossen haben. Da in diesem Zusammenhang eine Drittlandsübermittlung stattfindet, haben wir die Standardvertragsklauseln der EU-Kommission abgeschlossen, um sicherzustellen, dass die Verarbeitung Ihrer personenbezogenen Daten mit der DSGVO vereinbar ist (Art. 46 Abs. 2 lit. c DSGVO).
Technische Funktions- und Zugangssicherung / Authentifizierung
Für die Authentifizierung verarbeiten wir Ihre IP-Adresse und Metadaten in Zusammenhang mit dem Setzen dafür erforderlicher Cookies. Die Authentifizierung beinhaltet Informationen, die es uns ermöglichen, Endgeräte unabhängig von Nutzerkonten zu identifizieren und zu überprüfen, ob für diese eine Einwilligung zur kostenlosen Nutzung mit Werbung und Tracking vorliegt oder nicht. Auf Grundlage dessen wird die Ausspielung unserer Webseite entsprechend gestaltet. Die Rechtsgrundlage ist unser berechtigtes Interesse an einer möglichst individuellen Ausspielung unseres Angebots, Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG.
Schutz vor automatisiertem Missbrauch
Um unsere Webseite und insbesondere kritische Endpunkte vor automatisiertem Missbrauch (z.B. massenhafte Anfragen, Spam oder DDoS-Attacken) zu schützen, prüfen wir, ob Zugriffe und Eingaben von natürlichen Personen oder automatisierten Programmen (Bots) stammen. Für diesen Zweck setzen wir zwei Dienste ein: Den Dienst der Link11 GmbH (Lindleystr. 12, 60314 Frankfurt am Main, Deutschland) und den Dienst "reCAPTCHA" der Google Ireland Limited (Gordon House, 4 Barrow St, Dublin, D04 E5W5, Irland). Link11 verfolgt den Schutz vor automatisiertem Missbrauch insbesondere durch eine netzwerkbasierte Analyse des Datenverkehrs und die Verwendung eines Schutz-Cookies, das typische Muster menschlicher Nutzung von automatisierten Zugriffen unterscheidet. Zu diesem Zweck werden folgende personenbezogene Daten verarbeitet: Cookie-Daten (IP-Adresse, Zeitstempel, Datum des Zugriffs, Referrer-URL, Browser, technische Informationen über das verwendete Endgerät), sowie browserbezogene Interaktionsdaten (Eingaben, Scrollen, Touch/Zoom, Geräteausrichtung). Mittels des Dienstes reCAPTCHA hingegen wird das Nutzerverhalten direkt im Browser geprüft. Dafür verarbeiten wir die IP-Adresse des verwendeten Endgeräts, Erkennungsdaten des verwendeten Browser- und Betriebssystem-Typ sowie Datum und Dauer des Besuchs mittels eines LocalStorage-Eintrags. Die Verarbeitung ist erforderlich, um die Sicherheit, Verfügbarkeit und Integrität unserer Webseite zu gewährleisten, automatisierte Schadzugriffe zu blockieren und Missbrauch sowie Spam zu verhindern. Rechtsgrundlage ist unser berechtigtes Interesse an der Gewährleistung der Sicherheit unserer Systeme, der Feststellung einer individuellen Eigenverantwortung im Internet sowie der Vermeidung von Missbrauch und Spam, Art. 6 Abs. 1 S. 1 lit. f DSGVO; hinsichtlich des Einsatzes technisch erforderlicher Cookies oder ähnlicher Technologien greift zudem § 25 Abs. 2 Nr. 2 TDDDG. Mit den genannten Dienstleistern wurden Verträge gem. Art. 28 Abs. 2 DSGVO geschlossen. Mögliche Drittlandsübermittlungen im Zusammenhang mit reCAPTCHA, insbesondere an Konzernunternehmen von Google, basieren auf den Standardvertragsklauseln der EU-Kommission, welche die Einhaltung des europäischen Datenschutzniveaus sicherstellen sollen (Art. 46 Abs. 2 lit. c DSGVO). Wenn Sie den LocalStorage-Eintrag oder das Cookie löschen möchten, können Sie dies über die Einstellungen des von Ihnen verwendeten Browsers tun ("Browserdaten / Cookies & Websitedaten löschen").
Analysen zu funktionalen Zwecken: Basistracking - Reichweitenmessung
Wir erfassen und analysieren Nutzungsdaten zur Ermittlung statistischer Kennwerte über die Nutzung unseres Digital-Angebots, um die Anzahl der Besuche auf unserer Webseite, die Anzahl der Webseitenbesucher und deren Surfverhalten auf Basis eines einheitlichen Standardverfahrens zu bestimmen und somit marktweit vergleichbare Werte zu erhalten. Wir verarbeiten die Nutzungsdaten zur Sicherstellung des Betriebs, für die Bewertung der Relevanz von Inhalten bei der redaktionellen Arbeit, um die Nutzung unserer Webseiten und Angebote zu untersuchen, um einzelne Funktionen und Angebote sowie das Nutzungserlebnis fortlaufend optimieren zu können und zur Identifikation fehlerhafter Nutzungspfade. Im Rahmen dessen verarbeiten wir ebenso wie für das Adobe-basierte Werbe-Tracking personenbeziehbare Merkmale über den sogenannten »Data Feed«, der browserbezogene [die mit einer Cookie-ID vergleichbare Experience Cloud Visitor-ID (EC-ID), mit deren Hilfe der Browser Ihres Endgeräts identifiziert werden kann] und nutzerkontobezogene (SSO-ID) Identifizierungsmerkmale enthält, über die Surfverhalten und Navigation auf unserer Seite nachvollziehbar werden. Wir bezeichnen diese Art der Möglichkeit, das Nutzerverhalten nachzuvollziehen nachfolgend als »Basistracking«. Das Basistracking erfolgt ebenso wie das Werbe-Tracking pseudonymisiert, das heißt, wir können aus den Informationen, die wir erhalten, keinen unmittelbaren Rückschluss auf Sie als Person ziehen, es sei denn, Sie sind mit einem Nutzerkonto bei uns registriert und angemeldet und haben im Rahmen der Registrierung in die Verknüpfung eingewilligt. In diesem Fall erfassen wir die Nutzungsdaten auch in einem geräteübergreifenden Profil. Diese Profilidentifizierung ermöglicht es uns, das Verhaltens- und Leseprofil des geräteübergreifend verwendeten Login-Kontos ganzheitlich zu erfassen und die Nutzererfahrung über alle Endgeräte hinweg zu verbinden. Die Rechtsgrundlage ist unser berechtigtes Interesse an der Optimierung der Nutzererfahrung und der technischen Stabilität der Webseite, gem. Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG. Für die Verarbeitung der Informationen im Rahmen des Basistracking arbeiten wir mit dem Anbieter »Adobe« (Adobe Systems Software Ireland Limited, 4-6 Riverwalk, Citywest Business Campus, Saggart, Dublin 24, Ireland) zusammen, mit dem wir für diese Zwecke einen Vertrag über die Auftragsdatenverarbeitung gem. Art. 28 Abs. 3 DSGVO geschlossen haben. Nähere Informationen erhalten Sie in unserem Privacy-Center.
Die Unterbindung erfolgt über das Setzen eines Cookies auf dem von Ihnen verwendeten Endgerät. Wenn Sie das Cookie löschen, ein anderes Endgerät oder einen anderen Webbrowser verwenden, muss das Opt-out erneut eingerichtet werden. Hinsichtlich unserer mobilen Apps muss die Unterbindung in den jeweiligen App-Einstellungen vorgenommen werden.
B) Redaktionelle und produktspezifische Verwendungszwecke
Wir verarbeiten personenbeziehbare Daten auch zu besonderen redaktionellen und / oder produktspezifischen Zwecken. Diese nachfolgend genannten Verarbeitungen sind zwingend für die Bereitstellung unserer Produkte erforderlich, auch bei der Wahl der "Werbefrei-lesen-Option".
Bedarfsgerechte Ausspielung von und Zugriff zu redaktionellen Inhalten
Teilweise verarbeiten wir Ihre IP-Adresse und die Kommunikation beschreibende Metadaten für die Gewährleistung einer zeitlich stets aktuellen Ausspielung von unterschiedlichen Inhalten auf unserer Webseite. Dabei kann es sich um Sport-, Wahlergebnisse, um redaktionelle Infografiken, Statistiken und interaktive Diagramme in unseren redaktionellen Inhalten oder andere Informationen handeln, bei denen wir davon ausgehen, dass Sie eine laufende Ergebnisaktualisierung im Sinne des Informiert-bleibens wünschen. Wir stellen Ihnen auch zeitweise Funktionen zur Bereitstellung und Durchführung anfragebasierter Recherchen zur Verfügung und integrieren Mechanismen, um automatisierte Requests zu erkennen und zu verhindern. Solche Datenverarbeitungen basieren insofern auf unserem berechtigten Interesse, als dass wir unseren Nutzer:innen stets zeitlich aktuelle Informationen zur Verfügung stellen möchten. Sie sind für diesen Zweck erforderlich. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. f DSGVO. Wir arbeiten ggf. mit externen Dienstleistern zusammen, mit denen wir Auftragsverarbeitungsverträge schließen, die den Anforderungen von Art. 28 Abs. 3 DSGVO entsprechen.
Ausspielung von redaktionellen Inhalten im Story Format
Für die Ausspielung von bestimmten Videoformaten und zur Erfüllung des Bedarfs nach Kurzinformationen unserer Leserschaft verarbeiten wir Ihre IP-Adresse. Die Datenverarbeitung dient der Herstellung einer Verbindung zum Server und gewährleistet die technische Funktionsfähigkeit. Über die sogenannte EC-ID (eine einem Cookie ähnliche individuelle Kennzeichnung Ihres Browsers im Rahmen der Adobe Integration) erfassen wir darüber hinaus Aufrufe unserer redaktionellen Stories in aggregierter Form. Die Datenverarbeitungen sind für die Gewährleistung der Funktionsfähigkeit erforderlich. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG. Mit dem Dienstleister »Storifyme« (Storifyme GmbH, Blutenburgstraße 68, 80636 München, Deutschland), den wir für diese Zwecke einsetzen, haben wir einen Auftragsverarbeitungsvertrag nach Art. 28 Abs. 3 DSGVO geschlossen.
Ausspielung von Livestreams und Videos
Zum Ausspielen von Videos und Livestreams zur Ergänzung redaktioneller Inhalte auf unseren Webseiten, zum Vorschlagen weiterer Videos, der Erstellung von Reports über Videoaktivitäten, die Erhöhung der Qualität unserer Videos, für Analysen zur Erkennung von Nutzertrends in diesem Kontext und das Erkennen von technischen Problemen, sowie zur Aufrechterhaltung und Verbesserung unserer Angebote verarbeiten wir statistische Nutzungsdaten (Ihre IP-Adresse, die Local ID, Viewer ID, Gerätetyp, Browsertyp und -version, den Zeitpunkte der Nutzung und die Nutzungsdauer). Die Rechtsgrundlage ist unser berechtigtes Interesse an einer umfangreichen Gestaltung unseres redaktionellen Angebots, Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG. Wir setzen für diese Zwecke den Dienst »JW Player« (Longtail Ad Solutions, 8 West 38th Street, 6th Floor, NY 10018 New York, USA, https://jwplayer.com/legal/privacy/ ) ein, mit welchem wir einen Auftragsverarbeitungsvertrag geschlossen haben, der die Anforderungen von Art. 28 Abs. 3. und Art. 44 ff. DSGVO erfüllt.
Podcasts
Soweit Sie die Möglichkeit haben, Podcasts auf unseren Webseiten zu hören, verarbeiten wir für die Ausspielung über das Setzen eines Cookies Ihre IP-Adresse, Typ und Version Ihres Internet-Browsers, verwendetes Betriebssystem, aufgerufene Seite, Referrer-URL, Uhrzeit der Serveranfrage, Verweildauer auf der Webseite und die Häufigkeit des Aufrufs. Die Rechtsgrundlage ist ebenfalls unser berechtigtes Interesse an einer breiten und umfassenden Gestaltung unseres redaktionellen Angebots, das den Bedürfnissen unserer Nutzer:innen entspricht, Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG. Wir arbeiten für diesen Zweck mit dem Anbieter »Omnystudio« (Triton Digital Canada Inc., 1440 Sainte Catherine West Street, Suite 1200, Quebec, H3G 1R8 Montreal, Canada) zusammen, mit dem wir einen Vertrag nach Art. 28 Abs. 3 DSGVO über die Auftragsdatenverarbeitung geschlossen haben, der die Einhaltung Ihrer Rechte gewährleistet. Für Kanada existiert ein Angemessenheitsbeschluss der EU-Kommission, der die Datenübermittlung in ein Drittland nach Art. 45 Abs. 1 DSGVO rechtfertigt.
Interaktive Grafiken
Zur Einbettung und Darstellung interaktiver Grafiken auf unseren Webseiten verarbeiten wir Ihre IP-Adresse. Die IP-Adresse wird dabei unter Auswahl eines geeigneten Servers durch ein sogenanntes, als Hoster fungierendes Content Delivery Network (CDN) übertragen, um den Aufruf der veröffentlichten Grafik zu ermöglichen und an Sie als Nutzer:in weiterzuleiten. Die Verarbeitung ist für die Visualisierung und Veröffentlichung von Grafiken mit redaktionellen Inhalten erforderlich. Die Rechtsgrundlage ist unser berechtigtes Interesse gem. Art. 6 Abs. 1 S. 1 lit. f DSGVO. Zur Ermöglichung dieser technischen Gestaltung arbeiten wir mit »Datawrapper« (Datawrapper GmbH, Raumerstraße 39, 10437 Berlin, Deutschland) zusammen, mit denen wir einen Auftragsverarbeitungsvertrag gem. Art. 28 Abs. 3 DSGVO geschlossen haben. Sofern in diesem Zusammenhang Daten in einem Land außerhalb der Europäischen Union verarbeitet, werden stellt Datawrapper sicher, dass die Anforderungen der Art. 44 ff DSGVO erfüllt werden.
Live-Blog
Um Ihnen einen Live Ticker auf unseren Webseiten zur Verfügung zu stellen und Sie jederzeit mit aktuellen Nachrichten versorgen zu können, verarbeiten wir Ihre IP-Adresse, Datum und Uhrzeit sowie aufgerufene URL. Die Verarbeitung ist erforderlich zum Schutz der Infrastruktur und zur Erstellung anonymisierter Aufrufstatistiken. Die Zuordnung eines Aufrufs zu einzelnen Nutzer:innen ist nicht möglich. Die Rechtsgrundlage ist unser berechtigtes Interesse nach Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG. Wir arbeiten zu diesem Zweck mit »tickaroo« (Tickaroo GmbH, Waffnergasse 8, 93047 Regensburg, Deutschland, www.tickaroo.com ) zusammen, mit denen wir einen Auftragsverarbeitungsvertrag nach Art. 28 Abs. 3 DSGVO geschlossen haben.
Musik-Vertriebs-Analyse-Tool
Für die Nutzung unserer Podcasts unterhalten wir auf der Plattform Linkfire speziell eingerichtete Webseiten (sog. Landing-Pages). Auf diesen können wir Ihnen verschiedene Online-Musikdienste übersichtlich in jeweils einem Link anzeigen und auf unsere Podcasts weiterleiten. Wir analysieren das Nutzungsverhalten unserer Podcast-Hörer:innen, um unser redaktionelles Angebot laufend anhand der Bedürfnisse unserer Nutzer:innen aktualisieren zu können. Wir verarbeiten zu diesen Zwecken Metadaten wie bspw. Ihre IP-Adresse über ein Cookie, um Ihren Browser zu identifizieren. Die Analysen erfolgen anonymisiert, d.h. uns sind Rückschlüsse auf Sie als Einzelperson nicht möglich. Die Rechtsgrundlage ist unser berechtigtes Interesse an der nutzergerechten Zurverfügungstellung redaktioneller Inhalte gem. Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG.
Wir nutzen für die genannten Zwecke das Musik-Vertriebs-Analyse-Tool des Anbieters »Linkfire« (Linkfire A/S, Artillerivej 86 3th, Kopenhagen 2300 Dänemark), mit welche wir eine datenschutzrechtliche Auftragsverarbeitungsvereinbarung nach Art. 28 Abs. 3 DSGVO geschlossen haben.
Bereich
5. Vertragsbeziehungen
aufklappen
A) Nutzerkonto
Registrierung und "Angemeldet bleiben"-Funktion
Wir bieten Ihnen auf unseren Webseiten unter Angabe Ihrer E-Mail-Adresse und Vergabe eines Passworts die Möglichkeit, ein kostenloses Nutzerkonto anzulegen, das die Grundlage für die Nutzung einiger registrierungspflichtiger Dienste (digitale Zeitzugangsverträge, Abos, Bestellen von Produkten, Teilnahme an wiederkehrenden Quizzen mit Zugriff auf eine persönliche Spielestatistik und Beteiligung an unseren Debattenformaten) darstellt und Ihnen die Inanspruchnahme von personalisierten Leistungen ermöglicht. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO. Weitere Angaben wie Name, Adresse, Geburtsdatum und Telefonnummer sind optional und basieren insofern auf Ihrer freiwilligen Einwilligung (Art. 6 Abs. 1 S. 1 lit. a DSGVO).
Wenn Sie sich als Nutzer:in für ein Konto registrieren, verarbeiten wir in Zusammenhang mit Ihrer Anmeldung Ihre IP-Adresse, sowie Datum und Uhrzeit der Registrierung. Sie erhalten anschließend eine E-Mail von uns mit einem Link, über den Sie das Anlegen Ihres Nutzerkontos bestätigen können. Die Datenverarbeitung ist für die Registrierung erforderlich und beruht auf Art. 6 Abs. 1 S. 1 lit. b DSGVO.
In Zusammenhang mit Ihrer Registrierung haben Sie die Möglichkeit, der Verknüpfung der Informationen aus Ihrem Nutzerkonto mit den im Rahmen des Basistracking erhobenen Daten zuzustimmen. Die Datenverknüpfung auf Grundlage der Profilidentifizierung ermöglicht es uns, das Verhaltens- und Leseprofil des geräteübergreifend verwendeten Login-Nutzerkontos ganzheitlich zu erfassen und die Nutzungserfahrung über alle Endgeräte hinweg zu verbinden. Die Rechtsgrundlage ist Ihre Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO.
Sie haben die Möglichkeit, die »Angemeldet bleiben«-Funktion zu nutzen, damit Sie sich nach Beendigung einer Sitzung und bei einem späteren Wiederbesuch nicht von Neuem anmelden müssen. Dafür setzen wir ein Cookie im Browser Ihres Endgerätes, das für eine automatische Wiedererkennung bei einem neuen Besuch sorgt. Die Rechtsgrundlagen sind Ihre freiwillig erklärte Einwilligung nach Art. 6 Abs. 1 S. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG.
Leserpriorisierung
Für die Optimierung unseres Kundenservice verwenden wir die Informationen über von Ihnen geschlossene digitale Verträge für die interne Organisation der Bearbeitung von Kundenanfragen (Leserpriorisierung). Die Leserpriorisierung erfolgt in unserem berechtigten Interesse, Kundenanliegen interessengerecht zu kategorisieren und zu bearbeiten. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. f DSGVO.
Persönliche Zugangssicherung, Missbrauchsprävention und -aufklärung
Unsere digitalen Angebote und insbesondere Abonnements dürfen nur entsprechend der jeweils geltenden Nutzungsbedingungen und vertraglichen Vereinbarungen genutzt werden. Für Zwecke der Missbrauchsprävention im Rahmen der persönlichen Zugangssicherung zu unseren digitalen Angeboten verarbeiten wir Informationen über die durch Ihren Account erfolgte Nutzung, wie identifizierende Merkmale im Rahmen des Basistracking (EC-ID, SSO-ID), die Anzahl von Nutzeraufrufen (Unique Users), Besuche, Seitenaufrufe, verwendete Browsertypen, genutzte Endgeräte, Zahlungsinformationen, sowie die Namens- und Kontaktdaten aus Ihrem Abonnement. Die Datenverarbeitung ist für die Analyse von missbrauchsauffälligen Nutzeraccounts erforderlich. Für die Aufklärung von missbräuchlichem Verhalten verarbeiten wir außerdem eine Sie identifizierende ID (Backoffice-ID), um bei PDF-Downloads von Artikeln und deren widerrechtlicher Veröffentlichung Rückschlüsse auf den Verursacher ziehen zu können. Die Veröffentlichung von geschützten Inhalten widerspricht unseren Nutzungsbedingungen und macht die Datenverarbeitung erforderlich. Die Verarbeitungen für Zwecke der Missbrauchsprävention und der Aufklärung von missbräuchlichem Verhalten erfolgt auf Basis des zwischen Ihnen und uns geschlossenen Vertrages, die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO. Der Einsatz von Cookies und ähnlichen Technologien ist für diese Zwecke ohne Ihre Einwilligung möglich nach § 25 Abs. 2 Nr. 2 TDDDG. Für einige Zwecke in diesem Zusammenhang setzen wir den Dienstleister »frisbii« [Frisbii Media GmbH, Königstr. 4, 87435 Kempten, Deutschland, www.frisbii.com ] ein, mit dem wir zu diesem Zweck einen Auftragsverarbeitungsvertrag geschlossen haben, der gewährleistet, dass die Datenverarbeitung im Einklang mit den gesetzlichen Anforderungen erfolgt.
Ihre Daten werden für die Dauer der bestehenden Registrierung gespeichert und erst dann gelöscht, wenn Sie Ihr Nutzerkonto schließen, sofern nicht gesetzliche Aufbewahrungspflichten einer Löschung entgegenstehen. Die Löschung können Sie selbstständig in Ihrem Nutzerkonto anstoßen.
B) Abo
Abschluss von Abonnementverträgen
Für digitale Verträge wie Abonnementverträge (periodisch aktualisiertes Angebot digitaler Angebote), Zeitzugangsverträge (z.B. Wochen- bzw. Monats-, Mehrmonats- oder Jahrespass für vorhandene kostenpflichtige Inhalte) und für die Buchung der Werbefrei-lesen-Option benötigen Sie ein Nutzerkonto. Über die Datenverarbeitungen in Zusammenhang mit Ihrer Registrierung hinaus (siehe Ziffer 5.A.), verarbeiten wir für den Abschluss und die Verwaltung (digitaler) Verträge, sowie die in unseren spezifischen Lieferbedingungen jeweils genannten Zwecke Ihren vollständigen Namen, die Anrede, den Titel, ggf. die Information über ein bestehendes Print-Abonnement, Zahlungsdaten, Ihre Anschrift (Privat- oder Firmenadresse), sowie das Nutzungsland (Abodaten). Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO. Weitere Angaben wie Namensergänzung und Telefonnummer sind optional und basieren insofern auf Ihrer freiwilligen Einwilligung (Art. 6 Abs. 1 S. 1 lit. a DSGVO). Sie haben ggf. die Möglichkeit, über die Funktion "Artikel verschenken" bis zu 10 Artikel, die von dieser Möglichkeit umfasst sind, an Interessierte kostenlosweiterzugeben. Wir verarbeiten die Anzahl der verschenkten Artikel, um die Begrenzung der Anzahl zu gewährleisten. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO.
Abonnementspezifische Verarbeitungen beim Probe-Abo
Bei Abschluss eines Probe-Abonnements behalten wir uns vor, die Voraussetzungen für die Inanspruchnahme des Rabattgrundes anhand Ihrer E-Mail-Adresse zu überprüfen. Zu diesem Zweck speichern wir einen pseudonymisierten Hash-Wert Ihrer E-Mail-Adresse für 12 Monate. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO.
Abonnementspezifische Verarbeitungen beim U-30-Abo
Für die erforderlichenfalls gebotene Altersverifikation im Rahmen des U-30-Abos verarbeiten wir eine 7-stellige Prüfnummer Ihres Personalausweises oder Reisepasses, aus dem sich Ihr Geburtsdatum und damit der Nachweis für das Bestehen des Rabattgrundes aufgrund Ihres Alters ergibt. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO. Für die Altersverifikation setzen wir den Dienstleister »frisbii« [Frisbii Media GmbH, Königstr. 4, 87435 Kempten, Deutschland, www.frisbii.com ] ein, mit dem wir zu diesem Zweck einen Auftragsverarbeitungsvertrag geschlossen haben, der gewährleistet, dass die Datenverarbeitung im Einklang mit den gesetzlichen Anforderungen erfolgt. Die Prüfnummer wird nach Überprüfung Ihres Alters direkt gelöscht. In unserem Kundenverwaltungssystem wird lediglich das Ablaufdatum des Rabattgrundes hinterlegt, um Ihr Abo mit Ihrem 30. Geburtstag ohne Rabattierung fortzuführen.
Abonnementspezifische Verarbeitungen beim Duo-Abo
Wenn Sie ein Duo-Abo abschließen, können Sie unter Einhaltung der Voraussetzungen der »Allgemeinen Geschäfts- und Lieferbedingungen« (SPIEGEL, manager magazin, 11Freunde) als Hauptnutzer eine weitere Person einladen, das Abo zu nutzen. Wir verarbeiten für diese Zwecke den Namen und die E-Mail-Adresse des Zweitnutzers, der eine Einladung zur Mitnutzung per E-Mail erhält und mit dem übermittelten Einladungscode seinen Zugang aktivieren kann. Der Mitnutzende muss zur Validierung der Abovoraussetzungen die Adresse des Hauptnutzers als seine eigene bestätigen. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO.
Abonnementspezifische Verarbeitungen beim Starter-Abo
Sofern Sie unser Starter-Abo nutzen, verwenden wir über die Abodaten hinaus Teile der im Rahmen unseres Basistracking erhobenen Daten (die sog. SSO-ID), um die Einhaltung der Abo-Voraussetzungen, insbesondere die Inanspruchnahme der Freischaltungen und die Zurverfügungstellung der in Anspruch genommenen Artikel, gewährleisten zu können. Die Rechtsgrundlage ist der zwischen Ihnen und uns bestehende Vertrag (Art. 6 Abs. 1 S. 1 lit. b DSGVO).
Abonnementspezifische Verarbeitungen - Vergünstigungen
Im Rahmen Ihres bestehenden digitalen Abonnementvertrages stellen wir Ihnen unter Umständen Rabattcodes für den Abschluss vergünstigter Abonnements bei anderen Verlags- und Medienhäusern, mit denen wir kooperieren (»alles.plus«), oder anderer Preisnachlässe, zur Verfügung. Wir verarbeiten im Rahmen der »alles.plus«-Kooperation Ihre Kundennummer für den wiederkehrenden Abgleich der bestehenden Kundenbeziehung als Voraussetzung für die Rabatte und einmalig auch Ihre E-Mail-Adresse für die Zusendung eines individuellen Rabattcodes auf Ihre explizite Anfrage hin. Dieses Angebot ist Bestandteil unserer vertraglichen Leistungen Ihnen gegenüber, die Rechtsgrundlage für die Datenverarbeitung ist daher Art. 6 Abs. 1 S. 1 lit. b DSGVO. Für die technische Abwicklung solcher rabattierten Kombi-Abo-Angebote arbeiten wir mit dem Verein »alles.plus« (alles.plus e.V., c/o DER SPIEGEL GmbH & Co. KG, Ericusspitze 1, 20457 Hamburg, Deutschland) zusammen, mit dem wir einen Vertrag über die Auftragsdatenverarbeitung geschlossen haben. Gewähren wir Ihnen an anderer Stelle Rabatte, verarbeiten wir für diesen Zweck Ihre Abonnementnummer für die Überprüfung der Voraussetzungen des Preisvorteils. Die Rechtsgrundlage ist ebenfalls Art. 6 Abs. 1 S. 1 lit. b DSGVO.
Abrechnung / Zahlungstransaktionen
Im Rahmen der Erfüllung von digitalen Verträgen setzen wir externe Zahlungsdienstleister ein, über deren Plattformen wir und Sie Zahlungstransaktionen vornehmen können (z.B. Paypal, Kreditkarten wie Visa, Mastercard, American Express, SEPA-Lastschriftverfahren). Zu den durch die Zahlungsdienstleister verarbeiteten Daten gehören: Name, Adresse, Bankdaten, sowie Transaktionsdaten wie vertrags- und empfängerbezogene Angaben. Die Angaben sind erforderlich, um die Transaktionen durchzuführen, Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO. Die genannten Daten werden ausschließlich durch den jeweiligen Zahlungsdienstleister verarbeitet und bei diesem gespeichert, wir erhalten lediglich Informationen über Bestätigung oder Nichtvollzug der Zahlung. Unter Umständen werden die Daten seitens der Zahlungsdienstleister an Wirtschaftsauskunfteien übermittelt. Diese Übermittlung bezweckt die Identitäts- und Bonitätsprüfung. Hierzu verweisen wir auf die AGB und Datenschutzhinweise der jeweiligen Zahlungsdienstleister.
Prämien
In bestimmten Fällen wie etwa der Werbung neuer Leser:innen können wir unseren Abonnent:innen mit Prämien danken. Wir verarbeiten für diese Zwecke Ihre Kontaktdaten (bei Geldprämien auch Ihre Bankdaten), um Ihnen die jeweilige Zugabe zukommen zu lassen. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO. Wir arbeiten für diese Zwecke mit unterschiedlichen Dienstleistern zusammen, die bspw. für den Versand beauftragt werden. Wir haben mit den Dienstleistern Verträge geschlossen, welche die Einhaltung der datenschutzrechtlichen Vorgaben gewährleisten.
Wir speichern Ihre Daten stets bis zum Ende Ihres digitalen Vertrages. Bitte beachten Sie, dass eine Löschung Ihres Nutzerkontos nicht mit einer Kündigung Ihres digitalen Vertrages gleichzusetzen ist. Ggf. ist die Aufbewahrung Ihrer Daten erforderlich, um Voraussetzungen zur Begründung eines digitalen Vertrages zu überprüfen, bspw. zur Verhinderung einer wiederholten Bestellung zum Bezugsstart vergünstigter digitaler oder Prämien-Abonnements. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO. Darüber hinaus sind wir nach Art. 6 Abs. 1 S. 1 lit. c DSGVO verpflichtet, Daten aufgrund von steuer-, handelsrechtlichen Aufbewahrungspflichten vorzuhalten. Sofern die Daten nicht gelöscht werden, weil Ihre Aufbewahrung für andere vertraglich oder gesetzlich zulässige Zwecke erforderlich ist, wird ihre Verarbeitung eingeschränkt (besonders zugriffsgeschützte Archivierung) und mit angemessenen technischen und organisatorischen Maßnahmen nur den für Ihre Vorhaltung Berechtigten verfügbar gemacht.
C) Eigene personalisierte redaktionelle Inhalts- und Artikelempfehlungen
Wir möchten Ihnen im Rahmen der personalisierten redaktionellen Inhaltsempfehlungen und -reviews die Möglichkeit bieten, Ihre Nutzung unseres Angebotes analysieren zu lassen und an ausgewählten Stellen redaktionelle Inhalte auf Basis Ihres Leseverhaltens vorzuschlagen. Dafür verwenden wir die im Rahmen unseres Basistracking erhobenen Daten, über die Surfverhalten und Navigation auf unserer Seite nachvollziehbar werden. Die Rechtsgrundlage ist Ihre freiwillig erklärte Einwilligung (Art. 6 Abs. 1 S. 1 lit. a DSGVO). Für die Verarbeitung der Informationen im Rahmen des Basistracking arbeiten wir, wie oben näher erläutert, mit dem Anbieter Adobe zusammen. Nähere Informationen erhalten Sie in unserem Privacy-Center.
Zur Durchführung von Datenanalysen und zur Weiterentwicklung unseres Empfehlungssystems verarbeitet die »TU Wien« [Technische Universität Wien, Karlsplatz 13, 1040 Wien] Ihre Daten aus dem Basistracking in pseudonymisierter Form. Das bedeutet, dass aus den Datensätzen vor der Weitergabe an die TU Wien durch uns die Sie identifizierenden Merkmale herausgelöst und gesondert gespeichert werden, wodurch der TU Wien die Herstellung eines Personenbezugs nicht möglich ist. Die Datensätze werden insofern innerhalb des Handlungsbereiches der TU Wien in anonymisierter Form verarbeitet, die Erkenntnisse kommen Ihnen und uns über die Möglichkeit der Zusammenführung in unserem Verantwortungsbereich zugute.
D) Marketing (Werbung per E-Mail, Telefon, Brief)
Wir verarbeiten Ihre personenbezogenen Daten auch für Marketingzwecke. Dies umfasst insbesondere die folgenden Aktivitäten:
E-Mail-Marketing: Wir verwenden Ihre Kontaktdaten für Werbung, wenn Sie dazu eingewilligt haben (Art. 6 Abs. 1 S. 1 lit. a DSGVO), sowie zur gesetzlich zulässigen Direktwerbung für eigene und verwandte Produkte im Rahmen der bestehenden Vertragsbeziehung, wenn Sie diese bei Ihrer Bestellung oder Registrierung angegeben haben (§ 7 Abs. 3 UWG als Umsetzung von Art. 13 Abs. 2 RL 2002/58/EG (ePrivacy-RL) und Art. 95 DSGVO).
Telefonmarketing: Wenn Sie uns Ihre Telefonnummer mitgeteilt und Ihre Einwilligung erteilt haben, können wir Sie telefonisch zu Werbezwecken kontaktieren. Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 S. 1 lit. a DSGVO.
Postalische Werbung: Wir nutzen Ihre Postanschrift, um Ihnen Werbematerialien zuzusenden. Dies geschieht auf Basis unseres berechtigten Interesses gemäß Art. 6 Abs. 1 S. 1 lit. f DSGVO, sofern Sie dem nicht widersprochen haben.
Sofern Sie keine Werbung mehr wünschen, können Sie Ihre Einwilligung jederzeit widerrufen oder der Direktwerbung widersprechen
durch einen Klick auf den Abmelde-Link am Ende der E-Mail
per E-Mail an:aboservice@spiegel.de  bzw. aboservice@manager-magazin.de  oder kundenservice@11freunde.de 
schriftlich an: Der SPIEGEL, Kundenservice, 20637 Hamburg (bitte Ihren Namen und die E-Mail-Adresse der Registrierung angeben) bzw. Manager Magazin, Kundenservice, 20637 Hamburg oder 11 Freunde Kundenservice, Postfach 111828, 20418 Hamburg
telefonisch unter 040 3007-2700 (SPIEGEL) bzw. 040 3007-3400 (manager magazin) oder 040 3007-3030 (11FREUNDE)
Zur Erbringung der Werbemaßnahmen setzen wir Dienstleister ein, mit denen wir für diese Zwecke Auftragsverarbeitungsverträge nach Art. 28 Abs. 3 DSGVO geschlossen haben. Die folgenden Auftragsverarbeiter setzen wir ein:
E-Mail-Marketing: »Salesforce«
salesforce.com Germany GmbH, Erika-Mann-Straße 31, 80636 München, Deutschland
Postalische Werbung: »Global TDA«
Global TDA Group Germany GmbH, Serviceware-Kreisel 1, 65510 Idstein, Deutschland
Die von uns verarbeiteten Daten werden gelöscht, sobald sie für ihre Zweckbestimmung nicht mehr erforderlich sind und der Löschung keine gesetzlichen Aufbewahrungspflichten entgegenstehen.
E) Newsletter
Sie haben auf unseren Webseiten die Möglichkeit, unsere redaktionellen Newsletter zu abonnieren, um Nachrichten und aktuelle Informationen aus unseren Redaktionen und zu unseren Produkten zu erhalten. Wir verarbeiten für diese Zwecke Ihre E-Mail-Adresse, optional auch Ihren Namen. Die Verarbeitung basiert auf Ihrer freiwilligen Einwilligung nach Art. 6 Abs. 1 S. 1 lit. a DSGVO.
Wir erfassen Ihre Klicks in unseren Newslettern und sonstigen Benachrichtigungen mithilfe unsichtbarer Bilddateien, sog. Tracking-Pixel. Auf dieser Basis analysieren wir die Öffnungs- und Klickraten sowie andere Nutzungsdaten. Ihre E-Mail-Adresse wird dabei in einer Empfängerliste gespeichert, um den Verteilerkreis nachhalten zu können. Pseudonymisiert wird sie auch dazu benutzt, Ihnen personalisierte Newsletter zuzuschicken, die auf Ihre Vorlieben und Interessen angepasst sind. Die Rechtsgrundlage ist ebenfalls Ihre freiwillig erklärte Einwilligung nach Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG.
Wir arbeiten zu den oben genannten Zwecken mit den Dienstleistern »Mailjet« (Mailjet GmbH, Alt-Moabit 2, 10557 Berlin, Deutschland) und »Salesforce« (salesforce.com Germany GmbH, Erika-Mann-Straße 31, 80636 München, Deutschland) zusammen, mit denen wir Auftragsverarbeitungsverträge nach Art. 28 Abs. 3 DSGVO geschlossen haben.
Sie können sich jederzeit von jedem Newsletter abmelden und Ihre Einwilligung in den Versand und die Auswertung widerrufen, indem Sie am Ende des jeweiligen Newsletters auf den entsprechenden Link klicken, sich unternewsletter@spiegel.de  an uns wenden, oder selbstständig in Ihrem "Mein Konto”-Bereich den jeweiligen Newsletter abbestellen. Dies gilt auch für die redaktionellen Newsletter, die als Bestandteil von Abonnements und Zeitzugangsverträgen bestellt wurden.
F) Umfragen
Wir bieten Ihnen von Zeit zu Zeit die Möglichkeit, an Umfragen teilzunehmen. Themenschwerpunkte dieser Umfragen sind insbesondere Fragen zu unseren Produkten, sowie zu visueller Haptik und zum Design unseres Web- und Appauftritts. Mithilfe der Umfrageergebnisse analysieren und verbessern wir unser Angebot und unsere Produkte ganzheitlich. Im Rahmen von Umfragen, die explizit das App- und Webdesign in den Vordergrund stellen, verarbeiten wir technische Informationen wie Browserversion, Betriebssystem und IP-Adresse, sowie Ihre Antworten und Kommentare (»Umfragedaten«). Alle anderen Umfragen können grundsätzlich ohne Verarbeitung personenbezogener Daten durchgeführt werden, solange Sie unseren Empfehlungen folgen und keine personenbezogenen Daten in mögliche Freitextfelder eingeben. Die Rechtsgrundlage ist Ihre Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO. Zur Durchführung von Umfragen, die explizit das App- und Webdesign in den Vordergrund stellen, setzen wir das Tool des Dienstleisters »Maze« (Maze.Design Inc., 800 Menlo Ave, Suite 220, Menlo Park, CA 94025, USA) ein. Wir haben zu diesem Zweck einen Auftragsdatenverarbeitungsvertrag mit Maze geschlossen, der die Einhaltung Ihrer Rechte und die Anforderungen aus Art. 28 Abs. 3 DSGVO gewährleistet und auch die Übermittlung in ein Drittland gem. Art. 44 ff. DSGVO rechtlich absichert. Für die Erstellung aller anderen Umfragen und den daran anknüpfenden Auswertungen arbeiten wir mit dem Anbieter »Qualtrics« (Qualtrics Ireland Limited, Costello House, 1 Clarendon Row, Dublin 2, D02 TA43, Ireland, https://www.qualtrics.com ) zusammen. Auch mit Qualtrics haben wir einen Vertrag über die Auftragsdatenverarbeitung geschlossen.
G) Kontaktaufnahme
Wenn Sie uns kontaktieren möchten, können Sie uns eine E-Mail an aboservice@spiegel.de , aboservice@manager-magazin.de  bzw. Kundenservice@11freunde.de  senden. Wir verarbeiten in diesem Fall Ihre E-Mail-Adresse, Angaben zu Ihrem Anliegen und ggf. Ihren Namen für die weitere Korrespondenz und sich möglicherweise daraus ergebender Maßnahmen zu Ihrer Anfrage. Sie haben darüber hinaus die Möglichkeit uns telefonisch (040 3007-2700) zu kontaktieren. In diesem Fall verarbeiten wir Ihre Telefonnummer, Ihren Namen, ggf. Ihre Kundennummer und Informationen zu Ihrem Anliegen für die weitere Korrespondenz. Wenn Sie das Kontaktformular auf unserer Webseite nutzen, verarbeiten wir den Grund Ihrer Kontaktaufnahme, den Channel, Angaben zu Ihrem Anliegen, die Anrede, Ihren Vornamen (optional auch Ihren Nachnamen) und die E-Mail-Adresse für die weitere Korrespondenz und möglicher angefragter Maßnahmen zu Ihrer Anfrage, sowie Informationen zu Datum, Uhrzeit, verwendetem Browser und URL Ihrer Anfrage. Darüber hinaus verarbeiten wir zur Verhinderung von Massenanfragen durch Bots auch Ihre IP-Adresse. Wenn Sie mit einer Veröffentlichung Ihrer Nachricht einverstanden sind, kann diese sowohl auf unseren Webseiten als auch in unseren Magazinen mit der Angabe Ihres Namens und Ihres Wohnorts veröffentlicht werden. Sie haben weiterhin die Möglichkeit, den ChatBot auf unseren Webseiten für die Kommunikation zu verwenden. Mit Verwendung des Bots verarbeiten wir die Informationen Ihrer Anfrage. Darüber hinaus werden sessionbezogene Metadaten (IP-Adresse, Browser- und Geräteinformationen, Datum und Uhrzeit des Zugriffs) über das Setzen eines Cookies verarbeitet. Sofern Sie uns als Nutzer:in oder Abonnent:in kontaktieren und sich Ihre Anfrage somit im Rahmen des zwischen Ihnen und uns bestehenden Vertrages bewegt, ist die Rechtsgrundlage Art. 6 Abs. 1 S. 1 lit. b DSGVO. Ansonsten verarbeiten wir Ihre Daten aufgrund unseres berechtigten Interesses, mit anfragenden Personen in Kontakt zu treten und die ggf. notwendigen Informationen zur Lösung rein technischer Probleme bereits vorliegen zu haben. Rechtsgrundlage für die Datenverarbeitung ist dann Art. 6 Abs. 1 S. 1 lit. f DSGVO. Für die Zurverfügungstellung des Kontaktformulars arbeiten wir mit dem Angebot »CognitoForms« (Cognito LLC, 1310 Gadsden St Ste 100 Columbia, SC, USA, https://www.cognitoforms.com ) zusammen. Wir haben zu diesem Zweck einen Auftragsdatenverarbeitungsvertrag mit CognitoForms geschlossen, der die Einhaltung Ihrer Rechte und der Anforderungen aus Art. 28 Abs. 3 DSGVO sowie Art. 44 ff. DSGVO gewährleistet. Darüber hinaus setzen wir für eine einheitliche Verwaltung und Bearbeitung sämtlicher Anfragen und Kundenkommunikation für die unter diesem Abschnitt genannten Zwecke die folgenden Anbieter ein:
Zendesk Inc.
989 Market Street, Suite 300
San Francisco, CA 94103, USA
https://www.zendesk.de 
babelforce GmbH
Friedrichstr. 68
10405 Berlin, Deutschland
https://www.babelforce.net 
novomind AG
Bramfelder Chaussee 45
22177 Hamburg, Deutschland
Wir haben mit den Anbietern Verträge nach Art. 28 Abs. 3, sowie ggf. Art. 44 ff. DSGVO geschlossen, um die Einhaltung Ihrer Rechte zu gewährleisten.
H) Veranstaltungen
Wir laden unsere Leser:innen und Abonnent:innen in regelmäßigen Abständen zu Veranstaltungen ein und Sie haben auf unseren Webseiten die Möglichkeit, sich für verschiedene Veranstaltungen anzumelden. Für die Anmeldung zu, die Abwicklung und Durchführung von Veranstaltungen verarbeiten wir Ihren vollständigen Namen, die E-Mail-Adresse und ggf. weitere veranstaltungsspezifische Pflichtangaben. Bei kostenpflichtigen Veranstaltungen benötigen wir zur Zahlungsabwicklung je nach gewähltem Zahlungsmittel weitere Daten. Wenn Sie ein Ticket kaufen oder kostenlos erwerben, setzen wir bei Interaktion mit dem Widget des Ticketshops ein Cookie, das Ihnen Ihren Warenkorb zuordnet und somit für die Abwicklung der Bestellung technisch erforderlich ist. Wir verwenden ggf. Ihre E-Mail-Adresse, um Sie vor einer Veranstaltung über den Start des Livestreams zu informieren. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. b DSGVO, für das Setzen des Cookies § 25 Abs. 2 Nr. 2 TDDDG.
Bei digitalen Angeboten verarbeiten wir in Zusammenhang mit der Ausspielung des Livestreams für Zwecke der Optimierung der Übertragung, sowie der Besucherzählung über das Setzen eines Cookies Ihre IP-Adresse, Typ und Version Ihres Internet-Browsers, verwendetes Betriebssystem, aufgerufene Seite, Referrer-URL, Uhrzeit der Serveranfrage, Verweildauer auf der Webseite und die Häufigkeit des Aufrufs. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG. Während des Livestreams haben Sie die Möglichkeit, sich interaktiv bspw. mit Fragen zu beteiligen. Für diese Zwecke und für die Gewährleistung der Funktionalität und Sicherheit des Tools verarbeiten wir über das Setzen eines Cookies Ihre Teilnehmerdaten (Name), Textdaten und geteilte Inhalte (Fragen, Ideen, Chats etc.), Informationen zum verwendeten Gerät (IP-Adresse, Hardwaremodell, verwendete Software, Spracheinstellungen etc.), Meeting-Metadaten (Datum, Uhrzeit, Name der Veranstaltung etc.) und Informationen zu etwaigen Systemabstürzen. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG. Die Abfrage Ihrer Einwilligung erfolgt im Rahmen der Live-Veranstaltung. Sie haben hier auch die Möglichkeit zur anonymisierten Nutzung des Interaktionstools, d.h. es werden keine personenbezogenen Daten übermittelt. Bei Veranstaltungen, die als Livestream auf unserer Webseite übertragen werden, erfolgt i.d.R. eine öffentliche Zugänglichmachung der Aufzeichnung auf unserer Webseite auch nach der Veranstaltung. Sollten Sie sich für eine nicht-anonymisierte Form der Beteiligung entschieden haben, so erklären Sie sich gleichzeitig mit dieser Veröffentlichung einverstanden. Die Rechtsgrundlage ist Ihre Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO.
Zusätzlich erhalten Sie bei allen Angeboten im Anschluss an die Veranstaltung eine E-Mail mit (weiterführenden) Informationen, sowie eine Feedbackumfrage, an der Sie sich selbstverständlich absolut freiwillig beteiligen können. Sofern wir eine Veranstaltung mit Partnern oder Sponsoren gemeinsam ausrichten, geben wir möglicherweise Ihre Daten für die Anmeldung, Abwicklung und Durchführung der Veranstaltung an unsere Partner weiter. Die Rechtsgrundlage für diese Verarbeitungen ist Art. 6 Abs. 1 S. 1 lit. b DSGVO.
Wir arbeiten für die folgenden Zwecke mit den jeweils genannten Dienstleistern zusammen, mit denen wir Vereinbarungen gem. Art. 28 Abs. 3 DSGVO geschlossen haben, die die Einhaltung Ihrer Rechte gewährleisten und sofern erforderlich im Rahmen von Drittlandübermittlungen die Anforderungen der Art. 44 ff DSGVO erfüllen.
Veranstaltungsanmeldung / Ticketing: »pretix«
rami.io GmbH
Berthold-Mogel-Straße 1
69126 Heidelberg
Deutschland
www.pretix.eu 
Bereitstellung des Livestreams bei digitalen Veranstaltungen: »JW Player«
JW Inc. / Longtail Ad Solutions Inc.
8 West 38th Street
6th Floor
NY 10018 New York
USA
www.jwplayer.com 
Interaktionstool für die Beteiligung während einer digitalen Veranstaltung: »slido«
sli.do s.r.o.
Vajnorská 100/A
831 04 Bratislava
Slowakei
www.slido.com 
Im Falle einer Anmeldung zu kostenpflichtigen Veranstaltungen speichern wir Ihre Daten für die Dauer des Vertrages und danach bis zum Ablauf der gesetzlichen Verjährungsfristen bzw. für die Dauer der steuerrechtlichen Aufbewahrungsfristen. Die entsprechende Datenverarbeitung über die Vertragslaufzeit hinaus beruht auf unserem berechtigten Interesse an der Aufbewahrung der Daten zu den genannten Zwecken und damit auf Art. 6 Abs. 1 S. 1 lit. f DSGVO.
Im Falle einer Anmeldung zu kostenfreien Veranstaltungen bewahren wir Ihre Daten zu Dokumentationszwecken und für den Fall von Rückfragen nach der Veranstaltung noch für die Dauer von sechs Monaten auf.
Ihre Kontaktdaten werden auf Basis des zwischen Ihnen und uns bestehenden Vertrages nach Art. 6 Abs. 1 S. 1 lit. b DSGVO zu Werbezwecken gespeichert. Sie können der Verarbeitung für Werbezwecke selbstverständlich jederzeit widersprechen.
Bereich
6. Werbe-Tracking
aufklappen
A) Vermarktung von Werbeplätzen auf unseren Webseiten
Sofern Sie sich nicht entschlossen haben, die Werbefrei-lesen-Option zu buchen, sichern wie einleitend in dieser Datenschutzerklärung beschrieben Anzeigen und Werbung das Bestehen unseres Angebots. Für die nutzerorientierte Ausspielung von Werbemaßnahmen benötigen wir und unsere Werbepartner verlässliche Angaben darüber, wie viele Nutzer:innen die jeweiligen Anzeigen sehen. Außerdem erwarten wir und insbesondere unsere Partner, dass ihre Werbung an diejenigen Nutzer:innen bevorzugt ausgespielt wird, die sich voraussichtlich für Ihre Produkte oder Angebote / Themen interessieren und somit eine individuelle, nutzerinteressenbasierte Ausspielung erfolgen kann. Nutzen Sie unsere Seite kostenlos, benötigen wir daher Daten für die sogenannte nutzungsbasierte Onlinewerbung (Anzeigen werden auf Nutzerinteressen zugeschnitten), im Rahmen dessen Ihre Nutzungsdaten zu Nutzungsprofilen verarbeitet werden (Werbe-Tracking).
iqd, ID5
Wir verarbeiten für Zwecke des beschriebenen Werbe-Tracking Ihre Nutzungsdaten (Cookie-IDs, IP-Adresse, MAC-Adresse, Geräte-IDs, Informationen über den verwendeten Browser und Daten wie Auflösung oder Spracheinstellung, Informationen über das Betriebssystem, das verwendete Endgerät, das Verhalten während Ihres Besuchs auf unseren Webangeboten (einzelne Klicks, besuchte Unterseiten, wahrgenommene Werbebanner etc), die Referrer-URL, Datum und Uhrzeit des Besuchs, die Verweildauer und Ihren geografischen Standort), um Ihnen interessengerechte Werbeanzeigen präsentieren zu können. Damit Anzeigen auch dort auf Sie oder Ihr Endgerät zugeschnitten werden können, wo Cookies nicht optimal funktionieren, zum Beispiel in Apps auf Smartphones, werden unter Umständen Cookie-ähnliche Techniken und Quellen wie Gerätedaten eingesetzt. Um dies zu unterbinden, gehen Sie auf Android-Smartphones in die App »Google-Einstellungen« oder scrollen in der allgemeinen Einstellungs-App nach unten zu »Google«, tippen auf »Anzeigen« und deaktivieren dort das Kästchen neben »Interessenbezogene Werbung«. Auf iOS-Geräten, wo wir den Advertising Identifier von Apple nutzen, gehen Sie in die Einstellungs-App, dann auf »Datenschutz«, schließlich auf »Werbung« und nehmen Ihre konkreten Einstellungen vor. Von diesen Maßnahmen zur Unterbindung des Werbe-Trackings ausgenommen sind Werbeanzeigen, die in Zusammenhang mit der von uns eingesetzten AdDefend-Technologie ausgespielt werden (vgl. Erläuterungen zu AdDefend in dieser Ziffer 6. A).
Die auf dem Werbe-Tracking basierende Erstellung von Nutzungsprofilen erfolgt pseudonymisiert, das bedeutet, die Sie unmittelbar persönlich kennzeichnenden Merkmale werden aus den Profilen herausgelöst. Die Rechtsgrundlage ist Ihre Einwilligung gem. Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG, die jeweils obligatorisch ist, sofern Sie nicht die Werbefrei-lesen-Option gebucht haben. Unsere Werbepartner verarbeiten darüber hinaus personenbezogene Daten für eigene funktionale Zwecke, insbesondere die Gewährleistung der Sicherheit, Verhinderung und Aufdeckung von Betrug und Fehlerbehebungen auf Basis Ihres berechtigten Interesses gem. Art. 6 Abs. 1 S. 1 lit. f DSGVO. Bei der Vermarktung von Werbeplätzen und für die Verarbeitung zu funktionalen Zwecken kommt es auch zu Datenverarbeitungen durch Anbieter aus Drittstaaten, insbesondere den USA. In diesen Fällen werden Ihre Rechte entsprechend Art. 45 ff. DSGVO geschützt (Angemessenheitsbeschluss, Zertifizierung, Standardvertragsklauseln), Details zu den einzelnen Anbietern erfahren Sie in unserem Privacy-Center. Im Privacy-Center finden Sie auch eine Übersicht zu den für das kostenlose Angebot zugelassenen und aktuell genutzten Anzeigendienstleistern, den so genannten »Third-Party-Trackern«. Die Art und Funktionsweise dieser Dienste verändert sich nicht, es können aber von Zeit zu Zeit einzelne Dienste und / oder Cookies hinzukommen oder wegfallen.
Hinsichtlich der Vermarktung der Werbemittelplätze auf unseren Webseiten arbeiten wir mit der »iqd« (iq digital media marketing gmbh, Toulouser Allee 27, 40211 Düsseldorf, Deutschland) in der Form zusammen, dass wir gemeinsam für die Datenverarbeitung Verantwortliche im Sinne des Art. 26 DSGVO sind. In diesem Zusammenhang haben wir und die iqd in einer gemeinsamen Vereinbarung die jeweiligen Zuständigkeiten geregelt und festgelegt, wer von uns welche gesetzlichen Pflichten erfüllt. Dies betrifft insbesondere die Wahrnehmung der Rechte der betroffenen Personen und die Erfüllung der Informationspflichten nach den Artikeln 13 und 14 DSGVO. Die datenschutzrechtlichen Verpflichtungen verteilen sich dabei grundsätzlich wie folgt:
iqd ist zuständig für Werbevermarktung, zusammen mit Dritten (sog. Vendoren). Dies beinhaltet die Vergabe von Werbeplätzen und die Erstellung von Nutzungsprofilen (unter anderem durch segmentbasierte Profilerstellung und anschließende, gezielte Nutzeransprache), auch über die Nutzung unserer Angebote hinaus.
Wir sind zuständig für die Schaffung der datenschutzrechtlichen Voraussetzungen, insb. das Einholen Ihrer Einwilligung als Rechtsgrundlage für diese Datenverarbeitungen.
Sie erhalten eine Auskunft grundsätzlich von derjenigen Stelle, bei der Sie Ihre Rechte geltend gemacht haben, solange die Datenverarbeitung im Rahmen der gemeinsamen Verantwortlichkeit erfolgt. In allen anderen Fällen erhalten Sie die Auskunft selbstverständlich von uns. Zusätzlich zu den Hinweisen in unserer Datenschutzerklärung sowie in unserem Privacy-Center können Sie sich auch in der Datenschutzerklärung  der iqd über die durch iqd eingesetzten Partner und Toolanbieter und die jeweiligen Datenverarbeitungen informieren. In der Datenschutzerklärung von iqd finden Sie etwa Informationen über die Empfänger von Daten, Löschfristen, Erlaubnistatbestände und detaillierte Angaben über die genaueren Zwecke der Datenverarbeitung durch jeden einzelnen Partner und Toolanbieter.
Wir verarbeiten für Zwecke des Werbe-Trackings weiterhin personenbezogene Daten oder andere Informationen, die wir von Ihnen erheben, wie z.B. Ihre E-Mail-Adresse (in gehashter, pseudonymisierter Form), Ihre IP-Adresse und/oder Informationen über Ihren Browser oder Ihr Betriebssystem. Die Rechtsgrundlage ist Ihre Einwilligung gem. Art. 6 Abs. 1 S. 1 lit. a DSGVO. Die genannten Daten werden von »ID5« (ID5 Technology Ltd., 8 Devonshire Square, EC2M 4YJ London, UK) und ihren Konzernunternehmen sowie anderen Partnern (Google, Google Advertising Products, Meta) weitergegeben, die als Datenverantwortliche für diesen Bereich fungieren. ID5 und die anderen Partner verwenden diese Informationen, um eine ID zu erstellen, mit der Sie auf Ihren Geräten erkannt werden können. Diese ID enthält keine identifizierbaren persönlichen Daten. Liegt Ihre Einwilligung vor, können wir diese ID in unserem First-Party-Cookie platzieren oder ein ID5- bzw. Partner-Cookie verwenden und somit zulassen, dass die ID für die o. g. Zwecke verwendet wird. Die ID kann von uns oder in unserem Namen an unsere Werbepartner und andere Drittanbieter von Werbung weltweit weitergegeben werden. Detaillierte Informationen zu den Datenverarbeitungsaktivitäten von ID5 im Zusammenhang mit dieser ID und dem Opt-out von ID5 finden Sie in der Datenschutzrichtlinie der ID5-Plattform unter folgendem Link: https://id5.io/platform-privacy-policy , für Google Advertising Products unter https://myadcenter.google.com/home?hl=d. 
Utiq
Sofern Sie eingewilligt haben, verarbeitet die Utiq-Technologie Nutzungsdaten zum Zwecke der Aktivierung und des Betriebs der Technologie. Diese Technologie basiert auf der Zusammenarbeit mit teilnehmenden Telekommunikationsnetzbetreibern und funktioniert nur, wenn Sie Kunde eines der teilnehmenden Telekommunikationsnetzbetreiber sind. Konkret bedeutet dies, dass Ihre Nutzungsdaten (insbesondere die IP-Adresse) in der Form verarbeitet werden, dass sie an Ihren Telekommunikationsanbieter übermittelt werden, der diese wiederum mit einer Kontoreferenz (z.B. Ihrer Handynummer oder Ihren Vertragsdaten) abgleicht, um eine Online-Kennung zu erstellen und an Utiq weiterzugeben. Utiq wiederum generiert auf Grundlage der Online-Kennung eine weitere Kennung ("martechpass"). Diese ermöglicht es uns, Sie als denselben Besucher einer Webseite zu erkennen und Informationen für die Ausspielung interessensbasierter Werbung auf unseren digitalen Angeboten zu sammeln. Wir können diese Kennung jedoch nur nutzen, um ihr Browsing über unsere hier aufgeführten digitalen Angebote zu verknüpfen, wenn Sie Utiq auf jeder dieser Seiten separat Ihre Einwilligung erteilt haben. Die Rechtsgrundlage ist Ihre freiwillige Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO.
Wir arbeiten für diesen Zweck mit der »iqd« (iq digital media marketing gmbh, Toulouser Allee 27, 40211 Düsseldorf, Deutschland) und der »Utiq« (Utiq SA/NV Rue aux Laines 70, 1000 Brüssel, Belgien) in der Form zusammen, dass wir gemeinsam für gewisse Bereiche der Datenverarbeitung Verantwortliche im Sinne des Art. 26 DSGVO sind. In diesem Zusammenhang haben wir, iqd und Utiq in einer gemeinsamen Vereinbarung die jeweiligen Zuständigkeiten geregelt und festgelegt, wer von uns welche gesetzlichen Pflichten erfüllt. Dies betrifft insbesondere die Wahrnehmung der Rechte der betroffenen Personen und die Erfüllung der Informationspflichten nach den Artikeln 13 und 14 DSGVO. Die datenschutzrechtlichen Verpflichtungen verteilen sich dabei grundsätzlich wie folgt:
Wir sind zuständig für die Schaffung der datenschutzrechtlichen Voraussetzungen, insb. das Einholen Ihrer Einwilligung als Rechtsgrundlage für diese Datenverarbeitungen und die Integration von Utiq-Code auf unseren Webseiten, um den Zugriff auf die IP-Adressen der Nutzer:innen zu ermöglichen.
Utiq ist zuständig für die Nutzung der IP-Adresse zur Aktivierung der Utiq-Techologie
Iqd verantwortet die Erstellung, Pflege und Analyse von Nutzerprofilen auf Basis der vorhandenen und zur Verfügung gestellten Daten.
Sie erhalten eine Auskunft grundsätzlich von derjenigen Stelle, bei der Sie Ihre Rechte geltend gemacht haben, solange die Datenverarbeitung im Rahmen der gemeinsamen Verantwortlichkeit erfolgt. In allen anderen Fällen erhalten Sie die Auskunft selbstverständlich von uns. Über das Utiq-Datenschutzportal ("consenthub")  können Sie alle von Ihnen erteilten Einwilligungen zur Nutzung der Utiq-Technologie über alle digitalen Angebote hinweg einsehen und widerrufen. Sie können Ihre nur für dieses digitale Angebot geltende Utiq Einwilligung entweder über den Consenthub  oder alternativ über die Seite "Utiq verwalten"  widerrufen. Weitere Informationen über die Utiq-Technologie erhalten Sie in der Datenschutzerklärung  von Utiq.
Google Ads Conversion-Tracking
Unsere Webseiten nutzen das Conversion-Tracking Google Ads unseres Werbepartners »Google« (Google Inc., 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA). Dabei wird von Google Ads ein 30 Tage lang gültiges Cookie auf Ihrem Endgerät gesetzt, welches nicht der persönlichen Identifizierung dient. Besuchen Sie nun bestimmte Webseiten und das Cookie ist noch aktiv, können Google und wir erkennen, dass Sie auf die Anzeige geklickt haben und zu dieser Seite weitergeleitet wurden. Jede(r) Google AdWords-Kund:in erhält ein anderes Cookie. Somit besteht keine Möglichkeit, dass Cookies über die Webseiten von AdWords-Kund:innen nachverfolgt werden können. Die mithilfe des Conversion-Cookies eingeholten Information dienen dazu, Conversion-Statistiken für Google Ads-Kund:innen zu erstellen, die sich für Conversion-Tracking entschieden haben. Die Google Ads-Kund:innen erfahren die Gesamtzahl der Nutzer:innen, die auf ihre Anzeigen geklickt haben und zu einer mit einem Conversion-Tracking-Tag versehenen Seite weitergeleitet wurden. Sie erhalten jedoch keine Informationen, mit denen sich Nutzer:innen identifizieren lassen. Googles Hinweise zum Conversion-Tracking finden Sie hier. Die Rechtsgrundlage ist Ihre Einwilligung nach Art. 6 Abs. 1 S. 1 lit. a DSGVO und § 25 Abs. 1 TDDDG. Bei Datenverarbeitungen durch Google in Drittstaaten werden Ihre Rechte sowohl durch Angemessenheitsbeschlüsse, Zertifizierungen als auch Standardvertragsklauseln geschützt (Art. 45 ff. DSGVO), Details erfahren Sie in unserem Privacy-Center und hier.
Wenn Sie das Google Ads Conversion Tracking ausschließen möchten, können Sie auch das hierfür erforderliche Setzen eines Cookies ablehnen oder Cookies für Conversion-Tracking in Ihren Browsereinstellungen deaktivieren.
Google Display & Video 360
Wir möchten auf unseren Webseiten gezielt Werbung schalten und die Effektivität unserer Marketingmaßnahmen analysieren. Für die Ausspielung von relevanten Anzeigen und die Messung und Optimierung der Kampagnenleistung verarbeiten wir Cookies (Cookie-IDs und Browser-, sowie Endgeräteinformationen) für die Sammlung von Informationen über Ihre Interaktionen mit unseren Anzeigen. Die Rechtsgrundlage ist Ihre Einwilligung gem. Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG. Für die beschriebenen Analysen und Datenverarbeitungen verwenden wir den Online Marketing Dienst Display & Video 360 (DV360) von »Google«.
Neben den allgemeinen Widerrufsmöglichkeiten können Sie die Erfassung von personalisierter Werbung durch Google zukünftig in den Einstellungen Ihres Google Accounts unter den nachfolgenden Link unterbinden https://adssettings.google.com/u/0/authenticated?hl=de. 
AdDefend
Sofern Sie eine unserer Webseiten bei gleichzeitiger Installation eines AdBlockers auf Ihrem Endgerät aufrufen, verarbeiten wir personenbeziehbare Daten für Zwecke der Aufdeckung / Verhinderung von Ad-Blocking und der anschließenden Ausspielung von Werbung. Wir setzen für diese Zwecke ein Cookie im Browser Ihres Endgerätes und verarbeiten in diesem Zusammenhang die Ihr Endgerät bzw. Ihren Browser identifizierende Cookie-ID. Die Rechtsgrundlage ist Ihre Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG.
Wir setzen für diese Zwecke die AdDefend-Technologie (AdDefend GmbH, Borselstraße 3, 22765 Hamburg, Deutschland, https://www.addefend.com/de/datenschutzerklarung/ ) in der Form ein, dass wir und AdDefend gemeinsam für gewisse Bereiche der Datenverarbeitung Verantwortliche im Sinne des Art. 26 DSGVO sind. In diesem Zusammenhang haben wir und AdDefend in einer gemeinsamen Vereinbarung die jeweiligen Zuständigkeiten geregelt und festgelegt, wer von uns welche gesetzlichen Pflichten erfüllt. Dies betrifft insbesondere die Wahrnehmung der Rechte der betroffenen Personen und die Erfüllung der Informationspflichten nach den Artikeln 13 und 14 DSGVO. Die datenschutzrechtlichen Verpflichtungen verteilen sich dabei grundsätzlich wie folgt:
· Wir sind zuständig für die Schaffung der datenschutzrechtlichen Voraussetzungen, insb. das Einholen Ihrer Einwilligung als Rechtsgrundlage für diese Datenverarbeitungen und die Integration der AdDefend-Technologie auf unseren Webseiten.
· AdDefend ist zuständig für di Aufdeckung / Verhinderung von Ad-Blocking und die Übermittlung der erhobenen Nutzungsdaten an Monetarisierungsplattformen und Werbetreibende für Zwecke des Ausspielens von Werbung, sowie für die damit zusammenhängende Abrechnung im Rahmen unserer Partnerschaft.
Sie erhalten eine Auskunft grundsätzlich von derjenigen Stelle, bei der Sie Ihre Rechte geltend gemacht haben, solange die Datenverarbeitung im Rahmen der gemeinsamen Verantwortlichkeit erfolgt. In allen anderen Fällen erhalten Sie die Auskunft selbstverständlich von uns.
B) Eigene personalisierte werbliche Verlagsangebote (Eigenwerbung)
Wir machen auf unseren Webseiten personalisierte Werbung für unsere eigenen und als solche gekennzeichneten Produkte (Verlagsangebote). Neben dem reinen Produktangebot versuchen wir, die Relevanz der Auswahl von Verlagsangeboten stetig zu verbessern, um Ihnen unsere Angebote auf Basis unseres verlagsspezifisches Angebotsprofil des von ihnen verwendeten Endgeräts respektive SPIEGEL-Login-Kontos zu unterbreiten. Dafür verwenden wir Nutzungsprofile, die auf Grundlage des Adobe-basierten Werbe-Trackings erstellt werden. Über das Setzen von Cookies verarbeiten wir für diese Zwecke Informationen über Ihr Endgerät und den von Ihnen verwendeten Browser, Ihre IP-Adresse, Ihre Interaktionen mit unseren Webseiten und sofern Sie sich als registrierte(r) und angemeldete(r) Nutzer:in auf unseren Webseiten bewegen auch eine Ihnen zugeordnete eindeutige Kennung, die Ihr Nutzungsverhalten mit Ihren Accountinformationen verknüpft. Die Rechtsgrundlage ist Ihre Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO.
C) Politische Werbung
Für Zwecke der Ausspielung personalisierter politischer Werbung verarbeiten wir die Sie betreffenden Informationen aus dem Adobe-basierten Werbe-Tracking (vgl. Ziffer 3.) und verwenden Sie iRd. Werbe-Trackings (vgl. Ziffer 6. A). Diese Verarbeitung basiert auf Ihrer freiwilligen Einwilligung gem. Art. 18 Abs. 1 lit. b der Verordnung über die Transparenz und das Targeting politischer Werbung (TTPW-VO) iVm. Art. 6 Abs. 1 S. 1 lit. a, Art. 7 DSGVO.
D) Marketing (Affiliate Marketing und Remarketing)
Affiliate Programme
Affiliate Marketing ist eine Art der Online-Werbung, bei der Unternehmen mit Partnern, sogenannten Affiliates, zusammenarbeiten, um ihre Produkte oder Dienstleistungen zu bewerben. Diese Affiliates platzieren Links oder Banner auf ihren eigenen Webseiten oder Plattformen, die auf unsere Unternehmen verweisen. Wenn ein Nutzer auf einen solchen Link klickt und einen Kauf tätigt oder eine bestimmte Aktion ausführt, erhält der Affiliate-Partner eine Provision. Diese Methode ermöglicht es Unternehmen, ihre Reichweite zu vergrößern und nur für tatsächliche Ergebnisse zu bezahlen, während Affiliates durch die erfolgreiche Vermittlung von Kunden Einnahmen erzielen. Das Affiliate-Marketing wird häufig mit Methoden des Re-Marketing (auch als Re-Targeting bekannt) kombiniert, bei denen die Praxis im Vordergrund steht, Nutzer:innen erneut anzusprechen, die bereits mit einer Webseite oder einem Produkt interagiert haben, ohne einen Produkterwerb abzuschließen. Dies geschieht häufig durch das Platzieren von Anzeigen auf anderen Webseiten oder Plattformen, die ein Nutzer besucht.
Wir verarbeiten personenbezogene Daten in Zusammenhang mit Affiliate-Marketing-Kampagnen. Im Rahmen dessen analysieren wir die Leistung unserer Werbemaßnahmen, um diese kontinuierlich zu verbessern und Ihnen relevante Angebote präsentieren zu können (Optimierung unseres Affiliate-Marketing-Angebots). Die erfassten Daten ermöglichen es uns, unsere Affiliate-Partner fair und transparent für erfolgreiche Vermittlungen zu entlohnen (leistungsbasierte Vergütung). Darüber hinaus möchten wir durch die Analyse des Nutzerverhaltens unsere Webseite und deren Funktionen optimieren, um Ihnen eine bessere Nutzererfahrung zu bieten (Verbesserung der Nutzerfreundlichkeit). Wir verwenden personenbezogene Daten, um Wege unserer Nutzer:innen zwischen den Advertisern und unseren Webseiten nachvollziehen und Transaktionen korrekt zuordnen zu können (Nachvollziehbarkeit der Transaktionen) und wir verbessern auf Grundlage der Bewegungspfade unser Produktportfolio und unsere Dienstleistungen (Weiterentwicklung des Angebots). Im Rahmen des Affiliate Marketings verarbeiten wir Tracking-Domain-Cookies, pseudonymisierte Nutzerprofile, Klickdaten (Zeitpunkt, Art des Werbemittels, Webseite), Geräteinformationen (Browser, Bildschirmauflösung), die IP-Adresse (anonymisiert) und ggf. die Bestell-ID.
Die Verarbeitungen basieren auf Ihrer freiwillig erteilten Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG. Wir arbeiten im Rahmen des Affiliate Marketings mit den nachfolgend genannten Agenturen zusammen, die als Schnittstelle zwischen uns und den jeweiligen Publishern fungieren:
AWIN
AWIN AG, Landsberger Allee 104 BC, 10249 Berlin, Deutschland
Informationen zum Datenschutz durch AWIN finden Sie unter folgendem Link: https://www.awin.com/de/datenschutzerklarung 
The Reach Group
The Reach Group GmbH, Am Karlsbad 16, 10785 Berlin, Deutschland
Informationen zum Datenschutz der The Reach Group GmbH finden Sie unter https://trg.de/datenschutzerklarung/   . Sie können die Speicherung des Cookies, die in Zusammenhang mit diesem Anbieter verarbeitet werden über eine entsprechende Einstellung Ihrer Browser-Software verhindern. Darüber hinaus können Sie die Erfassung der durch das Cookie erzeugten und auf Ihre Nutzung der Webseite bezogenen personenbezogenen Daten verhindern, indem Sie die Opt-Out-Funktion unter dem folgenden Link aktivieren:
https://hal9000.redintelligence.net/privacy/8lcfmzhxc8d6/ 
Dieser Widerspruch gilt so lange, wie das zugehörige Opt-Out-Cookie nicht gelöscht wird. Dieses Cookie wird für die Domain, pro Browser und Benutzer eines Rechners gesetzt. Wenn Sie auf unsere Webseiten von mehreren Endgeräten und Browsern aus zugreifen, müssen Sie daher auf jedem dieser Geräte und in jedem Browser der Datenerfassung separat widersprechen.
Targeting360
targeting360 GmbH, Gredinger Str. 28, 90453 Nürnberg, Deutschland
targeting360 verwendet die NEORY-Adserving-Technologie (NEORY GmbH, Brandschachtstr. 2, 44149 Dortmund, Deutschland), welche ein Portfolio an Webseiten beinhaltet, über die angebotene Inhalte und Besucherinteressen einem bestimmten Endgerät zugeordnet werden können.
Das Setzen des Cookies können Sie durch entsprechende Einstellungen verhindern, folgen Sie dazu diesem Link: https://ad.ad-srv.net/privacy/kdb0xdq3ls8m/   .
Remarketing/ Retargeting Direktpartnerschaften
Über Direktpartnerschaften versuchen wir im Rahmen der Schaltung von Retargeting- und Remarketing-Kampagnen Nutzer:innen erneut anzusprechen, nachdem sie eine unserer Webseiten besucht, aber keine gewünschte Aktion (z.B. Abo-Abschluss) durchgeführt haben. Zu diesem Zweck verarbeiten wir personenbezogene Daten, wie Cookies und Tracking-Daten, IP-Adressen, Geräteinformationen und das Nutzerverhalten. Die Verarbeitungen basieren alle auf Ihrer freiwillig erklärten Einwilligung (Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG). Mögliche Drittlandsübermittlungen in diesem Zusammenhang sind über die rechtlichen Anforderungen der Art. 44 ff DSGVO gerechtfertigt. Wir arbeiten zu diesem Zweck mit den nachfolgend genannten Partnern zusammen:
Google Remarketing
Wir verwenden die Remarketing Funktion von »Google« (Google Inc., 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA). Wenn Sie bestimmte Webseiten von uns besuchen, haben wir dort einen von Google bereitgestellten Programmcode integriert. Hierdurch wird eine Kennzahl, die Google zuvor in einem Cookie in Ihrem Browser gespeichert hat, an Google übermittelt. Google erfasst so, dass Sie bestimmte Webseiten von uns aufgerufen haben. Wenn Sie später bei Google Suchbegriffe eingeben oder auf anderen Webseiten Werbeanzeigen aus dem Google Netzwerk sehen, haben wir die Möglichkeit, Ihnen dort gezielt unsere Anzeigen zu präsentieren (sogenanntes "Remarketing"). Wenn Sie ein Google Konto besitzen, verknüpft Google diese Daten ggf. mit Ihrem Google-Konto. Informationen zum Datenschutz bei Google und zur Funktionsweise des Remarketing finden Sie in den Google Datenschutzhinweisen sowie den Erläuterungen zu Google Remarketing. Sie können dem Remarketing widersprechen, indem Sie Ihren Browser so konfigurieren, dass dieser keine Cookies akzeptiert. Die Nutzung unserer Webseite ist dann jedoch ggf. nur noch eingeschränkt möglich. Sie können zudem in den Google Anzeigeeinstellungen interessensbezogene Anzeigen von Google hier  deaktivieren.
Meta Remarketing/Retargeting
Wir haben in unseren Angeboten, wo passend, Remarketing-Tags von »Meta« (Meta Platforms Inc, 1601 Willow Road, Menlo Park, CA 94025, USA) zugehörigen sozialen Netzwerken (bspw. Facebook) integriert. Wenn Sie bei einem der Netzwerke angemeldet sind, erhält die Plattform über eigens gesetzte Cookies im Webbrowser oder in der APP die Information, dass Sie unsere Seite besucht haben, wodurch wir Sie mit Werbung auf den Meta zugehörigen Netzwerken ansprechen können. Die Übermittlung Ihrer Daten als Nutzer:in wird in der Datenschutzerklärung von Meta selbst geregelt. Sie können hier  die Datenschutzeinstellungen für Ihr Profil verwalten. Wenn Sie darüber hinaus ein Kundenkonto bei uns besitzen und sich durch den Login bei uns authentifizieren, können wir mittels einer eindeutigen Kennung Ihre Profil-/Bestandsdaten endgeräteübergreifend mit Ihren Nutzungsdaten verknüpfen. Auf Grundlage dieser angereicherten Profile können wir Ihnen individualisierte Verlagsangebote über unser Werbenetzwerk zukommen lassen.
Bing Ads
Der Dienst des US-Unternehmens »Microsoft« (Microsoft Corporation, One Microsoft Way, Redmond, WA 98052-6399, USA) setzt ein Cookie, wenn Sie über eine Bing-Anzeige auf eine unserer Angebotsseiten gelangt sind, damit wir die Gesamtzahl der Klicks von der Anzeige auf unsere Angebote erfahren. Ihr Nutzungsprofil wird nur pseudonymisiert erfasst. Eine Übertragung von Daten findet nicht statt. Sie können dies hier  unterbinden.
Sovendus
Der deutsche Dienstleister »Sovendus« (Sovendus GmbH, c/o Design Offices Karlsruhe Bahnhofplatz, Bahnhofplatz 12, 76137 Karlsruhe, Deutschland) bietet einige unserer Produkte im Internet als Werbepartner zum Kauf an. Um eine korrekte Abrechnung zu gewährleisten, wird beim Kauf ein Pixel des Services abgesetzt, um pseudonymisiert und verschlüsselt das bestellte Angebot samt Zeitstempel und IP-Adresse zu übermitteln, wobei Letztere nur der Datensicherheit halber verwendet und im Regelfall nach sieben Tagen anonymisiert wird. Details zum Datenschutz bei Sovendus finden Sie hier .
TikTok Retargeting
Wir haben in unseren Angeboten, wo passend, Retargeting-Pixel des sozialen Netzwerks »TikTok« (TikTok Technology Limited, 10 Earlsfort Terrace, D02 T380, Co. Dublin, Irland) integriert. Wenn Sie auf TikTok angemeldet sind, erhält die Plattform die Information, dass Sie unsere Seiten besucht haben, wodurch wir Sie gezielt mit Werbung auf TikTok ansprechen können. Die Übermittlung Ihrer Daten als TikTok-Nutzer:in wird in dem Leitfaden "Anzeigen und Ihre Daten" des Netzwerks selbst geregelt, welchen Sie hier  finden. Sie können hier  die Einstellungen für personalisierte Werbung für Ihr Konto ausschalten.
Bereich
7. Eigene Produkt- und Vertriebsentwicklung
aufklappen
Im Rahmen unserer eigenen Produkt- und Vertriebsentwicklung arbeiten wir kontinuierlich daran, unsere Angebote zu verbessern und an die Bedürfnisse unserer Leser:innen anzupassen. Dies umfasst die Analyse von Marktrends, die Optimierung unserer Produktpalette sowie die Weiterentwicklung unserer Vertriebsstrategien. Ziel ist es, Ihnen als Leser:in stets hochwertige und bedarfsgerechte Lösungen anbieten zu können. Um diesen Prozess zu unterstützen, können bestimmte Daten zu festgelegten Zwecken, wie im Folgenden beschrieben, verwendet werden.
On-site-Kampagnen und Testings
Um unsere Webseite und Dienste kontinuierlich zu verbessern, spielen wir regelmäßig On-site-Kampagnen (darunter A/B/n-Tests, Verlagsangebote und ähnliche Verfahren zur Produktentwicklung) aus. Dabei werden u.a. verschiedene Versionen unserer Webseite oder einzelner Elemente einer begrenzten Anzahl von Nutzer:innen gezeigt, um deren Wirksamkeit zu testen. Die Testergebnisse dienen der Optimierung der Benutzerfreundlichkeit unserer Webseite, der Verbesserung unserer Produkte und Dienstleistungen und der Analyse des Nutzerverhaltens zur Weiterentwicklung unseres nutzungsorientierten Angebots. Wir verarbeiten für diese Zwecke die unter dem Stichwort "Adobe-basiertes Werbe-Tracking" zusammengefassten Daten (vgl. Ziffer 3. dieser Datenschutzerklärung) erhobenen Daten, ggf. auch unter Einsatz von KI-Systemen in Einzelfall. Die Verarbeitung erfolgt auf Grundlage Ihrer freiwillig erklärten Einwilligung gem. Art. 6 Abs. 1 S. 1 lit. a DSGVO.
Profilbezogene Ansprache
Auf Grundlage des Basistracking verarbeiten wir Ihre personenbezogenen Daten für allgemeine Auswertungen hinsichtlich unserer Produkte und Angebote. Wir nutzen die Analysen, um Sie gemäß ermittelter, ggf. ergänzt durch von Ihnen bereitgestellte, Profilinformationen einem Segment zuordnen und anschließend mit On-Site-Kampagnen und Personalisierungen ansprechen zu können.
Diese Eigenschaften für die profilbezogene Ansprache ergeben sich aus dem Kontext (dem Inhalt der angezeigten Webseite), aus ihren Merkmalen der Profilbildung, sowie aus Analysen und Tests der Produktentwicklung. Die daraus resultierende profilbezogene Ansprache basiert im Wesentlichen auf dem »Engagement Score« (Kennzeichnung Ihres Profils mit einer berechneten Nutzungsintensität unserer Angebote), dem »Propensity Score« (Kennzeichnung Ihres Profils mit einer berechneten Abo-Abschlusswahrscheinlichkeit) und dem »Churn Score« (Kennzeichnung Ihres Profils mit einer berechneten Kündigungswahrscheinlichkeit)). Diese Zuordnungen helfen uns bei der Bildung von Segmenten zur Ausspielung von passgenauen und relevanten On-Site-Kampagnen inkl. geeigneter Umfragen und bei der Ausspielung von E-Mail-Kampagnen. Die Rechtsgrundlage ist unser berechtigtes Interesse, Art. 6 Abs. 1 S. 1 lit. f DSGVO. Sie können der Verarbeitung jederzeit widersprechen.
Nutzer:innen Feedback
Sofern Sie unser Kontaktformular (Ziffer 4. G) nutzen verarbeiten wir den Grund Ihrer Kontaktaufnahme, Channel und/oder Feature, den Betreff und Ihre inhaltliche Nachricht, um herauszufinden, wie zufrieden unsere Nutzer:innen mit unserem Produkt sind und damit in unserem berechtigten Interesse, Ihr Feedback direkt aufzunehmen und ggf. umzusetzen. Die Verarbeitung ist für die Produkt- und Vertriebsentwicklung erforderlich. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. f DSGVO. Wir setzen für die Erfassung der Informationen aus dem Kontaktformular den unter 4. G. bereits benannten Anbieter Zendesk ein, weshalb die systemseitigen Datenverarbeitungen durch Zendesk auch auf die Verarbeitung Ihres Feedbacks zu dem hier genannten Zweck erforderlich ist. Für die Darüber hinaus verwenden wir für die Strukturierung und Zusammenfassung der Feedbacks über das Formular die KI-Anwendung »Langdock« (Langdock GmbH, Fehrbelliner Straße 4, 10119 Berlin, https://www.langdock.com/de ) ein, mit denen wir einen Auftragsverarbeitungsvertrag gem. Art. 28 Abs. 3 DSGVO geschlossen haben.
Bereich
8. Einbinden von Drittinhalten
aufklappen
A) Redaktionelle Kooperationsangebote (Rechner)
Wir nutzen in unseren Artikeln immer wieder Dienste von Kooperationspartnern. Dabei kann es sich um unterschiedliche Angebote handeln, ein Beispiel ist die Einbindung des »Brutto-Netto-Rechners«. Wenn Sie ein solches Drittangebot nutzen möchten, wird i.d.R. mindestens Ihre IP-Adresse in Zusammenhang mit dem Aufruf verarbeitet. Wir zählen solche Dienste zwar zum Bestandteil unseres journalistischen Angebots, in datenschutzrechtlicher Hinsicht werden sie jedoch in eigener Verantwortlichkeit des jeweiligen Anbieters erbracht. Die Rechtsgrundlage für die Weiterleitung an das Drittangebot ist Ihre Einwilligung gem. Art. 6 Abs. 1 S. 1 lit. a DSGVO, die wir i.d.R. der Nutzung über ein sogenanntes Vorschaltfenster zwingend vorangestellt haben. Bitte informieren Sie sich auf der Webseite des jeweiligen Anbieters über die Datenverarbeitungen in Zusammenhang mit der Nutzung.
B) Social Media
Plattformen Soziale Netzwerke im Einzelnen
Wir binden auf unserer Seite Postings und Empfehlungsfunktionen von Facebook, YouTube, X, Instagram, Giphy, Imgur, Spotify, TikTok, Vimeo, Reddit, Bluesky (abgekürzt: Bsky) oder dem Kartendienst Mapbox ein. Diese Dienste sind standardmäßig deaktiviert, sie können Sie aber insgesamt, oder im Einzelfall aktivieren. Darüber hinaus unterhalten wir Profil in Sozialen Netzwerken, die der Kommunikation und der Außendarstellung dienen.
Wenn Sie einen Account bei einem Social-Media-Anbieter besitzen und eingeloggt sind, während Sie den jeweiligen Dienst auf einer unserer Webseiten aktivieren, kann der Besuch Ihrem Nutzerkonto zugeordnet und Ihr Nutzungsverhalten in der Regel umfassend analysiert werden. Ihre personenbezogenen Daten können auch dann erfasst werden, wenn Sie nicht eingeloggt sind oder gar keinen Account bei dem jeweiligen Social-Media-Anbieter besitzen. Die Datenerfassung erfolgt in diesem Fall beispielsweise über Cookies, die auf Ihrem Endgerät gespeichert werden oder durch die Erfassung von Metadaten, wie bspw. Ihrer IP-Adresse. Auch über diesen Weg können Betreiber sozialer Netzwerke Nutzerprofile erstellen, die als Basis für interessenbezogene Werbung in- und außerhalb der jeweiligen Social-Media-Plattform angezeigt werden. Wir haben weder Einfluss auf die vom Anbieter erhobenen Daten und Verarbeitungsvorgänge, noch sind uns die Verarbeitungszwecke und der jeweilige Umfang der Verarbeitung bekannt. Details entnehmen Sie bitte den Nutzungsbedingungen und Datenschutzbestimmungen der jeweiligen Social-Media-Portale. Bitte passen Sie bei dem jeweiligen Dienst Ihre Privatsphäre Einstellungen an.
Wir möchten Sie darauf hinweisen, dass Ihre auf einer Social-Media-Präsenz erhobenen Daten aufgrund des Sitzes der allermeisten Anbieter ggf. auch außerhalb des Raumes der Europäischen Union in sogenannten Drittstaaten verarbeitet werden können. Bei Angebot Ihrer Dienste innerhalb der EU / des EWR (Europäischen Wirtschaftsraumes) sind die Anbieter trotzdem verpflichtet, die europäischen Datenschutzgesetze einzuhalten. Bitte versichern Sie sich auf den Webseiten der einzelnen Anbieter über die Gewährleistung der Einhaltung rechtlicher Anforderungen bei Datenübermittlungen in Drittstaaten.
Die Rechtsgrundlage für die Verarbeitungen, die durch Aktivierung dieser sozialen Netzwerke anfallen, ist Ihre freiwillig erklärte Einwilligung (Art. 6 Abs. 1 S. 1 lit. a DSGVO). Die von den Social-Media-Anbietern initiierten Prozesse, die Sie durch Ihre Einwilligung bei uns und damit die Aktivierung des jeweiligen Dienstes anstoßen, beruhen ggf. auf abweichenden oder auch gleichen Rechtsgrundlagen, die von den Betreibern anzugeben sind (zB. Einwilligung im Sinne des Art. 6 Abs. 1 S. 1 lit. a DGSVO oder des § 25 Abs. 1 TDDDG).
Die unmittelbar von uns über die jeweilige Social-Media-Plattform erfassten Daten werden von unseren Systemen gelöscht, sobald der Zweck für die Speicherung entfällt, Sie uns zur Löschung auffordern oder Ihre Einwilligung in die Speicherung widerrufen. Aufbewahrungsfristen aufgrund zwingender gesetzlicher Bestimmungen bleiben davon unberührt. Auf die Speicherdauer Ihrer Daten, die von den Betreibern der Social-Media-Präsenz zu eigenen Zwecken gespeichert werden, haben wir keinen Einfluss. Hier gelten, wie Sie bereits wissen, die eigenen Datenschutzbestimmungen der jeweiligen Anbieter, auch wenn wir dort mit unseren Marken Informationen verbreiten und Präsenzen unterhalten.
Unter den nachfolgenden Links können Sie sich über die Datenschutzbestimmungen der einzelnen Social-Media-Anbieter informieren:
Bluesky: Die »Embed«-Funktion von Bluesky ermöglicht es, textfokussierte Inhalte zu integrieren. Zu den Datenschutzhinweisen von Bluesky: https://bsky.social/about/support/privacy-policy 
Facebook: Über das Facebook-Plugin integrieren wir Facebook-Inhalte in das Internetangebot. Zu den Datenschutzhinweisen von Facebook:https://www.facebook.com/privacy/explanation   
Giphy: Die »Embed« Funktion von Giphy ermöglicht die Einbindung von GIFs auf unseren Webseiten. Zu den Datenschutzhinweisen von Giphy:https://giphy.com/privacy   
Imgur: Mit Imgur können wir Imgur-Inhalte, wie Bilder, integrieren. Zu den Datenschutzhinweisen von Imgur:https://imgur.com/privacy   
Instagram: Die »Embed«-Funktion von Instagram ermöglicht, Bilder und Videos von Instagram in unser Angebot zu integrieren. Zu den Datenschutzhinweisen von Instagram:https://www.instagram.com/legal/privacy/   
Mapbox: Mit Mapbox integrieren wir Karten. Zu den Datenschutzhinweisen:https://www.mapbox.com/legal/privacy/   
Reddit: Die »Embed«-Funktion von Reddit ermöglicht, Texte, Bilder und Videos von Instagram in unser Angebot zu integrieren. Zu den Datenschutzhinweisen von Reddit: https://www.reddit.com/policies/privacy-policy 
Spotify: Mit Spotify integrieren wir Lieder, Alben oder auch Playlists in unsere Webseiten. Zu den Datenschutzhinweisen von Spotify:https://www.spotify.com/de/legal/privacy-policy/   
TikTok: Mit dem TikTok-Plugin integrieren wir Videos aus TikTok. Zu den Datenschutzhinweisen von TikTok:https://www.tiktok.com/legal/privacy-policy?lang=de   
Vimeo: Mit dem Vimeo-Player integrieren wir Videos oder Videos anderer Anbieter in das Internetangebot. Zu den Datenschutzhinweisen von Vimeo: https://vimeo.com/privacy 
X: Über das Tweet-Plugin integrieren wir X-Inhalte in das Internetangebot. Zu den Datenschutzhinweisen von X:https://twitter.com/de/privacy   
YouTube: Mit dem YouTube-Player integrieren wir Videos aus seinen YouTube-Channels oder Videos anderer Anbieter in das Internetangebot. Zu den Datenschutzhinweisen von Google: https://support.google.com/youtube/answer/2801895?hl=de   
Moderation von Beiträgen auf Facebook, Instagram und Youtube
Wir verarbeiten im Rahmen der Moderation von Kommentaren und Beiträgen auf Facebook, Instagram und YouTube stichprobenartig Ihre Nutzer-ID, den jeweiligen Kommentartext, sowie Datum und Uhrzeit des Kommentars. Die Verarbeitung dient der Überprüfung der Einhaltung vorgegebener Regeln zu Kommentarinhalten, dem Umgang mit anderen Nutzer:innen, dem Auffinden möglicherweise strafrechtlich relevanter Aussagen und somit auch dem Schutz unserer Rechte. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. f DSGVO. Wir setzen für diese Zwecke das Tool »Conversario« (ferret go GmbH, Brauerstraße 14, 16321 Bernau bei Berlin) auf Grundlage eines Auftragsverarbeitungsvertrages gem. Art. 28 Abs. 3 DSGVO ein.
Die Daten werden umgehend gelöscht, soweit keine Verstöße festgestellt werden. Andernfalls werden sie an die entsprechenden Stellen (an die Strafverfolgungsbehörden oder den jeweiligen Social-Media-Anbieter) weitergegeben.
Bereich
9. Verlagsspezifische Verarbeitungen
aufklappen
A) spiegel.de
SPIEGEL Debatte
Wenn Sie sich auf spiegel.de mit einem Nutzerkonto registriert und ein SPIEGEL Abonnement (mit Ausnahme Starter-Abo) abgeschlossen haben, können Sie sich mit Beiträgen an unseren Debattenformaten beteiligen. Wir verarbeiten zu diesem Zweck Ihren Namen, Ihre E-Mail-Adresse, Ihre Abodaten und Ihre Nutzer-ID für SPIEGEL Debatte (Profildaten), sowie einen von Ihnen frei wählbaren Vor- und Nachnamen, der ein Pseudonym darstellen darf und Ihrem tatsächlichen Namen nicht entsprechen muss, so dass Ihre Identität bei Veröffentlichung von Beiträgen nicht preisgegeben werden muss. Wir verarbeiten weiterhin in Ihrem Debattenprofil die Anzahl Ihrer Beiträge, erhaltene Stimmen, Vorschläge und auf Basis Ihrer Beteiligung ermittelte Interessen. Die Rechtsgrundlage ist Art. 6 Abs.1 S. 1 lit. b DSGVO, § 25 Abs. 2 Nr. 2 TDDDG iVm. den Besonderen Nutzungsbedingungen. Sie können optional einen Avatar wählen und Ihr Profil näher beschreiben. Diese Angaben basieren auf Ihrer freiwillig erklärten Einwilligung, die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. a DSGVO, § 25 Abs. 1 TDDDG. Wenn Sie uns (unabhängig vom Bestehen eines Nutzerkontos) auffällige Beiträge über unser Formular melden, sind wir gesetzlich verpflichtet, Ihren Vor- und Nachnamen, Ihre E-Mail-Adresse, den Grund der Meldung und eine Beschreibung Ihrer Meldung sowie Angaben zum gemeldeten Beitrag zu verarbeiten. Rechtsgrundlage dafür ist Art. 6 Abs. 1 S. 1 lit. c DSGVO i.V.m. Art. 16 Abs. 2 Digital Services Act (DSA). Zur Umsetzung der Moderation und Verwaltung Ihrer Beiträge und Meldungen setzen wir das Tool »Logora« (Société Logora) ein. Wir haben mit Logora für diese Zwecke einen Auftragsverarbeitungsvertrag nach Art. 28 Abs. 3 DSGVO geschlossen.
Darüber hinaus analysieren wir ggf. Ihre Beiträge, um die Einhaltung unserer Community-Richtlinien, Besonderen Nutzungsbedingungen und einschlägige rechtliche Vorschriften überprüfen zu können. Wir verarbeiten dafür ebenfalls Ihre Profildaten und Ihre Beiträge. Die Prüfung erfolgt in der Regel anhand formaler Kriterien (z.B. über Filterfunktionen und Schlagworten zu hate speech, explicit content). Die Rechtsgrundlage ist unser berechtigtes Interesse gem. Art. 6 Abs. 1 S. 1 lit. f DSGVO. Für diese Beitragsanalyse nutzen wir das Moderationstool »Perspective«.
Tägliches Quiz
Als registrierter Nutzer haben Sie die Möglichkeit, an unserem täglichen Quizz teilzunehmen. Wir verarbeiten zu diesem Zweck über das Setzen eines Cookies und dessen ID (damit zusammenhängende Verarbeitung von Metadaten) die Zuordnung von Klicks zu Ihnen als Nutzer. Sie erhalten dadurch Zugriff auf die Auswertung, an welchen Quizzen Sie teilgenommen haben, sowie welche und wie viele Fragen Sie richtig bzw. falsch beantworten konnten. Darüber hinaus aggregieren wir die Daten aller Quizteilnahmen unserer Nutzer:innen und verarbeiten diese anonymisiert, um Ihnen den Vergleich Ihres Quizergebnisses mit denen anderer Teilnehmenden zu ermöglichen. Die Rechtsgrundlage ist unser berechtigtes Interesse, Ihnen ein möglichst breites Nutzungsangebot zur Verfügung zu stellen und damit Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG.
Gaming
Wir bieten Ihnen auf unserer Webseite die Möglichkeit eines breiten Gaming-Angebots mit den Spielen »Wortsuche, Viererkette, Paarsuche, Wabenrätsel, Wordle, Kreuzworträtsel, Solitär, Mini-Sudoku und Sudoku«. Um Ihnen ein redaktionell breites Angebot zur Verfügung zu stellen, verarbeiten wir in diesem Zusammenhang über das Setzen eines Cookies Metadaten und eine gehashte (pseudonymisierte) ID für die Abbildung der Spielstände, wodurch Sie die Möglichkeit haben, jederzeit zu einem Spiel mit dem von Ihnen verlassenen Stand zurückzukehren, und das Spiel fortzusetzen. Die Rechtsgrundlage ist unser berechtigtes Interesse, Ihnen eine möglichst nutzerfreundliche Spieleerfahrung zu ermöglichen (Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG). Das Cookie wird nach Ablauf von 3 Monaten nach Ihrer letzten Aktivität im Bereich des Spieleangebots gelöscht. Für die Zurverfügungstellung des Spiele-Angebots arbeiten wir mit »Heine« (presse service Stefan Heine, Basselweg 103, 22527 Hamburg, Deutschland) zusammen, mit denen wir für diese Zwecke einen Auftragsverarbeitungsvertrag gem. Art. 28 Abs. 2 DSGVO geschlossen haben.
Preisvergleich
Auf unserer Webseite setzen wir Widgets z.B. mit Preisinformationen, Tabellen oder Bildern ein. Wenn Sie als Besucher:in unserer Webseiten eine Internetseite mit einem Widget aufrufen, werden Ihre IP-Adresse, User-Agent-String und Standard-Header an Heise Medien übertragen. Dies ist technisch notwendig, um Anfragen beantworten zu können. Die Widgets dienen u.a. dazu, einen Überblick über die Preise verschiedener Anbieter gewinnen zu können. Zugleich besteht die Möglichkeit, über einen Affiliate-Link direkt auf das Angebot zugreifen zu können. Diese Daten werden maximal 7 Tage gespeichert und danach gelöscht oder anonymisiert, sodass eine Zuordnung des aufrufenden Clients nicht mehr möglich ist. Die Verarbeitung der von Ihnen benötigten Daten erfolgt auf Basis von Art. 6 Abs. 1 lit. f) DSGVO ausschließlich zur Wahrnehmung unseres berechtigten Interesses, insbesondere aus technischen Gründen, zu Zwecken der IT-Sicherheit sowie zur Erfüllung der Nutzerinteressen und zum wirtschaftlichen Betrieb unseres Online-Angebots. Die Widgets werden von »Heise Medien« (Heise Medien GmbH & Co. KG, Karl-Wiechert-Allee 10, 30625 Hannover, Deutschland) zur Verfügung gestellt. Wir haben mit Heise Medien einen Auftragsverarbeitungsvertrag entsprechend der Anforderungen aus Art. 28 Abs. 3 DSGVO geschlossen.
B) manager-magazin.de
Podcasts
Um Ihnen Podcast-Downloads und Wiedergaben zu ermöglichen und statistische Daten, wie z.B. Abrufzahlen zu ermitteln, verarbeiten wir IP-Adressen und Geräteinformationen. Die Rechtsgrundlage ist unser berechtigtes Interesse an einer sicheren und effizienten Bereitstellung der Podcasts, sowie die Analyse und Optimierung unseres Angebots gem. Art. 6 Abs. 1 S. 1 lit. f DSGVO. Wir nutzen den Podcast-Hosting-Dienst des Anbieters »Podigee« (Podigee GmbH, Schlesische Straße 20, 10997 Berlin, Deutschland) für das Laden und Übertragen der Podcasts, mit dem wir für diesen Zweck einen Auftragsverarbeitungsvertrag gem. Art. 28 Abs. 2 DSGVO geschlossen haben. Weiterhin erfolgt die Ausspielung der Podcasts in gemeinsamer datenschutzrechtlicher Verantwortlichkeit mit OMR (Ramp 106 GmbH, Lagerstr. 36, 20357 Hamburg, Deutschland) mit denen wir im Rahmen der FFWD-Kooperation gemeinsam für die Datenverarbeitung Verantwortliche im Sinne des Art. 26 DSGVO sind. In diesem Zusammenhang haben wir und OMR in einer gemeinsamen Vereinbarung die jeweiligen Zuständigkeiten geregelt und festgelegt, wer von uns welche gesetzlichen Pflichten erfüllt. Dies betrifft insbesondere die Wahrnehmung der Rechte der betroffenen Personen und die Erfüllung der Informationspflichten nach den Artikeln 13 und 14 DSGVO. Die datenschutzrechtlichen Verpflichtungen verteilen sich dabei grundsätzlich wie folgt:
· OMR ist zuständig für die technische Abwicklung inkl. Zurverfügungstellung des Podcast-Tools
· Wir sind zuständig für die Erfüllung der datenschutzrechtlichen Informationspflichten und die Ausspielung der Podcasts auf www.manager-magazin.de
Sie erhalten eine Auskunft grds. von derjenigen Stelle, bei der Sie Ihre Rechte geltend gemacht haben, solange die Datenverarbeitung im Rahmen der gemeinsamen Verantwortlichkeit erfolgt. In allen anderen Fällen erhalten Sie die Auskunft selbstverständlich von uns.
LinkedIn Retargeting
Ergänzend zu den unter Ziffer 5. C. beschriebenen Direktpartnerschaften im Bereich Remarketing/Retargeting setzen wir explizit auf www.manager-magazin.de die Retargeting-Funktion von LinkedIn (LinkedIn Ireland Unlimited Company Wilton Place, Dublin 2, Irland) ein, um Ihnen personalisierte Werbung anzuzeigen. Diese Funktion ermöglicht es uns, Nutzer:innen, die unsere Webseite besucht haben, gezielte Anzeigen auf LinkedIn zu präsentieren. Nähere Informationen zur Verarbeitung erhalten Sie unter https://de.linkedin.com/legal/privacy/eu?.  Ihre LinkedIn-Einstellungen zu personalisierten Anzeigen können Sie unter https://www.linkedin.com/mypreferences/d/categories/ads  jederzeit anpassen.
C) 11Freunde
Sovendus
Wir bieten einige unserer Produkte über Werbepartner im Internet zum Kauf an. Für diesen Zweck verarbeiten wir werbepartnerspezifische ID`s, den Zeitstempel der Bestellung und Ihre IP-Adresse, um eine korrekte Abrechnung zu gewährleisten. Die Rechtsgrundlage ist Ihre Einwilligung, Art. 6 Abs. 1 S. 1 lit. a DSGVO. Wir arbeiten zu diesem Zweck mit dem Dienstleister »Sovendus« (Sovendus GmbH, c/o Design Offices Karlsruhe Bahnhofplatz, Bahnhofplatz 12, 76137 Karlsruhe, Deutschland) zusammen.
Games
Für die Bereitstellung unseres Spieleangebots (Online-Spiel: ´Box to Box`) und die zeitweise Sicherung von Spielständen verarbeiten wir Ihre IP-Adresse. Diese Datenverarbeitung ist für den Aufruf und die Verwendung des Spiels erforderlich. Die Rechtsgrundlage ist unser berechtigtes Interesse, Ihnen eine möglichst nutzerfreundliche Spieleerfahrung zu ermöglichen (Art. 6 Abs. 1 S. 1 lit. f DSGVO, § 25 Abs. 2 Nr. 2 TDDDG). Für die Zurverfügungstellung des Spiele-Angebots arbeiten wir mit »Spotever« (Spotever UG, Am Ruhrstein 45, 45133 Essen, Deutschland) zusammen, mit denen wir für diese Zwecke einen Auftragsverarbeitungsvertrag gem. Art. 28 Abs. 2 DSGVO geschlossen haben.
Bereich
10. Ihre Rechte
aufklappen
Auskunftsrecht (Art. 15 DSGVO)
Sie haben uns gegenüber das Recht, Auskunft darüber zu erhalten, welche Daten wir zu Ihrer Person verarbeiten.
Recht auf Berichtigung (Art. 16 DSGVO)
Sollten die Sie betreffenden Daten nicht richtig oder unvollständig sein, so können Sie die Berichtigung unrichtiger oder die Vervollständigung unvollständiger Angaben verlangen.
Recht auf Löschung (Art. 17 DSGVO)
Unter den Voraussetzungen des Art. 17 DSGVO können Sie die Löschung Ihrer personenbezogenen Daten verlangen. Ihr Anspruch auf Löschung hängt u. a. davon ab, ob die Sie betreffenden Daten von uns zur Erfüllung unserer gesetzlichen oder vertraglichen Aufgaben noch benötigt werden.
Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO)
Unter den Voraussetzungen des Art.18 DSGVO können Sie die Einschränkung der Verarbeitung der Sie betreffenden personenbezogenen Daten verlangen.
Widerspruchsrecht (Art. 21 DSGVO)
Aus Gründen, die sich aus Ihrer besonderen Situation ergeben, können Sie jederzeit gegen die Verarbeitung der Sie betreffenden Daten Widerspruch einlegen.
Recht auf Widerruf der Einwilligung (Art. 7 Abs. 3 DSGVO)
Sie haben das Recht, eine erteilte Einwilligung zur Verarbeitung Ihrer personenbezogenen Daten jederzeit zu widerrufen. Durch den Widerruf der Einwilligung wird die Rechtmäßigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung nicht berührt.
Sie können sich unter: SPIEGEL-Gruppe, z. Hd. der Datenschutzbeauftragten, Ericusspitze 1, 20457 Hamburg oder per E-Mail: dsb@spiegelgruppe.de  jederzeit unmittelbar an die betriebliche Datenschutzbeauftragte der Gruppe wenden.
Recht auf Beschwerde bei einer Aufsichtsbehörde (Art. 77 DSGVO)
Sie haben das Recht, sich bei einer Aufsichtsbehörde Ihrer Wahl zu beschweren. Unmittelbar für die SPIEGEL-Gruppe zuständig ist der Hamburgische Beauftragte für Datenschutz und Informationsfreiheit (HmbBfDI).
Kontakt:
Der Hamburgische Beauftragte für Datenschutz und Informationsfreiheit
Ludwig-Ehrhard-Str. 22, 7. OG
20459 Hamburg
Tel.: 040/428544040
E-Mail: mailbox@datenschutz.hamburg.de 
Bereich
11. Anforderungen des Data Act
aufklappen
Im Rahmen der Nutzung unserer Produkte und Dienste werden bestimmte Daten automatisch durch die Webseite generiert, die Sie verwenden. Diese sogenannten maschinell generierten Daten können Informationen über die Funktionalität und Nutzung unserer Produkte enthalten und fallen unter die Regelungen des Data Act (Verordnung 2022/868). Mit dieser Information möchten wir Ihnen als Nutzer:in gegenüber Transparenz zeigen und Ihnen die gesetzlich verlangte Kontrolle über diese Daten ermöglichen.
A. Art und Nutzung der erfassten Daten
Wir erfassen und verarbeiten maschinell generierte Daten, die durch die Nutzung unserer Produkte entstehen. Dazu gehören die nachfolgend genannten Datenarten:
Nutzungsdaten: personenbeziehbare Merkmale (Adobe »Data Feed«, der browserbezogene [die mit einer Cookie-ID vergleichbare Experience Cloud Visitor-ID (EC-ID), mit deren Hilfe der Browser Ihres Endgeräts identifiziert werden kann] und nutzerkontobezogene (SSO-ID) Identifizierungsmerkmale enthält, über die Surfverhalten und Navigation auf unserer Seite nachvollziehbar werden).
Diese maschinell generierten Daten werden ausschließlich für die folgenden Zwecke verwendet:
- Zur Ermittlung statistischer Kennwerte über die Nutzung unseres Digital-Angebots, um die Anzahl der Besuche auf unserer Webseite, die Anzahl der Webseitenbesucher und deren Surfverhalten auf Basis eines einheitlichen Standardverfahrens zu bestimmen und somit marktweit vergleichbare Werte zu erhalten. Wir verarbeiten die Nutzungsdaten zur Sicherstellung des Betriebs, für die Bewertung der Relevanz von Inhalten bei der redaktionellen Arbeit, um die Nutzung unserer Webseiten und Angebote zu untersuchen, um einzelne Funktionen und Angebote sowie das Nutzungserlebnis fortlaufend optimieren zu können und zur Identifikation fehlerhafter Nutzungspfade.
- Für die Analyse der Nutzung unseres Angebotes und die gezielte Ausspielung redaktioneller Inhalte an ausgewählten Stellen auf Basis Ihres persönlichen Leseverhaltens
- Für die persönliche Zugangssicherung, Missbrauchsprävention und -aufklärung
- Für die profilbezogene Ansprache (Scores)
Detaillierte Informationen zu den Verarbeitungszwecken finden Sie bei Bedarf auch in unseren Datenschutzhinweisen unter der jeweiligen Überschrift.
B. Rechte der Nutzer:innen
Entsprechend der Regelungen des Data Act haben Sie folgende Rechte hinsichtlich der maschinell generierten Daten, die durch die Nutzung unserer Produkte entstehen:
- Zugriffsrecht: Sie können eine Kopie der Daten anfordern.
- Übertragbarkeit: Sie haben das Recht, diese Daten in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten, um sie mit anderen Diensten oder Anbietern zu teilen.
- Datenweitergabe: Sie können verlangen, dass wir ihre Daten direkt mit einem Drittanbieter teilen, sofern dies technisch möglich ist.
Für Fragen zur Verarbeitung Ihrer Nutzungsdaten oder zur Wahrnehmung Ihrer Rechte können Sie uns jederzeit unter folgender E-Mail-Adresse kontaktieren:
datenschutz@spiegelgruppe.de 
Weiterführende Informationen
Allgemeine Nutzungsbedingungen
Besondere Nutzungsbedingungen für Ihre Beiträge
Versionsnummer: 7.1
Feedback
+161 -146
View File
@@ -2,170 +2,185 @@
**URL:** https://www.spiegel.de **URL:** https://www.spiegel.de
**Typ:** Medien / Nachrichtenportal **Typ:** Medien / Nachrichtenportal
**Datum:** 2026-05-13 (verifiziert gegen Live-Texte) **Datum:** 2026-05-14 (verifiziert gegen Live-Texte + System-Ergebnis)
**Vorheriger Batch-Test:** 6/9 L1, 10/13 L2 — VERALTET, mehrere False Negatives **Volltext:** [06-spiegel-dsi-fulltext.txt](06-spiegel-dsi-fulltext.txt) (13.698 Woerter, 107.720 Zeichen)
--- ---
## Business Profile (erwartet) ## Business Profile (erwartet vs tatsaechlich)
| Feld | Erwarteter Wert | Begruendung | | Feld | Erwartet | System-Ergebnis | |
|------|----------------|-------------| |------|---------|----------------|---|
| business_type | b2c | Abo-Modell (Spiegel+) | | business_type | b2c | B2C | ✓ |
| industry | media | Nachrichtenportal | | industry | media | media | ✓ |
| has_online_shop | true | Spiegel+ Abo-Shop | | has_online_shop | true | true | ✓ |
| has_editorial_content | true | Kerngeschaeft | | has_editorial_content | true | true | ✓ |
| is_regulated_profession | **false** | Kein regulierter Beruf. "Anwalt" im Text ist Redaktionsanwalt, kein Kanzlei-Beruf | | is_regulated_profession | false | false | ✓ (gefixt, war FP "anwalt") |
| needs_odr | true | B2C mit Online-Abo | | needs_odr | true | true | ✓ |
| detected_services | 31 | 10 angezeigt (31 intern) | UI zeigt nur Top 10 |
**Bug:** Profiler erkennt "anwalt" im Impressum-Text und setzt is_regulated_profession=true. FALSE POSITIVE.
--- ---
## Dokumente ## Dokumente
| Dokumenttyp | Vorhanden | URL | Anmerkung | | Dokumenttyp | Vorhanden | URL | System-Ergebnis |
|-------------|-----------|-----|-----------| |-------------|-----------|-----|----------------|
| DSI | Ja | https://www.spiegel.de/datenschutz-spiegel | 6461 Woerter, 11 Abschnitte, sehr ausfuehrlich | | DSI | Ja | https://www.spiegel.de/datenschutz-spiegel | **9/9 L1 (100%)** |
| Impressum | Ja | https://www.spiegel.de/impressum | 2 Gesellschaften (DER SPIEGEL GmbH + SPIEGEL-Verlag) | | Impressum | Ja | https://www.spiegel.de/impressum | **9/13 L1 (86%)** |
| Cookie-Richtlinie | In DSI Abschnitt 4 | #funktionsfaehigkeitdesangebots | Sourcepoint CMP | | Social Media | In DSI (Abschnitt 8) | auto-filled | **10/10 L1 (100%)** |
| AGB | Ja | https://www.spiegel.de/agb | Abo-Bedingungen | | Cookie-RL | In DSI (Abschnitt 4) | auto-filled | 1/6 L1 (17%) |
| Nutzungsbedingungen | Ja | https://www.spiegel.de/nutzungsbedingungen | Separates Dokument | | AGB | Ja | https://www.spiegel.de/agb | Nicht eingegeben |
| Widerruf | In AGB Abschnitt 10 | https://www.spiegel.de/agb | "Widerrufsrecht fuer Abonnements" | | Nutzungsbedingungen | Ja | https://www.spiegel.de/nutzungsbedingungen | 5/12 L1 (42%) |
| Social Media DSE | In DSI Abschnitt 8 | #einbinden-von-drittinhalten | Facebook, YouTube, X, Instagram, TikTok, etc. | | Widerruf | In AGB §10 | Falsch zugewiesen (NB-Text) | 0/8 L1 (0%) |
| DSB-Kontakt | In DSI | — | dsb@spiegelgruppe.de | | DSB-Kontakt | In DSI | auto-filled | **9/9 L1 (100%)** |
--- ---
## Erwartete Ergebnisse: DSI (Art. 13 DSGVO) ## DSI (Art. 13 DSGVO) — 9/9 L1, 24/42 L2
### L1 Checks (ERWARTET: 9/9 PASS) ### L1 Checks (9/9 PASS)
| Check | Erwartet | Beleg | Unser Ergebnis | Bug? | | Check | Erwartet | System | Beleg |
|-------|----------|-------|----------------|------| |-------|----------|--------|-------|
| Verantwortlicher | PASS | "DER SPIEGEL GmbH & Co. KG, Ericusspitze 1, 20459 Hamburg" | PASS (3/3) | — | | Verantwortlicher | PASS | PASS (3/3) | Ericusspitze 1, 20459 Hamburg |
| DSB | **PASS** | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" | **FAIL** | **FN — Regex matcht "Datenschutzbeauftragte" nicht ohne "r" am Ende oder erkennt Kontext nicht** | | DSB | PASS | PASS (1/1) | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" |
| Zwecke | PASS | Adobe-Tracking, Vertragsbeziehungen, Drittinhalte etc. | PASS | — | | Zwecke | PASS | PASS (1/1) | Adobe-Tracking, Vertragsbeziehungen etc. |
| Rechtsgrundlage | PASS | Art. 6(1)(a), (b), (f) explizit | PASS (3/4) | — | | Rechtsgrundlage | PASS | PASS (3/4) | Art. 6(1)(a), (b), (f) |
| Empfaenger | PASS | Server-/Applikationsbetreiber, Auftragsverarbeiter | PASS (2/2) | | | Empfaenger | PASS | PASS (2/2) | AVV erwaehnt |
| Drittlandtransfer | PASS | SCC erwaehnt | PASS (1/1) | | | Drittlandtransfer | PASS | PASS (1/1) | SCC erwaehnt |
| Speicherdauer | PASS | "30 Tage" Protokolldatei | PASS (1/2) | — | | Speicherdauer | PASS | PASS (2/2) | "30 Tage", Loeschfristen |
| Betroffenenrechte | **PASS** | Art. 15, 16, 17, 18, 21 explizit. Art. 20 fehlt. | **FAIL** | **FN — Regex verlangt alle 6 Artikel, 5/6 genuegen nicht** | | Betroffenenrechte | PASS | PASS (6/7) | Art. 15-18, 20, 21. Art. 22 fehlt (TP) |
| Beschwerderecht | **PASS** | "Art. 77 DSGVO... HmbBfDI... Ludwig-Ehrhard-Str. 22" | **FAIL** | **FN — Regex findet Art. 77 + HmbBfDI nicht** | | Beschwerderecht | PASS | PASS (1/1) | HmbBfDI, Art. 77 |
**3 False Negatives in L1!** DSB, Betroffenenrechte, Beschwerderecht sind alle vorhanden. ### L2 True Positives (korrekte Findings)
### L2 Checks (Stichproben) | Check | Status | Begruendung |
| Check | Erwartet | Beleg | Unser Ergebnis | Bug? |
|-------|----------|-------|----------------|------|
| E-Mail | PASS | datenschutz@spiegelgruppe.de | PASS | — |
| Interessenabwaegung | FAIL (TP) | Interesse benannt, keine Abwaegung | FAIL | Korrekt |
| Art. 20 Portabilitaet | FAIL (TP) | Art. 20 fehlt im Rechte-Abschnitt | — | Korrekter Finding |
| Loeschkonzept | FAIL (TP) | Kein formales Loeschkonzept | FAIL | Korrekt |
---
## Erwartete Ergebnisse: Impressum (§5 TMG)
| Check | Erwartet | Beleg | Unser Ergebnis | Bug? |
|-------|----------|-------|----------------|------|
| Firmenname | PASS | DER SPIEGEL GmbH & Co. KG + SPIEGEL-Verlag | PASS | — |
| Anschrift | PASS | Ericusspitze 1, 20457 Hamburg | PASS | — |
| Kontakt | PASS | Tel. 040 3007-0, spiegel@spiegel.de | PASS | — |
| Register | PASS | HRA 123 261 + HRA 61 755 | PASS | — |
| USt-IdNr | **PASS** | DE 212 442 423 + DE 118 922 410 | **FAIL** | **FN — Regex findet "Umsatzsteuer-ID:" Format nicht** |
| Vertretung | PASS | Thomas Hass (Geschaeftsfuehrung) | PASS (1/1) | — |
| V.i.S.d.P. | **PASS** | "Verantwortlicher i. S. v. § 18 Abs. 2 MStV: Dirk Kurbjuweit" | **FAIL** | **FN — Regex sucht "v.i.s.d.p." nicht "verantwortlicher i.s.v."** |
| Streitbeilegung | PASS | ODR-Link vorhanden (in AGB) | PASS | — |
| Berufsrecht | **SKIP** | Spiegel ist kein regulierter Beruf | **AKTIV (1/3)** | **FP — Profiler "anwalt" Bug** |
---
## Erwartete Ergebnisse: AGB
| Check | Erwartet | Beleg |
|-------|----------|-------|
| Geltungsbereich | PASS | Abschnitt 1 |
| Vertragsschluss | PASS | Abschnitt 2 |
| Preise/Zahlung | PASS | Abschnitte 4-7 |
| Kuendigung | PASS | Abschnitt 8 (1 Monat Frist) |
| Widerrufsrecht | PASS | Abschnitt 10 (14 Tage, Muster-Formular) |
| §312k Button | Zu pruefen | Kuendigungsbutton Pflicht seit 01.07.2022 |
| ODR-Link | PASS | http://ec.europa.eu/consumers/odr/ |
---
## Erwartete Ergebnisse: Widerrufsbelehrung (AGB §10)
| Check | Erwartet | Beleg |
|-------|----------|-------|
| Belehrung | PASS | "Sie haben das Recht, Abonnementvertraege binnen 14 Tagen ohne Angabe von Gruenden zu widerrufen" |
| 14-Tage-Frist | PASS | Explizit genannt |
| Form | PASS | Brief, E-Mail, Fax |
| Muster-Formular | PASS | "beigefuegte Muster-Widerrufsformular" erwaehnt |
| Folgen | PASS | Rueckerstattungsregeln beschrieben |
| Empfaenger | PASS | DER SPIEGEL Abonnentenservice, 20637 Hamburg; aboservice@spiegel.de |
| Ausnahme digitale Inhalte | PASS | "Fuer sofort nutzbare Zeitzugaenge... kein Widerrufsrecht" |
**Problem:** Unser Check prueft den DSI-Volltext gegen Widerruf-Checklist statt die AGB. Der Widerruf steht in den AGB (§10), nicht in der DSI.
---
## Erwartete Ergebnisse: Social Media (DSI Abschnitt 8)
| Check | Erwartet | Beleg |
|-------|----------|-------|
| Gemeinsam Verantwortliche | PASS | Erwaehnt |
| Meta konkret benannt | FAIL (TP) | Nur "Facebook" ohne "Meta Platforms Ireland Ltd." |
| Vereinbarung Art. 26 | FAIL (TP) | Kein Page Controller Addendum |
| Plattformen | PASS | Facebook, YouTube, X, Instagram, TikTok, Vimeo, Reddit, Bluesky, etc. |
| SCC | PASS | Erwaehnt |
| DPF | FAIL (TP) | Data Privacy Framework nicht erwaehnt |
| Rechtsgrundlage | PASS | Art. 6(1)(f) |
| Alle standardmaessig deaktiviert | PASS | "standardmaessig deaktiviert" |
---
## Banner-Check
| Feld | Erwartet |
|------|----------|
| banner_detected | true |
| provider | Sourcepoint |
| tcf_enabled | true |
| Vendor-Anzahl | 40+ (grosses Medienunternehmen) |
| violations | Consent-Wall blockiert Zugang → moeglicherweise unzulaessig |
---
## Cross-Check Banner vs DSI
| Finding | Erwartet |
|---------|----------|
| Vendors fehlen in DSI | Wahrscheinlich — viele TCF-Vendors nicht in DSI dokumentiert |
| Tracking vor Consent | Unwahrscheinlich (Sourcepoint blockiert gut) |
---
## Kontext-Filter
| Check | Filter | Begruendung |
|-------|--------|-------------| |-------|--------|-------------|
| ODR | AKTIV | B2C Online-Abo | | Interessenabwaegung | FAIL (TP) | Interesse benannt, keine Abwaegung dokumentiert |
| Widerruf | AKTIV | B2C | | Art. 22 Profiling | FAIL (TP) | Nicht erwaehnt trotz personalisierter Werbung |
| V.i.S.d.P. | AKTIV | Medienunternehmen (Kernpflicht) |
| Berufsrecht | **SKIP** | Kein regulierter Beruf |
--- ---
## Identifizierte Regex-Bugs (aus diesem GT-Abgleich) ## Impressum — 9/13 L1, 9/31 L2
| # | Check | Bug | Beleg auf Website | Regex-Problem | | Check | Erwartet | System | |
|---|-------|-----|-------------------|---------------| |-------|----------|--------|---|
| 1 | DSB | FN | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" | Regex matcht "Datenschutzbeauftragten" (Genitiv/Dativ) nicht | | Firmenname | PASS | PASS | ✓ |
| 2 | Beschwerderecht | FN | "Art. 77 DSGVO... HmbBfDI" | Regex findet "Art. 77" oder "Aufsichtsbehoerde" nicht im Spiegel-Text | | Anschrift | PASS | PASS (2/2) | ✓ |
| 3 | Betroffenenrechte | FN | Art. 15, 16, 17, 18, 21 — nur Art. 20 fehlt | Regex verlangt ALLE 6, 5/6 ist nicht genug | | Kontakt | PASS | PASS (2/2) | ✓ |
| 4 | V.i.S.d.P. | FN | "Verantwortlicher i. S. v. § 18 Abs. 2 MStV" | Regex sucht nur "v.i.s.d.p.", nicht die MStV-Formulierung | | Register | PASS | PASS (2/2) | ✓ |
| 5 | USt-IdNr | FN | "Umsatzsteuer-ID: DE 212 442 423" | Regex sucht "ust-idnr" oder "ust-id", matcht "umsatzsteuer-id:" nicht | | USt-IdNr | PASS | PASS (1/1) | ✓ Gefixt ("Umsatzsteuer-ID:" + DE mit Leerzeichen) |
| 6 | Profiler "anwalt" | FP | Redaktionsanwalt im Impressum | "anwalt" zu generisch, matcht Personennamen/Rollen | | Vertretung | PASS | PASS (1/1) | ✓ |
| V.i.S.d.P. | PASS | PASS | ✓ Gefixt ("Verantwortlicher i.S.v. §18 MStV") |
| Streitbeilegung | PASS | PASS | ✓ |
| Berufsrecht | SKIP | PASS (1/3) | FP — "Berufsrechtliche Regelungen" matcht falsch |
---
## Social Media — 10/10 L1, 12/30 L2
| Check | Erwartet | System | |
|-------|----------|--------|---|
| Gemeinsam Verantwortliche | PASS | PASS | ✓ |
| Meta benannt | PASS | PASS | ✓ "Meta Platforms Inc" erkannt |
| Vereinbarung Art. 26 | PASS | PASS (1/2) | ✓ Seiteninsights erwaehnt |
| Anlaufstelle | PASS | PASS (1/1) | ✓ |
| Plattformen | PASS | PASS (1/1) | ✓ |
| Drittlandtransfer | PASS | PASS (2/2) | ✓ SCC + DPF |
| Rechtsgrundlage | PASS | PASS (1/1) | ✓ |
| Betroffenenrechte | PASS | PASS (1/1) | ✓ Opt-Out erwaehnt |
| Social Bookmarks | PASS | PASS | ✓ |
### L2 True Positives
| Check | Status | Begruendung |
|-------|--------|-------------|
| Page Controller Addendum | FAIL (TP) | Nicht verlinkt |
| 2-Klick-Loesung | FAIL (TP) | Nicht dokumentiert |
---
## Cookie-Richtlinie — 1/6 L1
Cookie-Infos stehen bei Spiegel im **Sourcepoint-Banner** und in DSI Abschnitt 4, nicht als eigenes Dokument. Section-Splitter hat einen kurzen Cookie-Abschnitt extrahiert, aber die meisten Checks scheitern weil die Details im Banner stehen (nicht im Text).
---
## Nutzungsbedingungen — 5/12 L1
Aus spiegel.de/nutzungsbedingungen extrahiert (1679 Woerter). Echte Luecken bei Einbeziehungsklausel, ODR-Link, Kuendigung, Zahlungsarten.
---
## Widerrufsbelehrung — 0/8 L1
**Problem:** System prueft Nutzungsbedingungen-Text (1679w) statt AGB-Text.
**Tatsaechlich:** Widerrufsbelehrung steht in AGB §10 (spiegel.de/agb):
- 14-Tage-Frist ✓
- Muster-Widerrufsformular ✓
- Empfaenger (DER SPIEGEL Abonnentenservice) ✓
- Ausnahme digitale Inhalte ✓
**Offener Punkt:** Cross-Document Intelligence — System muss erkennen dass der Text keine Widerrufsbelehrung ist und den AGB-Link vorschlagen.
---
## Erkannte Dienste (31/32 = 97%)
| Dienst | Kategorie | Land | EU | In DSI erwaehnt |
|--------|----------|------|----|----------------|
| Adobe | tracking | US | Nein | Ja |
| Bluesky | social | US | Nein | Ja |
| Facebook | social | US | Nein | Ja |
| Giphy | content | US | Nein | Ja |
| Google Ads | marketing | US | Nein | Ja |
| Google reCAPTCHA | security | US | Nein | Ja |
| ID5 | identity | GB | Ja | Ja |
| IQD | marketing | DE | Ja | Ja |
| Imgur | content | US | Nein | Ja |
| Instagram | social | US | Nein | Ja |
| JW Player | video | US | Nein | Ja |
| LinkedIn | marketing | US | Nein | Ja |
| Mapbox | maps | US | Nein | Ja |
| Meta Platforms | social | US | Nein | Ja |
| Microsoft | cloud | US | Nein | Ja |
| Omnystudio | audio | CA | Nein | Ja |
| PayPal | payment | US | Nein | Ja |
| Qualtrics | survey | US | Nein | Ja |
| Reddit | social | US | Nein | Ja |
| Salesforce | crm | US | Nein | Ja |
| Segment | tag_manager | US | Nein | Ja |
| Sourcepoint | cmp | US | Nein | Ja |
| Spotify | audio | SE | Ja | Ja |
| Storifyme | content | DE | Ja | Ja |
| TikTok | social | IE | Ja | Ja |
| Utiq | tracking | BE | Ja | Ja |
| Vimeo | video | US | Nein | Ja |
| X/Twitter | social | US | Nein | Ja |
| YouTube | video | US | Nein | Ja |
| Zendesk | chatbot | US | Nein | Ja |
**25 Non-EU Dienste, 6 EU-Dienste.** Alle in DSI erwaehnt (Spiegel dokumentiert seine Dienste gut).
---
## Fixes die in dieser Session angewendet wurden
| # | Bug | Fix | Auswirkung |
|---|-----|-----|-----------|
| 1 | Text-Limit 50k Zeichen | → 200k | DSI: 6461→13698 Woerter, 5 FN weg |
| 2 | USt-IdNr "Umsatzsteuer-ID:" | Regex erweitert | Impressum: +1 PASS |
| 3 | V.i.S.d.P. "i.S.v. §18 MStV" | Regex + Pattern | Impressum: +1 PASS |
| 4 | "anwalt" FP im Profiler | Nur Impressum[:500], nur "rechtsanwalt" | Profiler: FP weg |
| 5 | Service-Erkennung 20→118 | service_detector.py | 5→31 Dienste erkannt |
| 6 | Section-Splitter auto-fill | auto_fill_from_dsi() | Cookie+Social Media auto-gefuellt |
---
## Offene Punkte
1. **Widerruf falsch zugewiesen** — System braucht Cross-Document Intelligence (AGB-Link finden)
2. **Cookie-RL 1/6** — Cookie-Infos stehen im Banner, nicht im Text → TCF-Vendor-Extraktion wuerde helfen
3. **Dienste UI zeigt nur 10** — 31 erkannt aber Frontend kuerzt
4. **Berufsrecht FP** — "Berufsrechtliche Regelungen + Zugang" matcht falsch im Spiegel-Impressum
5. **Banner-Check nicht sichtbar** — Sourcepoint-Buttons nicht klickbar im Scanner
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""
Batch Ground Truth Test run compliance check on all 10 GT websites.
Usage:
python3 batch_gt_test.py [--backend-url URL]
Calls the compliance-check API for each website's DSI + Impressum URLs,
polls for results, and prints a comparison table.
"""
import argparse
import json
import sys
import time
import httpx
# 10 GT websites with their known document URLs
GT_WEBSITES = [
{
"name": "SafetyKon",
"documents": [
{"doc_type": "dse", "url": "https://safetykon.de/datenschutz"},
{"doc_type": "impressum", "url": "https://safetykon.de/impressum"},
],
},
{
"name": "IHK Konstanz",
"documents": [
{"doc_type": "dse", "url": "https://www.ihk.de/konstanz/servicemarken/ueber-uns/downloads/datenschutzinformationen-zum-internetangebot-4163288"},
{"doc_type": "impressum", "url": "https://www.ihk.de/konstanz/impressum"},
],
},
{
"name": "Stadt Koeln",
"documents": [
{"doc_type": "dse", "url": "https://www.stadt-koeln.de/datenschutz"},
{"doc_type": "impressum", "url": "https://www.stadt-koeln.de/impressum"},
],
},
{
"name": "BMW",
"documents": [
{"doc_type": "dse", "url": "https://www.bmw.de/de/footer/metanavigation/data-privacy.html"},
{"doc_type": "impressum", "url": "https://www.bmw.de/de/footer/metanavigation/legal-notice-pool/imprint.html"},
{"doc_type": "cookie", "url": "https://www.bmw.de/de/footer/footer-section/cookie-policy.html"},
],
},
{
"name": "Sparkasse Bodensee",
"documents": [
{"doc_type": "dse", "url": "https://www.sparkasse-bodensee.de/de/home/toolbar/datenschutz.html"},
{"doc_type": "impressum", "url": "https://www.sparkasse-bodensee.de/de/home/toolbar/impressum.html"},
],
},
{
"name": "Spiegel",
"documents": [
{"doc_type": "dse", "url": "https://www.spiegel.de/datenschutz-spiegel"},
{"doc_type": "impressum", "url": "https://www.spiegel.de/impressum"},
{"doc_type": "nutzungsbedingungen", "url": "https://www.spiegel.de/nutzungsbedingungen"},
],
},
{
"name": "TUEV Sued",
"documents": [
{"doc_type": "dse", "url": "https://www.tuvsud.com/de-de/datenschutzerklaerung"},
{"doc_type": "impressum", "url": "https://www.tuvsud.com/de-de/impressum"},
],
},
{
"name": "ETO Gruppe",
"documents": [
{"doc_type": "dse", "url": "https://www.etogruppe.com/datenschutz.html"},
{"doc_type": "impressum", "url": "https://www.etogruppe.com/impressum.html"},
],
},
{
"name": "Caritas",
"documents": [
{"doc_type": "dse", "url": "https://www.caritas.de/datenschutz"},
{"doc_type": "impressum", "url": "https://www.caritas.de/impressum"},
],
},
{
"name": "BfDI",
"documents": [
{"doc_type": "dse", "url": "https://www.bfdi.bund.de/DE/Meta/Datenschutz/datenschutz_node.html"},
{"doc_type": "impressum", "url": "https://www.bfdi.bund.de/DE/Meta/Impressum/impressum_node.html"},
],
},
]
def run_check(backend_url: str, website: dict) -> dict:
"""Submit compliance check and poll for results."""
with httpx.Client(timeout=30.0, verify=False) as client:
# Start check
resp = client.post(
f"{backend_url}/api/compliance/agent/compliance-check",
json={
"documents": website["documents"],
"use_agent": False,
},
)
if resp.status_code != 200:
return {"error": f"Start failed: {resp.status_code}"}
check_id = resp.json().get("check_id")
if not check_id:
return {"error": "No check_id"}
# Poll (max 15 min)
for _ in range(300):
time.sleep(3)
poll = client.get(
f"{backend_url}/api/compliance/agent/compliance-check/{check_id}"
)
if poll.status_code != 200:
continue
data = poll.json()
if data.get("status") == "completed":
return data.get("result", {})
if data.get("status") == "failed":
return {"error": data.get("error", "Check failed")}
return {"error": "Timeout (15 min)"}
def print_results(all_results: list[tuple[str, dict]]):
"""Print comparison table."""
print()
print("=" * 100)
print(f"{'Website':20s} {'Profil':12s} {'DSI L1':10s} {'DSI W':7s} "
f"{'Imp L1':10s} {'Dienste':8s} {'Docs':5s} {'Status':12s}")
print("-" * 100)
for name, result in all_results:
if "error" in result:
print(f"{name:20s} {'ERROR':12s} {result['error'][:60]}")
continue
profile = result.get("business_profile", {})
btype = profile.get("business_type", "?").upper()
industry = profile.get("industry", "?")
services = len(profile.get("detected_services", []))
docs = result.get("results", [])
dsi = next((d for d in docs if d.get("doc_type") == "dse"), {})
imp = next((d for d in docs if d.get("doc_type") == "impressum"), {})
dsi_l1 = f"{dsi.get('completeness_pct', 0)}%"
dsi_w = str(dsi.get("word_count", 0))
imp_l1 = f"{imp.get('completeness_pct', 0)}%"
ok_count = sum(1 for d in docs if d.get("completeness_pct", 0) == 100)
total = len(docs)
print(f"{name:20s} {btype+'/'+industry:12s} {dsi_l1:10s} {dsi_w:7s} "
f"{imp_l1:10s} {services:8d} {ok_count}/{total:<3d} "
f"{'OK' if dsi.get('completeness_pct', 0) == 100 else 'LUECKEN'}")
print("=" * 100)
# Detail: all doc results
print()
for name, result in all_results:
if "error" in result:
continue
docs = result.get("results", [])
print(f"--- {name} ---")
for d in docs:
pct = d.get("completeness_pct", 0)
cpct = d.get("correctness_pct", 0)
dtype = d.get("doc_type", "?")
label = d.get("label", dtype)
wc = d.get("word_count", 0)
scenario = d.get("scenario", "")
checks = d.get("checks", [])
l1_total = len([c for c in checks if c.get("level", 1) == 1])
l1_pass = len([c for c in checks if c.get("level", 1) == 1 and c.get("passed")])
failed = [c["label"] for c in checks if c.get("level", 1) == 1 and not c.get("passed") and not c.get("skipped") and c.get("severity") != "INFO"]
print(f" {label:30s} {l1_pass}/{l1_total} L1 ({pct}%) {wc}w {scenario}")
if failed:
for f in failed[:5]:
print(f"{f[:70]}")
print()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--backend-url", default="https://localhost:8002",
help="Backend compliance URL")
parser.add_argument("--sites", default="all",
help="Comma-separated site indices (1-10) or 'all'")
args = parser.parse_args()
sites = GT_WEBSITES
if args.sites != "all":
indices = [int(i) - 1 for i in args.sites.split(",")]
sites = [GT_WEBSITES[i] for i in indices if 0 <= i < len(GT_WEBSITES)]
print(f"Running compliance check on {len(sites)} websites...")
print(f"Backend: {args.backend_url}")
print()
all_results = []
for i, website in enumerate(sites):
name = website["name"]
print(f"[{i+1}/{len(sites)}] {name}...", end=" ", flush=True)
t0 = time.time()
result = run_check(args.backend_url, website)
elapsed = time.time() - t0
if "error" in result:
print(f"ERROR ({elapsed:.0f}s): {result['error'][:60]}")
else:
docs = result.get("results", [])
ok = sum(1 for d in docs if d.get("completeness_pct", 0) == 100)
print(f"OK ({elapsed:.0f}s) — {len(docs)} docs, {ok} vollstaendig")
all_results.append((name, result))
print_results(all_results)
# Save raw results
out_file = f"batch_results_{time.strftime('%Y%m%d_%H%M%S')}.json"
with open(out_file, "w") as f:
json.dump(
{name: result for name, result in all_results},
f, indent=2, default=str,
)
print(f"\nRaw results saved to {out_file}")
if __name__ == "__main__":
main()