Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf9d8a5ed3 | |||
| d45e08e25f | |||
| 3dbf3aa34a | |||
| 77308b783f | |||
| 9797234ff6 | |||
| 7080eb5f45 | |||
| c93cf2719a | |||
| 7a27dbc01b | |||
| de35dfce18 | |||
| 69240faf24 | |||
| f34305c0a1 | |||
| 2b5376ed54 | |||
| 958c03ab40 | |||
| fca67c1f43 | |||
| 70af018da5 | |||
| 0182c91ef9 | |||
| a67cfa7c4a | |||
| 3b7ab4cbd7 | |||
| 3469105d18 | |||
| 1414c63515 | |||
| 9f87bc5a2c | |||
| f5f4de7359 | |||
| 38d15d4d29 | |||
| 003eafa75d | |||
| b82853a95b | |||
| c060ac222a | |||
| 659c0505f8 | |||
| 02c2325e1b | |||
| d72aa10691 | |||
| 3c05ff8ef6 | |||
| 935c9205b9 | |||
| 826ce2a1b8 | |||
| bd2d6976d6 | |||
| a5d1814605 | |||
| ba07a7f6e6 | |||
| 708c61e50d | |||
| dc55253b9d | |||
| 8069d0ea89 | |||
| 4e9043f26d | |||
| 29fbd03c79 | |||
| 98e5b1a8aa | |||
| b175212516 | |||
| 16190583d1 | |||
| 70c9bfc069 | |||
| 4b9317b4fd | |||
| e4431da8d2 | |||
| 65f978368d |
@@ -202,9 +202,9 @@ export function ComplianceCheckTab() {
|
||||
setActiveCheckId(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
|
||||
while (attempts < 300) {
|
||||
while (attempts < 500) {
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
const pollRes = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${check_id}`)
|
||||
if (!pollRes.ok) { attempts++; continue }
|
||||
@@ -235,7 +235,7 @@ export function ComplianceCheckTab() {
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
if (attempts >= 300) {
|
||||
if (attempts >= 500) {
|
||||
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
|
||||
throw new Error('Zeitlimit ueberschritten (15 Min)')
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface BannerSite {
|
||||
site_name: string
|
||||
site_url: string
|
||||
is_active: boolean
|
||||
tcf_enabled?: boolean
|
||||
}
|
||||
|
||||
export function useCookieBanner() {
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function CookieBannerPage() {
|
||||
|
||||
{/* Tab: TCF/IAB */}
|
||||
{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) => {
|
||||
if (activeSiteId) {
|
||||
fetch(`/api/sdk/v1/banner/admin/sites/${activeSiteId}`, {
|
||||
|
||||
@@ -101,7 +101,35 @@ function DocumentGeneratorPageInner() {
|
||||
}
|
||||
}, [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(() => {
|
||||
const banner = state?.cookieBanner
|
||||
if (!banner) return
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useBannerConsents } from '../_hooks/useBannerConsents'
|
||||
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 {
|
||||
if (!iso) return '—'
|
||||
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() {
|
||||
const {
|
||||
records, sites, selectedSite, changeSite,
|
||||
stats, currentPage, setCurrentPage, totalRecords, loading,
|
||||
stats, currentPage, setCurrentPage, totalRecords, loading, reload,
|
||||
} = useBannerConsents()
|
||||
|
||||
const [detail, setDetail] = useState<BannerConsentRecord | null>(null)
|
||||
const [linkEmailInput, setLinkEmailInput] = useState('')
|
||||
const [linkingEmail, setLinkingEmail] = useState(false)
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{/* Stats + Site Selector */}
|
||||
@@ -184,6 +210,18 @@ export default function BannerConsentsTab() {
|
||||
))}
|
||||
</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">
|
||||
<span className="text-gray-500">Methode</span>
|
||||
<span>{detail.consent_method ? (
|
||||
@@ -192,9 +230,28 @@ export default function BannerConsentsTab() {
|
||||
</span>
|
||||
) : '—'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex justify-between items-center">
|
||||
<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 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>
|
||||
@@ -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>}
|
||||
</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>
|
||||
|
||||
@@ -108,6 +108,7 @@ export interface BannerConsentRecord {
|
||||
device_fingerprint: string
|
||||
categories: string[]
|
||||
vendors: string[]
|
||||
vendor_consents: Record<string, boolean>
|
||||
ip_hash: string | null
|
||||
user_agent: string | null
|
||||
linked_email: string | null
|
||||
@@ -144,4 +145,5 @@ export interface BannerSite {
|
||||
site_id: string
|
||||
site_name: string
|
||||
site_url: string
|
||||
tcf_enabled?: boolean
|
||||
}
|
||||
|
||||
+37
-9
@@ -14,14 +14,21 @@ type TabType = 'matched' | 'missing' | 'extra'
|
||||
export function HazardComparisonTable({ matched, missing, extra }: Props) {
|
||||
const [tab, setTab] = useState<TabType>('matched')
|
||||
|
||||
// Compute quality levels for matched pairs
|
||||
const greenCount = matched.filter(p => p.match_score >= 0.7).length
|
||||
const yellowCount = matched.filter(p => p.match_score >= 0.4 && p.match_score < 0.7).length
|
||||
// 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 }[] = [
|
||||
{ id: 'matched', label: `Zugeordnet (${greenCount} exakt, ${yellowCount} aehnlich)`, count: matched.length, color: 'text-green-600' },
|
||||
{ id: 'missing', label: 'Fehlend', count: missing.length, color: 'text-red-600' },
|
||||
{ id: 'extra', label: 'Zusaetzlich', count: extra.length, color: 'text-gray-500' },
|
||||
{ id: 'matched', label: `Zugeordnet (${greenCount} exakt, ${yellowCount} aehnlich)`, count: realMatched.length, color: 'text-green-600' },
|
||||
{ id: 'missing', label: 'Fehlend', count: allMissing.length, color: 'text-red-600' },
|
||||
{ id: 'extra', label: 'Engine Findings', count: allExtra.length, color: 'text-blue-500' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -44,9 +51,9 @@ export function HazardComparisonTable({ matched, missing, extra }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{tab === 'matched' && <MatchedTable pairs={matched} />}
|
||||
{tab === 'missing' && <MissingTable entries={missing} />}
|
||||
{tab === 'extra' && <ExtraTable entries={extra} />}
|
||||
{tab === 'matched' && <MatchedTable pairs={realMatched} />}
|
||||
{tab === 'missing' && <MissingTable entries={allMissing} />}
|
||||
{tab === 'extra' && <ExtraTable entries={allExtra} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -114,6 +121,21 @@ function MatchedTable({ pairs }: { pairs: HazardMatchPair[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
@@ -143,8 +165,14 @@ function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: Hazard
|
||||
<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 />
|
||||
) : (
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface HazardSummary {
|
||||
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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ export default function BenchmarkPage() {
|
||||
const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId)
|
||||
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
|
||||
|
||||
return (
|
||||
@@ -74,7 +76,7 @@ export default function BenchmarkPage() {
|
||||
<ScoreCard
|
||||
label="Hazard Coverage"
|
||||
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'}
|
||||
/>
|
||||
<ScoreCard
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"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) ──
|
||||
existingHazards, _ := h.store.ListHazards(ctx, projectID)
|
||||
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 {
|
||||
comps, _ := h.store.ListComponents(ctx, projectID)
|
||||
@@ -158,32 +160,35 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
}
|
||||
|
||||
created := 0
|
||||
seenCatZone := make(map[string]bool)
|
||||
seenCatZone := make(map[string]uuid.UUID) // dedupKey → hazardID
|
||||
catCount := make(map[string]int)
|
||||
for _, mp := range matchOutput.MatchedPatterns {
|
||||
// Narrative relevance filter: skip patterns whose zone/scenario
|
||||
// mentions machine-specific terms that don't appear in our components
|
||||
// Narrative relevance filter
|
||||
if !isPatternRelevant(mp, narrativeText, compNames) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, cat := range mp.HazardCats {
|
||||
// Per-category cap: limit hazards per category based on relevance
|
||||
maxForCat := categoryHazardCap(cat, len(comps))
|
||||
if catCount[cat] >= maxForCat {
|
||||
continue
|
||||
}
|
||||
|
||||
// Dedup by category + normalized zone
|
||||
zoneKey := normalizeZoneKey(mp.ZoneDE)
|
||||
if zoneKey == "" {
|
||||
zoneKey = mp.PatternID
|
||||
}
|
||||
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
|
||||
}
|
||||
seenCatZone[dedupKey] = true
|
||||
|
||||
name := mp.PatternName
|
||||
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{
|
||||
ProjectID: projectID,
|
||||
ComponentID: compID,
|
||||
@@ -212,6 +220,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
Category: cat,
|
||||
Scenario: mp.ScenarioDE,
|
||||
Function: iace.EncodeOpStates(mp.OperationalStates),
|
||||
LifecyclePhase: lifecycleStr,
|
||||
TriggerEvent: mp.TriggerDE,
|
||||
PossibleHarm: mp.HarmDE,
|
||||
AffectedPerson: mp.AffectedDE,
|
||||
@@ -220,7 +229,11 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
if cerr == nil {
|
||||
created++
|
||||
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.Count = len(existingHazards)
|
||||
for _, eh := range existingHazards {
|
||||
hazardIDsByCategory[eh.Category] = eh.ID
|
||||
hazardIDsByCategory[eh.Category] = append(hazardIDsByCategory[eh.Category], eh.ID)
|
||||
}
|
||||
}
|
||||
steps = append(steps, hazardStep)
|
||||
@@ -248,37 +261,60 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
}
|
||||
|
||||
created := 0
|
||||
usedMeasureIDs := make(map[string]bool)
|
||||
const maxMitigationsPerHazard = 5
|
||||
|
||||
for _, sm := range matchOutput.SuggestedMeasures {
|
||||
entry, ok := measureByID[sm.MeasureID]
|
||||
if !ok || usedMeasureIDs[sm.MeasureID] {
|
||||
continue
|
||||
}
|
||||
hazardID := findHazardForMeasureByCategory(entry.HazardCategory, hazardIDsByCategory)
|
||||
if hazardID == uuid.Nil {
|
||||
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
|
||||
// Build a flat list of all hazard IDs for iteration
|
||||
var allHazardIDs []uuid.UUID
|
||||
hazardCatByID := make(map[uuid.UUID]string)
|
||||
for cat, ids := range hazardIDsByCategory {
|
||||
for _, id := range ids {
|
||||
allHazardIDs = append(allHazardIDs, id)
|
||||
hazardCatByID[id] = cat
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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] {
|
||||
if usedMeasureIDs[m.ID] || added >= 8 {
|
||||
break
|
||||
if added >= maxMitigationsPerHazard || usedIDs[m.ID] {
|
||||
continue
|
||||
}
|
||||
rt := iace.ReductionType(m.ReductionType)
|
||||
if rt == "" {
|
||||
@@ -290,12 +326,16 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
})
|
||||
if cerr == nil {
|
||||
created++
|
||||
usedMeasureIDs[m.ID] = true
|
||||
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 {
|
||||
mitStep.Details = "Bereits vorhanden"
|
||||
mitStep.Count = len(existingMits)
|
||||
|
||||
@@ -217,6 +217,13 @@ var genericSafetyTerms = map[string]bool{
|
||||
"leitfaehig": true, "elektrisch": true, "mechanisch": true,
|
||||
"bedienfeld": true, "display": true, "anzeige": 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,
|
||||
@@ -369,18 +376,15 @@ func normalizeZoneKey(zone string) string {
|
||||
return strings.Join(sig, "_")
|
||||
}
|
||||
|
||||
// findHazardForMeasureByCategory finds a matching hazard for a measure.
|
||||
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID {
|
||||
if id, ok := hazardsByCategory[measureCat]; ok {
|
||||
return id
|
||||
// findHazardsForMeasureByCategory finds all hazards matching a measure's category.
|
||||
func findHazardsForMeasureByCategory(measureCat string, hazardsByCategory map[string][]uuid.UUID) []uuid.UUID {
|
||||
if ids, ok := hazardsByCategory[measureCat]; ok {
|
||||
return ids
|
||||
}
|
||||
for cat, id := range hazardsByCategory {
|
||||
for cat, ids := range hazardsByCategory {
|
||||
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
|
||||
return id
|
||||
return ids
|
||||
}
|
||||
}
|
||||
for _, id := range hazardsByCategory {
|
||||
return id
|
||||
}
|
||||
return uuid.Nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,49 +9,9 @@ import (
|
||||
// Fuzzy matching: Ground Truth entries ↔ Engine hazards
|
||||
// ============================================================================
|
||||
|
||||
const matchThreshold = 0.25
|
||||
const matchThreshold = 0.20
|
||||
|
||||
// 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"},
|
||||
}
|
||||
|
||||
// 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"},
|
||||
}
|
||||
// categoryMap, synonymSets, wrongMachineTerms → benchmark_synonyms.go
|
||||
|
||||
// CompareBenchmark runs the full comparison between Ground Truth and engine output.
|
||||
func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigation) *BenchmarkResult {
|
||||
@@ -68,15 +28,17 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
|
||||
engineSummaries := make([]HazardSummary, len(hazards))
|
||||
for i, h := range hazards {
|
||||
engineSummaries[i] = HazardSummary{
|
||||
ID: h.ID.String(),
|
||||
Name: h.Name,
|
||||
Category: h.Category,
|
||||
Zone: h.HazardousZone,
|
||||
Description: h.Description,
|
||||
Scenario: h.Scenario,
|
||||
PossibleHarm: h.PossibleHarm,
|
||||
TriggerEvent: h.TriggerEvent,
|
||||
Mitigations: mitNamesByHazard[h.ID.String()],
|
||||
ID: h.ID.String(),
|
||||
Name: h.Name,
|
||||
Category: h.Category,
|
||||
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()],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +58,17 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
|
||||
}
|
||||
}
|
||||
|
||||
// Greedy best-first 1:1 assignment
|
||||
sort.Slice(pairs, func(a, b int) bool { return pairs[a].score > pairs[b].score })
|
||||
// Greedy assignment: sort by score, but prioritize high-specificity matches
|
||||
// (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)
|
||||
usedEng := make(map[int]bool)
|
||||
var matched []HazardMatchPair
|
||||
@@ -187,52 +158,129 @@ func CompareBenchmark(gt *GroundTruth, hazards []Hazard, mitigations []Mitigatio
|
||||
}
|
||||
|
||||
// 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) {
|
||||
var score float64
|
||||
var reasons []string
|
||||
|
||||
// 1. Category match (weight 0.3)
|
||||
// 1. Category match (weight 0.2)
|
||||
catScore := categoryMatchScore(gt.HazardGroup, h.Category)
|
||||
score += 0.3 * catScore
|
||||
score += 0.2 * catScore
|
||||
if catScore > 0 {
|
||||
reasons = append(reasons, "Kategorie")
|
||||
}
|
||||
|
||||
// 2. Keyword/synonym match on hazard TYPE (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)
|
||||
score += 0.3 * kwScore
|
||||
score += 0.2 * kwScore
|
||||
if kwScore > 0 {
|
||||
reasons = append(reasons, "Keywords")
|
||||
}
|
||||
|
||||
// 3. Component/zone match (weight 0.4 — most important for specificity)
|
||||
// 3. Component/zone match (weight 0.3)
|
||||
zoneScore := zoneMatchScore(gt.ComponentZone, gt.HazardSubgroup, h.HazardousZone, h.MachineModule)
|
||||
score += 0.4 * zoneScore
|
||||
score += 0.3 * zoneScore
|
||||
if zoneScore > 0 {
|
||||
reasons = append(reasons, "Zone")
|
||||
}
|
||||
|
||||
// Penalty: if engine hazard mentions a machine-specific term not in the GT context,
|
||||
// it's likely a wrong-machine match (e.g. "Spielplatz" for a robot cell GT entry)
|
||||
// 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 // Heavy penalty
|
||||
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, "+")
|
||||
}
|
||||
|
||||
// wrongMachineTerms are words in an engine hazard that indicate it's about
|
||||
// a completely different machine type. If the GT entry doesn't mention these,
|
||||
// the match is penalized.
|
||||
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",
|
||||
// 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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -92,17 +92,19 @@ type HazardMatchPair struct {
|
||||
|
||||
// HazardSummary is a hazard representation for benchmark results with detail fields.
|
||||
type HazardSummary struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Component string `json:"component,omitempty"`
|
||||
Zone string `json:"zone,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"`
|
||||
Mitigations []string `json:"mitigations,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Component string `json:"component,omitempty"`
|
||||
Zone string `json:"zone,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.
|
||||
|
||||
@@ -54,6 +54,10 @@ type HazardPattern struct {
|
||||
// of the listed failure modes is relevant (by ComponentType match against project components).
|
||||
// Empty/nil = fires regardless of failure modes (backwards compatible).
|
||||
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).
|
||||
|
||||
@@ -126,7 +126,7 @@ func GetCNCHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M041", "M050"},
|
||||
@@ -149,7 +149,7 @@ func GetCNCHazardPatterns() []HazardPattern {
|
||||
Priority: 84, MachineTypes: cncTypes,
|
||||
OperationalStates: []string{"teach_mode", "manual_operation"},
|
||||
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",
|
||||
HarmDE: "Quetschung oder Schlagverletzung durch schnell verfahrende Maschinenteile",
|
||||
AffectedDE: "Einrichter, Programmierer", ZoneDE: "Verfahrbereich der Achsen",
|
||||
|
||||
@@ -49,7 +49,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M245", "M210"},
|
||||
@@ -57,7 +57,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
|
||||
Priority: 80, MachineTypes: cncTypes,
|
||||
OperationalStates: []string{"maintenance"},
|
||||
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",
|
||||
HarmDE: "Quetschung von Hand oder Fuss, Knochenbrueche",
|
||||
AffectedDE: "Wartungspersonal", ZoneDE: "Maschineninneres, Wartungszugang",
|
||||
@@ -193,7 +193,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M212", "M050", "M042"},
|
||||
@@ -202,7 +202,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
|
||||
OperationalStates: []string{"manual_operation", "teach_mode"},
|
||||
HumanRoles: []string{"maintenance_tech", "programmer"},
|
||||
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",
|
||||
HarmDE: "Quetschung, Kollision Werkzeug/Werkstueck",
|
||||
AffectedDE: "Wartungspersonal, Einrichter", ZoneDE: "Verfahrbereich, Bearbeitungsraum",
|
||||
@@ -218,7 +218,7 @@ func GetCNCHazardPatternsExt() []HazardPattern {
|
||||
Priority: 70, MachineTypes: cncTypes,
|
||||
OperationalStates: []string{"maintenance"},
|
||||
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",
|
||||
HarmDE: "Kurzschluss, Stromschlag bei Beruehrung nasser Teile",
|
||||
AffectedDE: "Wartungspersonal", ZoneDE: "Schaltschrank, Steuerungsbereich",
|
||||
|
||||
@@ -11,7 +11,7 @@ func builtinElectricalPatterns() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M061", "M062", "M063", "M121"},
|
||||
SuggestedEvidenceIDs: []string{"E01", "E04", "E10"},
|
||||
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.",
|
||||
HarmDE: "Stromschlag, Herzkammerflimmern, Verbrennungen, Todesfolge bei Hochspannung.",
|
||||
AffectedDE: "Wartungspersonal, Elektrofachkraefte, Bedienpersonal",
|
||||
|
||||
@@ -66,7 +66,7 @@ func builtinEnvironmentPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"ergonomic", "mechanical_hazard"},
|
||||
|
||||
@@ -130,7 +130,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M121", "M131"},
|
||||
SuggestedEvidenceIDs: []string{"E14"},
|
||||
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",
|
||||
HarmDE: "Wegschleudern des Werkzeugs, schwere Verletzungen durch Projektil",
|
||||
AffectedDE: "Bedienpersonal, Personen im Umfeld",
|
||||
@@ -262,7 +262,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M005"},
|
||||
SuggestedEvidenceIDs: []string{"E08"},
|
||||
Priority: 80,
|
||||
Priority: 80, MachineTypes: []string{"press"},
|
||||
ScenarioDE: "Exzentrische Belastung des Stoessels fuehrt zu seitlichem Ausbrechen des Werkstuecks.",
|
||||
TriggerDE: "Werkstueck nicht korrekt positioniert, seitliche Kraftkomponente entsteht",
|
||||
HarmDE: "Aufprallverletzung durch geschleudertes Werkstueck, Quetschung",
|
||||
@@ -290,7 +290,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
|
||||
// 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"},
|
||||
RequiredEnergyTags: []string{},
|
||||
RequiredLifecycles: []string{"setup"},
|
||||
@@ -336,7 +336,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
|
||||
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"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -361,7 +361,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001", "M051"},
|
||||
SuggestedEvidenceIDs: []string{"E08", "E20"},
|
||||
Priority: 80,
|
||||
Priority: 80, MachineTypes: []string{"conveyor", "packaging"},
|
||||
ScenarioDE: "Finger oder Kleidung werden an der Bandumlenkstelle eingezogen.",
|
||||
TriggerDE: "Eingriff am laufenden Band, lose Kleidung geraet in Umlenkrolle",
|
||||
HarmDE: "Fingeramputation, Armverletzung, Strangulation durch eingezogene Kleidung",
|
||||
@@ -595,7 +595,7 @@ func GetExtendedHazardPatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M051"},
|
||||
SuggestedEvidenceIDs: []string{"E08", "E20"},
|
||||
Priority: 80,
|
||||
Priority: 80, MachineTypes: []string{"rotary_transfer"},
|
||||
ScenarioDE: "Hand wird zwischen Drehteller und festem Anschlag eingeklemmt bei Taktbewegung.",
|
||||
TriggerDE: "Eingriff waehrend der Taktbewegung, fehlende Schutzabdeckung am Drehteller",
|
||||
HarmDE: "Quetschung, Fingerfraktur, Amputation von Fingern",
|
||||
|
||||
@@ -42,7 +42,7 @@ func GetDGUVExtendedPatterns() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001"},
|
||||
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,
|
||||
},
|
||||
{
|
||||
@@ -102,7 +102,7 @@ func GetDGUVExtendedPatterns() []HazardPattern {
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M051"},
|
||||
Priority: 80,
|
||||
Priority: 80, MachineTypes: []string{"crane", "construction"},
|
||||
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,
|
||||
},
|
||||
@@ -261,13 +261,13 @@ func GetDGUVExtendedPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"decommissioning", "disposal"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
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,
|
||||
},
|
||||
|
||||
|
||||
@@ -428,7 +428,7 @@ func GetFinalPatternsA() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001", "M005"},
|
||||
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",
|
||||
AffectedDE: "Wartungspersonal", ZoneDE: "Kettenrad, Kettenstrang",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
@@ -667,13 +667,13 @@ func GetFinalPatternsA() []HazardPattern {
|
||||
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"},
|
||||
RequiredEnergyTags: []string{"gravitational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001", "M141"},
|
||||
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",
|
||||
AffectedDE: "Montagepersonal", ZoneDE: "Montageplatz, Regalbereich",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
@@ -814,7 +814,7 @@ func GetFinalPatternsA() []HazardPattern {
|
||||
},
|
||||
// === 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"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -1027,7 +1027,7 @@ func GetFinalPatternsA() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001", "M005"},
|
||||
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",
|
||||
AffectedDE: "Transportpersonal", ZoneDE: "Kippbereich, Aufstellflaeche",
|
||||
DefaultSeverity: 5, DefaultExposure: 1,
|
||||
|
||||
@@ -624,7 +624,7 @@ func GetFinalPatternsB() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
SuggestedMeasureIDs: []string{"M124", "M141"},
|
||||
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",
|
||||
AffectedDE: "Wartungspersonal, Abbrucharbeiter", ZoneDE: "Altanlage, Dichtungen, Isolierungen",
|
||||
DefaultSeverity: 5, DefaultExposure: 1,
|
||||
|
||||
@@ -860,7 +860,7 @@ func GetFinalPatternsC() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"ergonomic_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
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)",
|
||||
AffectedDE: "Wartungspersonal", ZoneDE: "Bodenbereich",
|
||||
DefaultSeverity: 2, DefaultExposure: 4,
|
||||
|
||||
@@ -158,7 +158,7 @@ func GetFinalPatternsD() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "maintenance_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001"},
|
||||
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",
|
||||
AffectedDE: "Bedienpersonal", ZoneDE: "Verschleissteil, Fuehrung",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
@@ -573,7 +573,7 @@ func GetFinalPatternsD() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001", "M005"},
|
||||
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",
|
||||
AffectedDE: "Bedienpersonal", ZoneDE: "Gesamte Maschine",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
@@ -817,7 +817,7 @@ func GetFinalPatternsD() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M001", "M005"},
|
||||
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",
|
||||
AffectedDE: "Personen darunter", ZoneDE: "Unter Kranschwenkbereich",
|
||||
DefaultSeverity: 5, DefaultExposure: 2,
|
||||
|
||||
@@ -131,7 +131,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
RequiresExpertCalculation: true,
|
||||
ExpertHintDE: "IP-Schutzklasse muss fuer Nassreinigung (mindestens IPX5) nachgewiesen werden.",
|
||||
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.",
|
||||
HarmDE: "Elektrischer Schlag, Herzkammerflimmern, Tod durch Stromschlag.",
|
||||
AffectedDE: "Reinigungspersonal, Bedienpersonal bei Nassreinigung.",
|
||||
|
||||
@@ -65,7 +65,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M001", "M005"},
|
||||
SuggestedEvidenceIDs: []string{"E08", "E20"},
|
||||
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",
|
||||
HarmDE: "Amputationsverletzung an Fuessen/Haenden, tiefe Schnittwunden, Augenverletzung durch Steinschlag",
|
||||
AffectedDE: "Maehwerksfahrer, Gartenarbeiter, Umstehende",
|
||||
@@ -311,7 +311,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M052", "M141"},
|
||||
SuggestedEvidenceIDs: []string{"E20"},
|
||||
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",
|
||||
HarmDE: "Knochenbrueche, Wirbelsaeulenverletzung, toedlicher Sturz ab 2 m Hoehe",
|
||||
AffectedDE: "Wartungspersonal, Bediener bei Stoerung",
|
||||
|
||||
@@ -16,10 +16,10 @@ func GetMaintenanceExtPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"moving_part"}, RequiredLifecycles: []string{"maintenance"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"},
|
||||
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",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 50,
|
||||
@@ -33,11 +33,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
|
||||
ScenarioDE: "Unqualifiziertes Personal an Elektrik", TriggerDE: "Keine Elektrofachkraft",
|
||||
HarmDE: "Stromschlag, Fehlverdrahtung", AffectedDE: "Instandhalter", ZoneDE: "Schaltschrank",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
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",
|
||||
DefaultSeverity: 5, DefaultExposure: 3},
|
||||
{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",
|
||||
HarmDE: "Schnittwunden, Abschuerfungen", AffectedDE: "Instandhalter", ZoneDE: "Blechverkleidungen",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M005", "M082"}, SuggestedEvidenceIDs: []string{"E10"}, Priority: 60,
|
||||
@@ -72,11 +72,11 @@ func GetMaintenanceExtPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"chemical_risk"}, RequiredLifecycles: []string{"maintenance"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
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",
|
||||
DefaultSeverity: 2, DefaultExposure: 3},
|
||||
// — 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"},
|
||||
GeneratedHazardCats: []string{"safety_function_failure"},
|
||||
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",
|
||||
HarmDE: "Quetschung, Amputation", AffectedDE: "Einrichter", ZoneDE: "Werkzeugaufnahme",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M106", "M054"}, SuggestedEvidenceIDs: []string{"E08"}, Priority: 80,
|
||||
@@ -129,17 +129,17 @@ func GetMaintenanceExtPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"structural_part"}, RequiredLifecycles: []string{"setup"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
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",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M082", "M141"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
|
||||
ScenarioDE: "Einrichtarbeiten in Hoehe ohne sicheren Zugang", TriggerDE: "Improvisierte Aufstiegshilfe",
|
||||
HarmDE: "Absturz, Frakturen", AffectedDE: "Einrichter", ZoneDE: "Maschinenoberteil",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"safety_function_failure", "mechanical_hazard"},
|
||||
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",
|
||||
DefaultSeverity: 4, DefaultExposure: 2},
|
||||
// — 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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
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",
|
||||
HarmDE: "Anfahrunfall, Quetschung", AffectedDE: "Fussgaenger", ZoneDE: "Transportwege",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 65,
|
||||
@@ -339,7 +339,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
|
||||
ScenarioDE: "Reinigung ohne Abschaltung der Maschine", TriggerDE: "Zeitdruck",
|
||||
HarmDE: "Einzug, Quetschung, Aufwickeln", AffectedDE: "Reinigungspersonal", ZoneDE: "Rotierende Teile",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M082"}, SuggestedEvidenceIDs: []string{"E20"}, Priority: 45,
|
||||
@@ -410,7 +410,7 @@ func GetMaintenanceExtPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"electrical_part"}, RequiredEnergyTags: []string{"electrical"},
|
||||
RequiredLifecycles: []string{"maintenance"}, GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
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",
|
||||
DefaultSeverity: 4, DefaultExposure: 3},
|
||||
{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",
|
||||
DefaultSeverity: 3, DefaultExposure: 3},
|
||||
// — 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"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
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",
|
||||
HarmDE: "Verzoegerte Erstversorgung", AffectedDE: "Instandhalter", ZoneDE: "Abgelegene Wartungsorte",
|
||||
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"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
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",
|
||||
DefaultSeverity: 4, DefaultExposure: 2},
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func builtinMechanicalPatterns() []HazardPattern {
|
||||
TriggerDE: "Bediener befindet sich im Kraftwirkbereich waehrend des Arbeitshubes oder bei Stoerungsbeseitigung.",
|
||||
HarmDE: "Schwere Quetschung, Fraktur, innere Verletzungen, Todesfolge bei Ganzkompression.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
|
||||
ZoneDE: "Kraftwirkbereich (Pressenraum, Vorschubachse), Einlegestelle",
|
||||
ZoneDE: "Kraftwirkbereich, Einlegestelle, Vorschubachse",
|
||||
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.",
|
||||
HarmDE: "Kopfverletzung, Fraktur, Quetschung durch herabfallende Last; Sturzverletung.",
|
||||
AffectedDE: "Wartungspersonal, Bedienpersonal, Personen im Gefahrenbereich",
|
||||
ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen, Kran-/Hebezeugbereich",
|
||||
ZoneDE: "Bereich unterhalb angehobener Lasten, Wartungsplattformen",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -150,7 +150,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"maintenance"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
@@ -165,7 +165,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"maintenance", "cleaning"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -179,7 +179,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"maintenance", "fault_clearing"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
@@ -195,7 +195,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"maintenance"},
|
||||
GeneratedHazardCats: []string{"ergonomic"},
|
||||
@@ -273,7 +273,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"setup"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||
@@ -281,7 +281,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
Priority: 94,
|
||||
RequiresExpertCalculation: true,
|
||||
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",
|
||||
HarmDE: "Toedliches Quetschen oder Amputation durch vollen Pressenhub bei Anwesenheit",
|
||||
AffectedDE: "Einrichter",
|
||||
@@ -289,7 +289,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"changeover", "setup"},
|
||||
GeneratedHazardCats: []string{"safety_function_failure"},
|
||||
@@ -323,7 +323,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
// 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"},
|
||||
RequiredLifecycles: []string{"transport"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -337,7 +337,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"assembly"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -351,7 +351,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"commissioning"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -365,7 +365,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"decommissioning", "disposal"},
|
||||
GeneratedHazardCats: []string{"material_environmental", "pneumatic_hydraulic"},
|
||||
@@ -379,7 +379,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"decommissioning", "disposal"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -411,7 +411,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"cleaning"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -425,7 +425,7 @@ func GetOperationalHazardPatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredLifecycles: []string{"cleaning"},
|
||||
ExcludedComponentTags: []string{"interlocked"},
|
||||
|
||||
@@ -262,7 +262,7 @@ func GetPlasticsMetalPatterns() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
|
||||
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.",
|
||||
TriggerDE: "Tragen von Handschuhen an der Drehmaschine, offene Haare, lose Kleidung",
|
||||
HarmDE: "Skalpierung, Armfraktur, Strangulation, toedliche Aufwickelverletzung",
|
||||
|
||||
@@ -124,7 +124,7 @@ func GetPressHazardPatterns() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M051", "M131"},
|
||||
SuggestedEvidenceIDs: []string{"E01", "E08"},
|
||||
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.",
|
||||
HarmDE: "Schwere Schnittverletzungen durch Oelstrahl, Augenverletzungen, Verbrennungen.",
|
||||
AffectedDE: "Instandhaltungspersonal, Hydraulik-Fachkraefte.",
|
||||
|
||||
@@ -3,6 +3,9 @@ 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{
|
||||
@@ -14,33 +17,22 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"moving_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M062", "M054"},
|
||||
Priority: 95, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
|
||||
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 (z.B. nach Betreten der Roboterzelle).",
|
||||
TriggerDE: "Roboterarm bewegt sich waehrend Person im Gefahrenbereich steht.",
|
||||
HarmDE: "Quetschungen, Knochenbrueche, innere Verletzungen durch Einklemmen von Koerperteilen.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
|
||||
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal, Reinigungspersonal",
|
||||
ZoneDE: "Roboterarm, feststehende Anlagenteile innerhalb der Roboterzelle",
|
||||
DefaultSeverity: 4, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1601", NameDE: "Quetschen bei Teach-Betrieb am Roboter", NameEN: "Crushing during robot teach mode",
|
||||
RequiredComponentTags: []string{"moving_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 94, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
|
||||
ScenarioDE: "Einrichter befindet sich zum Teachen/Programmieren in der Roboterzelle. Roboterarm bewegt sich unerwartet oder mit zu hoher Geschwindigkeit.",
|
||||
TriggerDE: "Teach-Modus laesst Bewegung zu, Einrichter steht im Schwenkbereich des Arms.",
|
||||
HarmDE: "Quetschungen, Prellungen durch Kontakt mit bewegtem Roboterarm.",
|
||||
AffectedDE: "Einrichter, Programmierer",
|
||||
ZoneDE: "Roboterarm, Inneres der Roboterzelle",
|
||||
DefaultSeverity: 3, 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: 93,
|
||||
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.",
|
||||
@@ -53,11 +45,12 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"moving_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M054", "M141"},
|
||||
Priority: 94,
|
||||
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. Person kann sich nicht entziehen.",
|
||||
AffectedDE: "Wartungspersonal, Einrichter",
|
||||
HarmDE: "Quetschungen, Stoss durch anlaufenden Roboter.",
|
||||
AffectedDE: "Wartungspersonal, Einrichter, Reinigungspersonal",
|
||||
ZoneDE: "Inneres der Roboterzelle",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
@@ -66,24 +59,40 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"moving_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M002"},
|
||||
Priority: 92,
|
||||
ScenarioDE: "Roboterarm ueberschreitet den vorgesehenen Bewegungsbereich und trifft den Schutzzaun mit hoher Kraft.",
|
||||
TriggerDE: "Fehler in der Bahnplanung oder Ausfall der Achsbegrenzung. Roboter faehrt ueber Software-Endschalter hinaus.",
|
||||
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 des Roboters", NameEN: "Crushing in robot gripper area",
|
||||
ID: "HP1610", NameDE: "Quetschen im Greiferbereich", NameEN: "Crushing in gripper area",
|
||||
RequiredComponentTags: []string{"clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 94, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
|
||||
ScenarioDE: "Person greift in den Bereich des Greifers waehrend der Roboter ein Werkstueck aufnimmt oder ablegt. Hand wird zwischen Greifbacken und Werkstueck eingeklemmt.",
|
||||
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",
|
||||
@@ -95,8 +104,9 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M007", "M141"},
|
||||
Priority: 93,
|
||||
ScenarioDE: "Greifer verliert das Werkstueck waehrend des Transports (z.B. durch Druckverlust der Pneumatik, oelige Oberflaeche, falsches Werkstueck).",
|
||||
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",
|
||||
@@ -108,8 +118,9 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"clamping_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M141"},
|
||||
Priority: 92,
|
||||
ScenarioDE: "Greifer versagt und Roboterarm beschleunigt das freigesetzte Werkstueck in Richtung Schutzzaun oder Einhausung.",
|
||||
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",
|
||||
@@ -124,8 +135,9 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"entanglement_risk"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M061", "M003"},
|
||||
Priority: 93,
|
||||
ScenarioDE: "Person greift an Foerderband fuer Werkstueckzulauf oder -auslauf und wird zwischen beweglichen und feststehenden Teilen eingeklemmt.",
|
||||
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",
|
||||
@@ -137,7 +149,8 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"entanglement_risk", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M061"},
|
||||
Priority: 93,
|
||||
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.",
|
||||
@@ -150,9 +163,10 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"entanglement_risk"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M008"},
|
||||
Priority: 91,
|
||||
ScenarioDE: "Werkstueck faehrt ueber das Ende des Transportbandes hinaus und faellt herab, trifft Person die am Bandende steht.",
|
||||
TriggerDE: "Mechanischer Anschlag fehlt oder ist beschaedigt. Werkstueck wird nicht rechtzeitig gestoppt.",
|
||||
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",
|
||||
@@ -166,8 +180,9 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredComponentTags: []string{"guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003"},
|
||||
Priority: 90,
|
||||
ScenarioDE: "Person schneidet sich an nicht entgrateten oder scharfkantigen Blechen der Einhausung, des Schutzzauns oder der Verkleidung.",
|
||||
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",
|
||||
@@ -178,11 +193,12 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
// Pneumatik / Druckluft
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1630", NameDE: "Schlauch unter Druck springt ab", NameEN: "Pressurized hose comes loose",
|
||||
ID: "HP1630", NameDE: "Pneumatikschlauch springt unter Druck ab", NameEN: "Pressurized hose comes loose",
|
||||
RequiredComponentTags: []string{"pinch_point"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M420"},
|
||||
Priority: 91,
|
||||
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.",
|
||||
@@ -194,11 +210,12 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
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{"M420", "M141"},
|
||||
Priority: 91,
|
||||
ScenarioDE: "Pneumatik-Komponenten stehen nach Abschaltung noch unter Druck. Bei Arbeiten an der Anlage werden druckbeaufschlagte Teile geloest.",
|
||||
TriggerDE: "Fehlende Druckentlastung vor Wartungsarbeiten. Gesperrte Rueckschlagventile halten Druck.",
|
||||
HarmDE: "Unkontrolliertes Loesen von Verbindungen, wegfliegende Teile, Verletzung durch Druckstoss.",
|
||||
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,
|
||||
@@ -207,14 +224,56 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
// Kuehlschmierstoff (KSS)
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1635", NameDE: "KSS-Leckage fuehrt zu Rutschgefahr", NameEN: "Coolant leakage causes slip hazard",
|
||||
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{},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M420"},
|
||||
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ScenarioDE: "Kuehlschmierstoff tritt aus undichter Leitung oder Verbindung aus und bildet einen rutschigen Belag auf dem Boden.",
|
||||
TriggerDE: "Leckage an Schlauchverbindung, Dichtungsversagen, Ueberdrucksituation.",
|
||||
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",
|
||||
@@ -223,12 +282,12 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
{
|
||||
ID: "HP1636", NameDE: "Hautkontakt mit Kuehlschmierstoff", NameEN: "Skin contact with coolant",
|
||||
RequiredComponentTags: []string{},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
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 im Bearbeitungsraum.",
|
||||
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",
|
||||
@@ -237,13 +296,13 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
{
|
||||
ID: "HP1637", NameDE: "Einatmen von KSS-Aerosolen", NameEN: "Inhalation of coolant aerosols",
|
||||
RequiredComponentTags: []string{},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
Priority: 90, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ScenarioDE: "Waehrend der Werkstueckbearbeitung entstehen KSS-Aerosole die beim Oeffnen der Bearbeitungszelle freigesetzt werden.",
|
||||
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: "Atembeschwerden, Reizung der Atemwege, bei chronischer Exposition Atemwegserkrankungen.",
|
||||
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,
|
||||
@@ -254,25 +313,27 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
{
|
||||
ID: "HP1640", NameDE: "Direktes Beruehren spannungsfuehrender Teile", NameEN: "Direct contact with live parts",
|
||||
RequiredComponentTags: []string{},
|
||||
RequiredEnergyTags: []string{"electrical"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M009", "M410"},
|
||||
Priority: 93,
|
||||
ScenarioDE: "Person beruehrt spannungsfuehrende Teile der Anlage (Kabel, Klemmen, Stecker) die nicht ausreichend isoliert oder abgedeckt sind.",
|
||||
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: "Elektrischer Schlag, bei Hochspannung potentiell toedlich.",
|
||||
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: "Indirektes Beruehren durch Schutzleiterfehler", NameEN: "Indirect contact due to PE conductor failure",
|
||||
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{"M410", "M411"},
|
||||
Priority: 93,
|
||||
ScenarioDE: "Schutzleiter ist unterbrochen oder nicht korrekt angeschlossen. Beruehrbare leitfaehige Teile fuehren gefaehrliche Beruehrungsspannung.",
|
||||
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",
|
||||
@@ -285,9 +346,10 @@ func GetRobotCellPatterns() []HazardPattern {
|
||||
RequiredEnergyTags: []string{"electrical"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M009"},
|
||||
Priority: 92,
|
||||
ScenarioDE: "Kabelquerschnitt ist nicht auf die maximale Leistung ausgelegt oder Ueberstromschutz fehlt. Kabel ueberhitzt und entzuendet sich.",
|
||||
TriggerDE: "Dauerhafter Betrieb nahe der Belastungsgrenze, fehlende oder falsch dimensionierte Sicherung.",
|
||||
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",
|
||||
|
||||
@@ -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"},
|
||||
SuggestedEvidenceIDs: []string{"E01", "E14"},
|
||||
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.",
|
||||
HarmDE: "Ueberfahren mechanischer Anschlaege, zu hohe Kraefte/Geschwindigkeiten, Kollision.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter",
|
||||
|
||||
@@ -252,7 +252,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M141"},
|
||||
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.",
|
||||
TriggerDE: "Materialermuedung, Blitzschaden, Vereisung mit Unwucht, fehlende Inspektionen",
|
||||
HarmDE: "Toedliche Verletzung durch Blattstuecke, Sachschaeden im weiten Umkreis",
|
||||
@@ -261,7 +261,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
|
||||
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"},
|
||||
RequiredEnergyTags: []string{"gravitational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -297,7 +297,7 @@ func GetSpecificMachinePatterns() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
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.",
|
||||
TriggerDE: "Vereisung im Winter, fehlende Eiserkennungssysteme, Weiterbetrieb bei Eisansatz",
|
||||
HarmDE: "Verletzung durch Eisschlag, Sachschaeden an Fahrzeugen und Gebaeuden",
|
||||
|
||||
@@ -30,7 +30,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M141"},
|
||||
SuggestedEvidenceIDs: []string{"E08", "E20"},
|
||||
Priority: 80,
|
||||
Priority: 80, MachineTypes: []string{"escalator"},
|
||||
ScenarioDE: "Finger oder Handteile werden am Einzugspunkt des Handlaufs in die Verkleidung gezogen.",
|
||||
TriggerDE: "Kinderhand am Handlauf nahe der Verkleidung, fehlende Einlaufschutzbuegel",
|
||||
HarmDE: "Fingerquetschung, Hautabschuerfungen, bei Kindern Armverletzung",
|
||||
@@ -39,7 +39,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
|
||||
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"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -75,7 +75,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M141"},
|
||||
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.",
|
||||
TriggerDE: "Materialermuedung, Korrosion, fehlende Inspektionen, Vandalismus",
|
||||
HarmDE: "Einzug in Mechanik, Beinverletzungen, Sturz in Maschinenkammer",
|
||||
@@ -173,7 +173,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M141"},
|
||||
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).",
|
||||
TriggerDE: "Oeffnungen im kritischen Bereich 89-230 mm, V-foermige Spalte, Gelaendersprosse mit Kopffangmass",
|
||||
HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
|
||||
@@ -233,7 +233,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M141"},
|
||||
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.",
|
||||
TriggerDE: "Kleidung mit Kordeln am Hals, zu grosse Maschenweite, lose Seilenden",
|
||||
HarmDE: "Strangulation, Erstickung, toedliche Verletzung",
|
||||
@@ -361,7 +361,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
|
||||
SuggestedEvidenceIDs: []string{"E08", "E09"},
|
||||
Priority: 85,
|
||||
Priority: 85, MachineTypes: []string{"laundry"},
|
||||
ScenarioDE: "Person greift in die drehende Trommel der Industriewaschmaschine und wird eingezogen.",
|
||||
TriggerDE: "Defekte Tuerverriegelung, Oeffnen waehrend Nachlauf, Bedienfehler",
|
||||
HarmDE: "Schwere Quetschverletzung, Armeinzug, Strangulation durch Waeschestuecke",
|
||||
@@ -411,7 +411,7 @@ func GetSpecificMachinePatterns2() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M005", "M141"},
|
||||
SuggestedEvidenceIDs: []string{"E20"},
|
||||
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",
|
||||
HarmDE: "Tiefe Schnittwunden, Amputationsgefahr, toedliche Verletzung bei grossen Scheiben",
|
||||
AffectedDE: "Transportpersonal, Monteure, Passanten",
|
||||
|
||||
@@ -22,7 +22,7 @@ func GetTextileAgriPatterns() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M452", "M061"}, SuggestedEvidenceIDs: []string{"E01"},
|
||||
Priority: 78, MachineTypes: []string{"textile", "knitting"},
|
||||
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",
|
||||
AffectedDE: "Bedienpersonal", ZoneDE: "Nadelbett",
|
||||
DefaultSeverity: 3, DefaultExposure: 4},
|
||||
@@ -123,7 +123,7 @@ func GetTextileAgriPatterns() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M461", "M465"}, SuggestedEvidenceIDs: []string{"E01", "E08"},
|
||||
Priority: 94, MachineTypes: []string{"agricultural", "harvester", "combine"},
|
||||
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",
|
||||
HarmDE: "Amputation, schwere Schnittverletzungen", AffectedDE: "Bediener, Wartungspersonal", ZoneDE: "Schneidwerksbereich",
|
||||
DefaultSeverity: 5, DefaultExposure: 3},
|
||||
|
||||
@@ -42,7 +42,7 @@ func builtinThermalPatterns() []HazardPattern {
|
||||
SuggestedEvidenceIDs: []string{"E01"},
|
||||
Priority: 75,
|
||||
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.",
|
||||
AffectedDE: "Wartungspersonal, Einrichter",
|
||||
ZoneDE: "Motorgehaeuse, Getriebegehaeuse, Linearantrieb",
|
||||
|
||||
@@ -230,7 +230,7 @@ func GetWeldingGlassTextilePatterns() []HazardPattern {
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003", "M004", "M082"},
|
||||
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.",
|
||||
TriggerDE: "Manuelles Nachjustieren bei laufenden Walzen, fehlender Schutz am Einlaufbereich",
|
||||
HarmDE: "Fingerquetschung, Einzug der Hand, Hautabschaelungen",
|
||||
|
||||
@@ -71,21 +71,21 @@ func getSupplementaryMeasures() []ProtectiveMeasureEntry {
|
||||
// Elektrische Sicherheit — Potentialausgleich & 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: "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: "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: "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: "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: "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
|
||||
// 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: "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: "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: "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
|
||||
// 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"`
|
||||
GeneratedHazardType string `json:"generated_hazard_type,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.
|
||||
@@ -217,7 +219,9 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
||||
StateTransitions: p.StateTransitions,
|
||||
HumanRoles: p.HumanRoles,
|
||||
GeneratedHazardType: p.GeneratedHazardType,
|
||||
MatchedFailureModes: matchedFMs,
|
||||
MatchedFailureModes: matchedFMs,
|
||||
ApplicableLifecycles: p.ApplicableLifecycles,
|
||||
SuggestedMeasureIDs: p.SuggestedMeasureIDs,
|
||||
})
|
||||
|
||||
for _, cat := range p.GeneratedHazardCats {
|
||||
|
||||
@@ -38,5 +38,6 @@ func collectAllPatterns() []HazardPattern {
|
||||
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")
|
||||
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:
|
||||
async with httpx.AsyncClient(timeout=90.0) as client:
|
||||
async with httpx.AsyncClient(timeout=300.0) as client:
|
||||
resp = await client.post(
|
||||
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:
|
||||
return {
|
||||
@@ -86,10 +90,15 @@ async def extract_text(req: ExtractTextRequest):
|
||||
"error": "Kein Text extrahierbar",
|
||||
}
|
||||
|
||||
doc = docs[0]
|
||||
text = doc.get("full_text", "") or doc.get("text_preview", "") or doc.get("text", "")
|
||||
title = doc.get("title", "") or doc.get("doc_type", "")
|
||||
word_count = doc.get("word_count", 0) or len(text.split())
|
||||
# Merge all documents (handles multi-page DSIs like BMW)
|
||||
texts = []
|
||||
for doc in docs:
|
||||
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 {
|
||||
"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
|
||||
# 2. DSI text contains Cookie/Social-Media sections → auto-fill empty rows
|
||||
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)
|
||||
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:
|
||||
if entry.get("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
|
||||
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)
|
||||
total_findings += result.findings_count
|
||||
|
||||
@@ -302,17 +326,24 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
||||
else:
|
||||
r.scenario = "import"
|
||||
|
||||
# Step 5: Build report
|
||||
# Step 5: Build report with management summary
|
||||
_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)
|
||||
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])
|
||||
site_name = (
|
||||
extracted_profile.get("company_profile", {}).get("companyName")
|
||||
or _extract_domain(doc_entries)
|
||||
or "Unbekannt"
|
||||
)
|
||||
email_result = send_email(
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -349,23 +380,55 @@ def _update(check_id: str, msg: 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:
|
||||
async with httpx.AsyncClient(timeout=90.0) as client:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
resp = await client.post(
|
||||
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:
|
||||
return ""
|
||||
docs = resp.json().get("documents", [])
|
||||
if not docs:
|
||||
return ""
|
||||
doc = docs[0]
|
||||
return doc.get("full_text", "") or doc.get("text_preview", "") or ""
|
||||
if resp.status_code == 200:
|
||||
docs = resp.json().get("documents", [])
|
||||
if docs:
|
||||
texts = []
|
||||
for doc in docs:
|
||||
t = 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:
|
||||
logger.warning("Text fetch failed for %s: %s", url, e)
|
||||
return ""
|
||||
logger.warning("Consent-tester fetch failed for %s: %s", url, e)
|
||||
|
||||
# 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(
|
||||
@@ -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]:
|
||||
"""Doc_types to skip entirely. Currently empty — we check everything
|
||||
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(
|
||||
results: list[DocCheckResult],
|
||||
cookie_result: dict | None,
|
||||
|
||||
@@ -213,3 +213,197 @@ def auto_fill_from_dsi(doc_entries: list[dict]) -> None:
|
||||
"Auto-filled %d empty rows from DSI sections: %s",
|
||||
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)
|
||||
|
||||
@@ -532,19 +532,43 @@ async def _find_dsi_links(page: Page, base_domain: str) -> list[dict]:
|
||||
return []
|
||||
|
||||
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:
|
||||
await page.evaluate("""() => {
|
||||
// 1. Open all <details> that are closed
|
||||
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',
|
||||
'[class*="collapse"] > button','.panel-heading a'];
|
||||
sels.forEach(s => document.querySelectorAll(s).forEach(e => { try{e.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{}
|
||||
|
||||
// 2. Click buttons that are explicitly CLOSED (aria-expanded="false")
|
||||
document.querySelectorAll('button[aria-expanded="false"]').forEach(b => {
|
||||
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:
|
||||
pass
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
**URL:** https://www.bmw.de
|
||||
**Typ:** Konzern / B2C Automobil
|
||||
**Datum:** 2026-05-12
|
||||
**Batch-Test:** 8/9 L1, 10/21 L2 (Mangelhaft, 48%)
|
||||
**Datum:** 2026-05-15 (URLs + Inhalte verifiziert)
|
||||
**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 |
|
||||
|-------------|-----------|-----|
|
||||
| DSI | Ja | https://www.bmw.de/de/footer/metanavigation/datenschutz.html |
|
||||
| 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 |
|
||||
| AGB | Ja | TODO: URL verifizieren |
|
||||
| Widerruf | Ggf. in AGB | — |
|
||||
| Social Media DSE | Nein | — |
|
||||
| Nutzungsbedingungen | Ja | TODO: URL verifizieren |
|
||||
| DSB-Kontakt | In DSI | — |
|
||||
**ACHTUNG: BMW verteilt Rechtstexte ueber 3 Domains!**
|
||||
|
||||
| Dokumenttyp | Domain | URL |
|
||||
|-------------|--------|-----|
|
||||
| DSI | bmw.de | https://www.bmw.de/de/footer/metanavigation/data-privacy.html |
|
||||
| Impressum | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-notice-pool/imprint.html |
|
||||
| Cookie-Richtlinie | bmw.de | https://www.bmw.de/de/footer/footer-section/cookie-policy.html |
|
||||
| Legal Disclaimer / NB | bmw.de | https://www.bmw.de/de/footer/metanavigation/legal-disclaimer-pool/legal-disclaimer.html |
|
||||
| Konzern-Datenschutz + Widerruf | **bmwgroup.com** | https://www.bmwgroup.com/de/general/data_privacy.html |
|
||||
| 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)
|
||||
|
||||
### L1 Checks (8/9)
|
||||
### L1 Checks
|
||||
|
||||
| Check | Erwartet | Begruendung |
|
||||
|-------|----------|-------------|
|
||||
| Verantwortlicher | PASS | BMW AG, Muenchen |
|
||||
| DSB | PASS | DSB erwaehnt |
|
||||
| Zwecke | PASS | Ausfuehrlich |
|
||||
| Verantwortlicher | PASS | BMW AG, Petuelring 130, 80809 Muenchen |
|
||||
| DSB | PASS | datenschutz@bmw.de, Petuelring 130 |
|
||||
| Zwecke | PASS | Ausfuehrlich (Sub-Pages) |
|
||||
| Rechtsgrundlage | PASS | Art. 6 Referenzen |
|
||||
| Empfaenger | PASS | Kategorien aufgezaehlt |
|
||||
| Drittlandtransfer | PASS | USA-Transfer erwaehnt |
|
||||
| Speicherdauer | PASS | Zeitangaben vorhanden |
|
||||
| Betroffenenrechte | **FAIL** | Rechte ohne Art.-Referenzen |
|
||||
| Beschwerderecht | **FAIL** | Art. 77 nicht explizit erwaehnt |
|
||||
|
||||
### 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.**
|
||||
| Betroffenenrechte | Zu pruefen | Art. 15-21 in DSI? |
|
||||
| Beschwerderecht | Zu pruefen | Art. 77 in DSI? |
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|-------|----------|
|
||||
| Cookie-Arten | PASS (Essential, Analytics, Marketing) |
|
||||
| Cookie-Zwecke | PASS |
|
||||
| Speicherdauern | TODO: verifizieren |
|
||||
| Drittanbieter | PASS (Google, Meta etc.) |
|
||||
| Rechtsgrundlage | TODO: §25 TDDDG? |
|
||||
| Consent-Tool | PASS (OneTrust o.ae.) |
|
||||
| Firmenname | PASS (BMW AG) |
|
||||
| Anschrift | PASS (Petuelring 130, 80809 Muenchen) |
|
||||
| Vertretung | PASS (Vorstand benannt) |
|
||||
| USt-IdNr | PASS (DE129273398) |
|
||||
| Registergericht | PASS (HRB 42243) |
|
||||
| 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 |
|
||||
| provider | OneTrust oder aehnlich |
|
||||
| violations | Mehrere (grosser Konzern mit viel Tracking) |
|
||||
|
||||
---
|
||||
|
||||
## Cross-Check Banner vs Cookie
|
||||
|
||||
| Finding | Erwartet |
|
||||
|---------|----------|
|
||||
| Dienste fehlen in Cookie-RL | Moeglich (viele Third-Party-Tracker) |
|
||||
| Tracking vor Consent | Moeglich (Pre-Consent Analytics) |
|
||||
| violations | Zu pruefen |
|
||||
|
||||
---
|
||||
|
||||
@@ -124,6 +120,6 @@
|
||||
| Check | Filter | Begruendung |
|
||||
|-------|--------|-------------|
|
||||
| ODR | AKTIV | B2C mit Online-Angebot |
|
||||
| Widerruf | AKTIV | B2C |
|
||||
| Widerruf | In DSI | Marketing-Consent widerrufbar |
|
||||
| Berufsrecht | SKIP | Kein regulierter Beruf |
|
||||
| V.i.S.d.P. | AKTIV | Hat Magazine/Blog |
|
||||
|
||||
@@ -2,172 +2,185 @@
|
||||
|
||||
**URL:** https://www.spiegel.de
|
||||
**Typ:** Medien / Nachrichtenportal
|
||||
**Datum:** 2026-05-13 (verifiziert gegen Live-Texte)
|
||||
**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.705 Woerter, 107.720 Zeichen)
|
||||
**Root Cause aller FN:** API-Limit `text[:50000]` schnitt bei 47% ab → DSB/Art.77/Rechte fehlten
|
||||
**Datum:** 2026-05-14 (verifiziert gegen Live-Texte + System-Ergebnis)
|
||||
**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 |
|
||||
|------|----------------|-------------|
|
||||
| business_type | b2c | Abo-Modell (Spiegel+) |
|
||||
| industry | media | Nachrichtenportal |
|
||||
| has_online_shop | true | Spiegel+ Abo-Shop |
|
||||
| has_editorial_content | true | Kerngeschaeft |
|
||||
| is_regulated_profession | **false** | Kein regulierter Beruf. "Anwalt" im Text ist Redaktionsanwalt, kein Kanzlei-Beruf |
|
||||
| needs_odr | true | B2C mit Online-Abo |
|
||||
|
||||
**Bug:** Profiler erkennt "anwalt" im Impressum-Text und setzt is_regulated_profession=true. FALSE POSITIVE.
|
||||
| Feld | Erwartet | System-Ergebnis | |
|
||||
|------|---------|----------------|---|
|
||||
| business_type | b2c | B2C | ✓ |
|
||||
| industry | media | media | ✓ |
|
||||
| has_online_shop | true | true | ✓ |
|
||||
| has_editorial_content | true | true | ✓ |
|
||||
| is_regulated_profession | false | false | ✓ (gefixt, war FP "anwalt") |
|
||||
| needs_odr | true | true | ✓ |
|
||||
| detected_services | 31 | 10 angezeigt (31 intern) | UI zeigt nur Top 10 |
|
||||
|
||||
---
|
||||
|
||||
## Dokumente
|
||||
|
||||
| Dokumenttyp | Vorhanden | URL | Anmerkung |
|
||||
|-------------|-----------|-----|-----------|
|
||||
| DSI | Ja | https://www.spiegel.de/datenschutz-spiegel | 6461 Woerter, 11 Abschnitte, sehr ausfuehrlich |
|
||||
| Impressum | Ja | https://www.spiegel.de/impressum | 2 Gesellschaften (DER SPIEGEL GmbH + SPIEGEL-Verlag) |
|
||||
| Cookie-Richtlinie | In DSI Abschnitt 4 | #funktionsfaehigkeitdesangebots | Sourcepoint CMP |
|
||||
| AGB | Ja | https://www.spiegel.de/agb | Abo-Bedingungen |
|
||||
| Nutzungsbedingungen | Ja | https://www.spiegel.de/nutzungsbedingungen | Separates Dokument |
|
||||
| Widerruf | In AGB Abschnitt 10 | https://www.spiegel.de/agb | "Widerrufsrecht fuer Abonnements" |
|
||||
| Social Media DSE | In DSI Abschnitt 8 | #einbinden-von-drittinhalten | Facebook, YouTube, X, Instagram, TikTok, etc. |
|
||||
| DSB-Kontakt | In DSI | — | dsb@spiegelgruppe.de |
|
||||
| Dokumenttyp | Vorhanden | URL | System-Ergebnis |
|
||||
|-------------|-----------|-----|----------------|
|
||||
| DSI | Ja | https://www.spiegel.de/datenschutz-spiegel | **9/9 L1 (100%)** ✓ |
|
||||
| Impressum | Ja | https://www.spiegel.de/impressum | **9/13 L1 (86%)** ✓ |
|
||||
| Social Media | In DSI (Abschnitt 8) | auto-filled | **10/10 L1 (100%)** ✓ |
|
||||
| Cookie-RL | In DSI (Abschnitt 4) | auto-filled | 1/6 L1 (17%) |
|
||||
| AGB | Ja | https://www.spiegel.de/agb | Nicht eingegeben |
|
||||
| Nutzungsbedingungen | Ja | https://www.spiegel.de/nutzungsbedingungen | 5/12 L1 (42%) |
|
||||
| Widerruf | In AGB §10 | Falsch zugewiesen (NB-Text) | 0/8 L1 (0%) |
|
||||
| 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? |
|
||||
|-------|----------|-------|----------------|------|
|
||||
| Verantwortlicher | PASS | "DER SPIEGEL GmbH & Co. KG, Ericusspitze 1, 20459 Hamburg" | PASS (3/3) | — |
|
||||
| DSB | **PASS** | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" | **FAIL** | **FN — Regex matcht "Datenschutzbeauftragte" nicht ohne "r" am Ende oder erkennt Kontext nicht** |
|
||||
| Zwecke | PASS | Adobe-Tracking, Vertragsbeziehungen, Drittinhalte etc. | PASS | — |
|
||||
| Rechtsgrundlage | PASS | Art. 6(1)(a), (b), (f) explizit | PASS (3/4) | — |
|
||||
| Empfaenger | PASS | Server-/Applikationsbetreiber, Auftragsverarbeiter | PASS (2/2) | — |
|
||||
| Drittlandtransfer | PASS | SCC erwaehnt | PASS (1/1) | — |
|
||||
| Speicherdauer | PASS | "30 Tage" Protokolldatei | PASS (1/2) | — |
|
||||
| Betroffenenrechte | **PASS** | Art. 15, 16, 17, 18, 21 explizit. Art. 20 fehlt. | **FAIL** | **FN — Regex verlangt alle 6 Artikel, 5/6 genuegen nicht** |
|
||||
| Beschwerderecht | **PASS** | "Art. 77 DSGVO... HmbBfDI... Ludwig-Ehrhard-Str. 22" | **FAIL** | **FN — Regex findet Art. 77 + HmbBfDI nicht** |
|
||||
| Check | Erwartet | System | Beleg |
|
||||
|-------|----------|--------|-------|
|
||||
| Verantwortlicher | PASS | PASS (3/3) | Ericusspitze 1, 20459 Hamburg |
|
||||
| DSB | PASS | PASS (1/1) | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" |
|
||||
| Zwecke | PASS | PASS (1/1) | Adobe-Tracking, Vertragsbeziehungen etc. |
|
||||
| Rechtsgrundlage | PASS | PASS (3/4) | Art. 6(1)(a), (b), (f) |
|
||||
| Empfaenger | PASS | PASS (2/2) | AVV erwaehnt |
|
||||
| Drittlandtransfer | PASS | PASS (1/1) | SCC erwaehnt |
|
||||
| Speicherdauer | PASS | PASS (2/2) | "30 Tage", Loeschfristen |
|
||||
| Betroffenenrechte | PASS | PASS (6/7) | Art. 15-18, 20, 21. Art. 22 fehlt (TP) |
|
||||
| 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 | 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 |
|
||||
| Check | Status | Begruendung |
|
||||
|-------|--------|-------------|
|
||||
| ODR | AKTIV | B2C Online-Abo |
|
||||
| Widerruf | AKTIV | B2C |
|
||||
| V.i.S.d.P. | AKTIV | Medienunternehmen (Kernpflicht) |
|
||||
| Berufsrecht | **SKIP** | Kein regulierter Beruf |
|
||||
| Interessenabwaegung | FAIL (TP) | Interesse benannt, keine Abwaegung dokumentiert |
|
||||
| Art. 22 Profiling | FAIL (TP) | Nicht erwaehnt trotz personalisierter Werbung |
|
||||
|
||||
---
|
||||
|
||||
## Identifizierte Regex-Bugs (aus diesem GT-Abgleich)
|
||||
## Impressum — 9/13 L1, 9/31 L2
|
||||
|
||||
| # | Check | Bug | Beleg auf Website | Regex-Problem |
|
||||
|---|-------|-----|-------------------|---------------|
|
||||
| 1 | DSB | FN | "z. Hd. der Datenschutzbeauftragten... dsb@spiegelgruppe.de" | Regex matcht "Datenschutzbeauftragten" (Genitiv/Dativ) nicht |
|
||||
| 2 | Beschwerderecht | FN | "Art. 77 DSGVO... HmbBfDI" | Regex findet "Art. 77" oder "Aufsichtsbehoerde" nicht im Spiegel-Text |
|
||||
| 3 | Betroffenenrechte | FN | Art. 15, 16, 17, 18, 21 — nur Art. 20 fehlt | Regex verlangt ALLE 6, 5/6 ist nicht genug |
|
||||
| 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 |
|
||||
| 5 | USt-IdNr | FN | "Umsatzsteuer-ID: DE 212 442 423" | Regex sucht "ust-idnr" oder "ust-id", matcht "umsatzsteuer-id:" nicht |
|
||||
| 6 | Profiler "anwalt" | FP | Redaktionsanwalt im Impressum | "anwalt" zu generisch, matcht Personennamen/Rollen |
|
||||
| Check | Erwartet | System | |
|
||||
|-------|----------|--------|---|
|
||||
| Firmenname | PASS | PASS | ✓ |
|
||||
| Anschrift | PASS | PASS (2/2) | ✓ |
|
||||
| Kontakt | PASS | PASS (2/2) | ✓ |
|
||||
| Register | PASS | PASS (2/2) | ✓ |
|
||||
| USt-IdNr | PASS | PASS (1/1) | ✓ Gefixt ("Umsatzsteuer-ID:" + DE mit Leerzeichen) |
|
||||
| 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
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user