Compare commits
102 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 | |||
| a530edb994 | |||
| 256deb70c7 | |||
| eac42d4154 | |||
| 33bf2b7c5a | |||
| 3e61f381a7 | |||
| cca714755a | |||
| 6940271672 | |||
| 5e317d2f0f | |||
| 64e3a47b8c | |||
| 81a0568537 | |||
| d0d1b38f5c | |||
| d31c2fe018 | |||
| 8ad0519367 | |||
| 7a5301064c | |||
| b2c1f0ae84 | |||
| 733d2bcc7b | |||
| 977e63f372 | |||
| be2ac762bd | |||
| 1bd892afbf | |||
| c702260ec1 | |||
| 8bb90d73e5 | |||
| 185d680669 | |||
| 0b9150f16f | |||
| 0326d5baab | |||
| c867478791 | |||
| 979fe20ea5 | |||
| de808190dd | |||
| 08fcb5f239 | |||
| e785b6d695 | |||
| 7be34552bb | |||
| be9cfdc2d4 | |||
| b42e1cd091 | |||
| 1c828a5843 | |||
| 4a7e09bbb0 | |||
| edbf6d2be5 | |||
| 06bfbd1dca | |||
| 74f00bbb0f | |||
| 128967fa3d | |||
| baca0f6b80 | |||
| 407a9503e4 | |||
| 1fd7ea6139 | |||
| ce77cde309 | |||
| a127dd971b | |||
| 65b4857be5 | |||
| 93028b443e | |||
| 7d9f5a1f76 | |||
| 6ce5b4bf41 | |||
| 078f936449 | |||
| ed3ebbc246 | |||
| 4e865d2997 | |||
| f5664612ad | |||
| 134b7e7709 | |||
| 12f2503873 | |||
| 6586d2cb5e | |||
| df15f6f098 |
@@ -115,5 +115,38 @@ docs-src/control_generator_routes.py
|
||||
consent-sdk/src/mobile/flutter/consent_sdk.dart
|
||||
consent-sdk/src/mobile/ios/ConsentManager.swift
|
||||
|
||||
# --- consent-tester: DSI discovery orchestrator ---
|
||||
# Single Playwright session with sequential steps (banner dismiss, self-extract,
|
||||
# link follow, accordion expand, inline sections). Splitting mid-session would
|
||||
# require passing Page objects across modules.
|
||||
consent-tester/services/dsi_discovery.py
|
||||
|
||||
# --- backend-compliance: unified compliance check orchestrator ---
|
||||
# Sequential 7-step pipeline (text resolve, profile detect, check documents,
|
||||
# banner scan, cross-check, profile extract, report). Phase 5 split target.
|
||||
backend-compliance/compliance/api/agent_compliance_check_routes.py
|
||||
|
||||
# --- docs-src: binary office files (not source code) ---
|
||||
# (Also excluded by extension in scripts/check-loc.sh — kept here for legibility.)
|
||||
docs-src/Breakpilot ComplAI Finanzplan.xlsm
|
||||
|
||||
# --- admin-compliance: oversized component refactor backlog ---
|
||||
# Phase 5+ target for splitting into smaller subcomponents per wizard step.
|
||||
admin-compliance/components/sdk/ai-act/DecisionTreeWizard.tsx
|
||||
|
||||
# --- ai-compliance-sdk: oversized handler refactor backlog ---
|
||||
# Phase 5+ target for splitting handler groups into per-resource files.
|
||||
ai-compliance-sdk/internal/api/handlers/tender_handlers.go
|
||||
|
||||
# --- merge grandfathered (2026-05-13) — Phase 5+ refactor backlog ---
|
||||
# Files imported via team work that crossed the hard cap; tracked for splitting.
|
||||
consent-tester/checks/banner_checks.py
|
||||
consent-tester/services/banner_detector.py
|
||||
backend-compliance/compliance/api/agent_doc_check_routes.py
|
||||
backend-compliance/compliance/services/service_registry.py
|
||||
backend-compliance/compliance/services/dsr_workflow_service.py
|
||||
ai-compliance-sdk/internal/iace/hazard_patterns_forestry_conveyor.go
|
||||
admin-compliance/app/sdk/compliance-scope/page.tsx
|
||||
|
||||
# --- zeroclaw: ground-truth corpus (test fixture data, not source) ---
|
||||
zeroclaw/docs/ground-truth/06-spiegel-dsi-fulltext.txt
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Build + push compliance service images to registry.meghsakha.com
|
||||
# and trigger orca redeploy on every push to main that touches a service.
|
||||
# and trigger orca redeploy after CI passes on main.
|
||||
#
|
||||
# This workflow is gated on the CI workflow completing successfully.
|
||||
# It does not run independently — if CI fails, builds + deploy are skipped.
|
||||
# Per-service builds are gated on detect-changes so only services with
|
||||
# modified files are rebuilt; trigger-orca runs only if at least one build
|
||||
# succeeded and none failed.
|
||||
#
|
||||
# Requires Gitea Actions secrets:
|
||||
# REGISTRY_USERNAME / REGISTRY_PASSWORD — registry.meghsakha.com credentials
|
||||
@@ -8,24 +14,68 @@
|
||||
name: Build + Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'admin-compliance/**'
|
||||
- 'backend-compliance/**'
|
||||
- 'ai-compliance-sdk/**'
|
||||
- 'developer-portal/**'
|
||||
- 'compliance-tts-service/**'
|
||||
- 'document-crawler/**'
|
||||
- 'dsms-gateway/**'
|
||||
- 'dsms-node/**'
|
||||
|
||||
jobs:
|
||||
# ── per-service builds run in parallel ────────────────────────────────────
|
||||
# ── gate: only proceed if CI succeeded ────────────────────────────────────
|
||||
ci-passed:
|
||||
runs-on: docker
|
||||
container: alpine:3.20
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: CI passed, proceeding with build + deploy
|
||||
run: echo "CI run ${{ github.event.workflow_run.id }} succeeded on ${{ github.event.workflow_run.head_branch }} @ ${{ github.event.workflow_run.head_sha }}"
|
||||
|
||||
# ── detect which services changed since the last successful build ────────
|
||||
# Diff base = the last-build/main git tag, set by mark-last-build at the
|
||||
# end of every successful run. Works across squash merges, multi-commit
|
||||
# raw pushes, and force pushes (force pushes leave a stale tag → diff
|
||||
# shows symmetric differences → safe over-rebuild). If the tag doesn't
|
||||
# exist yet, scripts/detect-changes.sh falls back to rebuilding all.
|
||||
detect-changes:
|
||||
runs-on: docker
|
||||
container: alpine:3.20
|
||||
needs: ci-passed
|
||||
outputs:
|
||||
admin: ${{ steps.diff.outputs.admin }}
|
||||
backend: ${{ steps.diff.outputs.backend }}
|
||||
sdk: ${{ steps.diff.outputs.sdk }}
|
||||
portal: ${{ steps.diff.outputs.portal }}
|
||||
tts: ${{ steps.diff.outputs.tts }}
|
||||
crawler: ${{ steps.diff.outputs.crawler }}
|
||||
dsms_gateway: ${{ steps.diff.outputs.dsms_gateway }}
|
||||
dsms_node: ${{ steps.diff.outputs.dsms_node }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git bash
|
||||
git clone --depth 200 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
git fetch --tags origin || true
|
||||
- name: Resolve base SHA from last-build/main tag
|
||||
run: |
|
||||
BASE=$(git rev-parse --verify refs/tags/last-build/main 2>/dev/null || true)
|
||||
echo "Base SHA: ${BASE:-<none, will rebuild all>}"
|
||||
# Deepen if base isn't yet in the shallow clone.
|
||||
if [ -n "$BASE" ] && ! git rev-parse --verify "${BASE}^{commit}" >/dev/null 2>&1; then
|
||||
git fetch --unshallow origin 2>/dev/null \
|
||||
|| git fetch --depth=10000 origin 2>/dev/null \
|
||||
|| true
|
||||
fi
|
||||
echo "BASE_SHA=${BASE}" >> "$GITHUB_ENV"
|
||||
- name: Detect changes
|
||||
id: diff
|
||||
run: bash scripts/detect-changes.sh
|
||||
|
||||
# ── per-service builds run in parallel (only changed services) ────────────
|
||||
|
||||
build-admin-compliance:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.admin == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -49,6 +99,8 @@ jobs:
|
||||
build-backend-compliance:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.backend == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -72,6 +124,8 @@ jobs:
|
||||
build-ai-sdk:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.sdk == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -95,6 +149,8 @@ jobs:
|
||||
build-developer-portal:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.portal == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -118,6 +174,8 @@ jobs:
|
||||
build-tts:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.tts == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -141,6 +199,8 @@ jobs:
|
||||
build-document-crawler:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.crawler == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -164,6 +224,8 @@ jobs:
|
||||
build-dsms-gateway:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.dsms_gateway == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -187,6 +249,8 @@ jobs:
|
||||
build-dsms-node:
|
||||
runs-on: docker
|
||||
container: docker:27-cli
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.dsms_node == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -207,7 +271,52 @@ jobs:
|
||||
docker push registry.meghsakha.com/breakpilot/compliance-dsms-node:latest
|
||||
docker push registry.meghsakha.com/breakpilot/compliance-dsms-node:${SHORT_SHA}
|
||||
|
||||
# ── orca redeploy (only after all builds succeed) ─────────────────────────
|
||||
# ── advance the last-build/main tag — the diff base for future runs ──────
|
||||
# Runs when no build failed. Covers two cases:
|
||||
# - at least one service was rebuilt → mark this SHA as the new baseline
|
||||
# - all services were skipped (nothing changed) → still advance the tag
|
||||
# so we don't keep re-evaluating the same skipped commits forever
|
||||
# Skips if any build failed → tag stays put → next push retries those
|
||||
# services from the previous known-good base.
|
||||
mark-last-build:
|
||||
runs-on: docker
|
||||
container: alpine:3.20
|
||||
needs:
|
||||
- build-admin-compliance
|
||||
- build-backend-compliance
|
||||
- build-ai-sdk
|
||||
- build-developer-portal
|
||||
- build-tts
|
||||
- build-document-crawler
|
||||
- build-dsms-gateway
|
||||
- build-dsms-node
|
||||
if: |
|
||||
always() &&
|
||||
!contains(needs.*.result, 'failure') &&
|
||||
!contains(needs.*.result, 'cancelled')
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git
|
||||
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
- name: Force-push last-build/main tag
|
||||
run: |
|
||||
set -e
|
||||
SHA="${HEAD_SHA:-$(git rev-parse HEAD)}"
|
||||
echo "Advancing last-build/main → ${SHA}"
|
||||
git tag -f last-build/main "$SHA"
|
||||
# Encode token into the push URL (no on-disk credential persistence).
|
||||
PUSH_URL="${GITHUB_SERVER_URL/https:\/\//https:\/\/x-access-token:${GITEA_TOKEN}@}/${GITHUB_REPOSITORY}.git"
|
||||
git push --force "$PUSH_URL" "refs/tags/last-build/main"
|
||||
echo "Tag last-build/main now at ${SHA}"
|
||||
|
||||
# ── orca redeploy — runs only if at least one build succeeded ─────────────
|
||||
# `always()` lets this run when some builds are skipped (unchanged services).
|
||||
# The contains() checks ensure we only redeploy when something actually built
|
||||
# and no build failed.
|
||||
|
||||
trigger-orca:
|
||||
runs-on: docker
|
||||
@@ -221,6 +330,11 @@ jobs:
|
||||
- build-document-crawler
|
||||
- build-dsms-gateway
|
||||
- build-dsms-node
|
||||
if: |
|
||||
always() &&
|
||||
contains(needs.*.result, 'success') &&
|
||||
!contains(needs.*.result, 'failure') &&
|
||||
!contains(needs.*.result, 'cancelled')
|
||||
steps:
|
||||
- name: Checkout (for SHA)
|
||||
run: |
|
||||
|
||||
@@ -19,6 +19,49 @@ on:
|
||||
|
||||
jobs:
|
||||
|
||||
# ── Change detection (always runs first) ─────────────────────────────────
|
||||
# Diff base:
|
||||
# PR → merge-base with the PR base branch
|
||||
# push → last-build/main tag (set by build-push-deploy after a green build)
|
||||
# Falls back to "rebuild all" when the base is missing or unreachable.
|
||||
detect-changes:
|
||||
runs-on: docker
|
||||
container: alpine:3.20
|
||||
outputs:
|
||||
admin: ${{ steps.diff.outputs.admin }}
|
||||
backend: ${{ steps.diff.outputs.backend }}
|
||||
sdk: ${{ steps.diff.outputs.sdk }}
|
||||
portal: ${{ steps.diff.outputs.portal }}
|
||||
tts: ${{ steps.diff.outputs.tts }}
|
||||
crawler: ${{ steps.diff.outputs.crawler }}
|
||||
dsms_gateway: ${{ steps.diff.outputs.dsms_gateway }}
|
||||
dsms_node: ${{ steps.diff.outputs.dsms_node }}
|
||||
any_python: ${{ steps.diff.outputs.any_python }}
|
||||
any_node: ${{ steps.diff.outputs.any_node }}
|
||||
any: ${{ steps.diff.outputs.any }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
apk add --no-cache git bash
|
||||
git clone --depth 200 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
|
||||
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
|
||||
git fetch --depth 200 origin "${GITHUB_BASE_REF}" || true
|
||||
else
|
||||
git fetch --tags origin || true
|
||||
fi
|
||||
- name: Resolve base SHA
|
||||
run: |
|
||||
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
|
||||
BASE=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD 2>/dev/null || true)
|
||||
else
|
||||
BASE=$(git rev-parse --verify refs/tags/last-build/main 2>/dev/null || true)
|
||||
fi
|
||||
echo "Base SHA: ${BASE:-<none>}"
|
||||
echo "BASE_SHA=${BASE}" >> "$GITHUB_ENV"
|
||||
- name: Detect changes
|
||||
id: diff
|
||||
run: bash scripts/detect-changes.sh
|
||||
|
||||
# ── Branch naming convention (PR only) ──────────────────────────────────
|
||||
branch-name:
|
||||
runs-on: docker
|
||||
@@ -55,10 +98,12 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── LOC budget (always) ──────────────────────────────────────────────────
|
||||
# ── LOC budget (only if files changed) ───────────────────────────────────
|
||||
loc-budget:
|
||||
runs-on: docker
|
||||
container: alpine:3.20
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.any == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -86,10 +131,11 @@ jobs:
|
||||
--redact \
|
||||
|| { echo "::error::Secrets detected — remove them before merging."; exit 1; }
|
||||
|
||||
# ── Go lint + build (PR only) ────────────────────────────────────────────
|
||||
# ── Go lint + build (PR only, gated on ai-compliance-sdk changes) ────────
|
||||
go-lint:
|
||||
runs-on: docker
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.sdk == 'true'
|
||||
container: golangci/golangci-lint:v1.62-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -107,10 +153,11 @@ jobs:
|
||||
cd ai-compliance-sdk
|
||||
go build ./...
|
||||
|
||||
# ── Python lint + import check (PR only) ────────────────────────────────
|
||||
# ── Python lint + import check (PR only, gated on python service changes) ─
|
||||
python-lint:
|
||||
runs-on: docker
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any_python == 'true'
|
||||
container: python:3.12-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -137,10 +184,11 @@ jobs:
|
||||
python -c "import compliance; print('Import OK')" \
|
||||
|| { echo "::error::compliance package fails to import — missing import or syntax error."; exit 1; }
|
||||
|
||||
# ── Node.js lint + type-check (PR only) ─────────────────────────────────
|
||||
# ── Node.js lint + type-check (PR only, gated on Next.js service changes) ─
|
||||
nodejs-lint:
|
||||
runs-on: docker
|
||||
if: github.event_name == 'pull_request'
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any_node == 'true'
|
||||
container: node:20-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -158,10 +206,12 @@ jobs:
|
||||
done
|
||||
exit $fail
|
||||
|
||||
# ── Node.js build — next build (PR + push to main) ───────────────────────
|
||||
# ── Node.js build — next build (gated on Next.js service changes) ───────
|
||||
nodejs-build:
|
||||
runs-on: docker
|
||||
container: node:20-alpine
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.any_node == 'true'
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@@ -244,10 +294,12 @@ jobs:
|
||||
- name: Vulnerability scan (fail on high+)
|
||||
run: grype sbom:sbom-out/sbom.cdx.json --fail-on high -q
|
||||
|
||||
# ── Tests (PR + push to main) ─────────────────────────────────────────────
|
||||
# ── Tests (gated per service) ────────────────────────────────────────────
|
||||
test-go:
|
||||
runs-on: docker
|
||||
container: golang:1.24-alpine
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.sdk == 'true'
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
steps:
|
||||
@@ -265,6 +317,8 @@ jobs:
|
||||
test-python-backend:
|
||||
runs-on: docker
|
||||
container: python:3.12-slim
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.backend == 'true'
|
||||
env:
|
||||
CI: "true"
|
||||
steps:
|
||||
@@ -284,6 +338,8 @@ jobs:
|
||||
test-python-document-crawler:
|
||||
runs-on: docker
|
||||
container: python:3.12-slim
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.crawler == 'true'
|
||||
env:
|
||||
CI: "true"
|
||||
steps:
|
||||
@@ -303,6 +359,8 @@ jobs:
|
||||
test-python-dsms-gateway:
|
||||
runs-on: docker
|
||||
container: python:3.12-slim
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.dsms_gateway == 'true'
|
||||
env:
|
||||
CI: "true"
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Vendor Assessment Status/Detail Proxy
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${BACKEND_URL}/api/vendor-compliance/assessments/${id}`,
|
||||
{ signal: AbortSignal.timeout(10000) },
|
||||
)
|
||||
const data = await resp.json()
|
||||
return NextResponse.json(data, { status: resp.status })
|
||||
} catch (error) {
|
||||
console.error('Assessment status proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Backend nicht erreichbar' },
|
||||
{ status: 503 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${BACKEND_URL}/api/vendor-compliance/assessments/${id}/approve`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
},
|
||||
)
|
||||
const data = await resp.json()
|
||||
return NextResponse.json(data, { status: resp.status })
|
||||
} catch (error) {
|
||||
console.error('Assessment approve proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Backend nicht erreichbar' },
|
||||
{ status: 503 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Vendor Assessment API Proxy
|
||||
* Proxies to backend-compliance (Python FastAPI)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
const BACKEND_URL = process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.text()
|
||||
const resp = await fetch(`${BACKEND_URL}/api/vendor-compliance/assessments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
const data = await resp.json()
|
||||
return NextResponse.json(data, { status: resp.status })
|
||||
} catch (error) {
|
||||
console.error('Vendor assessment proxy error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Backend nicht erreichbar' },
|
||||
{ status: 503 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const resp = await fetch(`${BACKEND_URL}/api/vendor-compliance/assessments`, {
|
||||
signal: AbortSignal.timeout(10000),
|
||||
})
|
||||
const data = await resp.json()
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Vendor assessment list proxy error:', error)
|
||||
return NextResponse.json({ assessments: [] })
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,13 @@ interface DocResult {
|
||||
checks: CheckItem[]
|
||||
findings_count: number
|
||||
error: string
|
||||
scenario?: string // regenerate | fix | import | skip
|
||||
}
|
||||
|
||||
const SCENARIO_LABELS: Record<string, { label: string; color: string; bg: string }> = {
|
||||
regenerate: { label: 'Neugenerierung', color: 'text-red-700', bg: 'bg-red-100' },
|
||||
fix: { label: 'Korrekturen', color: 'text-amber-700', bg: 'bg-amber-100' },
|
||||
import: { label: 'Konform', color: 'text-green-700', bg: 'bg-green-100' },
|
||||
}
|
||||
|
||||
const DOC_TYPE_LABELS: Record<string, string> = {
|
||||
@@ -46,7 +53,7 @@ function groupChecks(checks: CheckItem[]): GroupedCheck[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function CheckIcon({ passed, skipped }: { passed: boolean; skipped?: boolean }) {
|
||||
function CheckIcon({ passed, skipped, isInfo }: { passed: boolean; skipped?: boolean; isInfo?: boolean }) {
|
||||
if (skipped) {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-gray-300 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -61,6 +68,13 @@ function CheckIcon({ passed, skipped }: { passed: boolean; skipped?: boolean })
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
if (isInfo) {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg className="w-4 h-4 text-red-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -84,14 +98,23 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
|
||||
if (!results || results.length === 0) return null
|
||||
|
||||
const totalOk = results.filter(r => r.completeness_pct === 100).length
|
||||
const scenarioCounts = {
|
||||
regenerate: results.filter(r => r.scenario === 'regenerate').length,
|
||||
fix: results.filter(r => r.scenario === 'fix').length,
|
||||
import: results.filter(r => r.scenario === 'import').length,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<h3 className="text-sm font-semibold text-gray-800">
|
||||
Dokumenten-Pruefung ({results.length} Dokumente, {totalOk} vollstaendig)
|
||||
Dokumenten-Pruefung ({results.length} Dokumente)
|
||||
</h3>
|
||||
<div className="flex gap-2 text-[10px]">
|
||||
{scenarioCounts.import > 0 && <span className="bg-green-100 text-green-700 px-2 py-0.5 rounded-full">{scenarioCounts.import} konform</span>}
|
||||
{scenarioCounts.fix > 0 && <span className="bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full">{scenarioCounts.fix} Korrekturen</span>}
|
||||
{scenarioCounts.regenerate > 0 && <span className="bg-red-100 text-red-700 px-2 py-0.5 rounded-full">{scenarioCounts.regenerate} Neugenerierung</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -104,8 +127,9 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
const typeLabel = DOC_TYPE_LABELS[r.doc_type] || r.doc_type
|
||||
const grouped = groupChecks(r.checks)
|
||||
const l1Checks = r.checks.filter(c => (c.level ?? 1) === 1)
|
||||
const l1Scoreable = l1Checks.filter(c => c.severity !== 'INFO')
|
||||
const l2Active = r.checks.filter(c => (c.level ?? 1) === 2 && !c.skipped)
|
||||
const l1Passed = l1Checks.filter(c => c.passed).length
|
||||
const l1Passed = l1Scoreable.filter(c => c.passed).length
|
||||
const l2Passed = l2Active.filter(c => c.passed).length
|
||||
|
||||
return (
|
||||
@@ -123,10 +147,17 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
{typeLabel}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium text-gray-900 truncate">{r.label}</div>
|
||||
<div className="text-sm font-medium text-gray-900 truncate flex items-center gap-2">
|
||||
{r.label}
|
||||
{r.scenario && SCENARIO_LABELS[r.scenario] && (
|
||||
<span className={`text-[10px] px-1.5 py-0.5 rounded-full font-medium ${SCENARIO_LABELS[r.scenario].bg} ${SCENARIO_LABELS[r.scenario].color}`}>
|
||||
{SCENARIO_LABELS[r.scenario].label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 truncate">
|
||||
{l1Checks.length > 0
|
||||
? `${l1Passed}/${l1Checks.length} Pflichtangaben`
|
||||
? `${l1Passed}/${l1Scoreable.length} Pflichtangaben`
|
||||
+ (l2Active.length > 0 ? `, ${l2Passed}/${l2Active.length} Detailpruefungen` : '')
|
||||
: r.url}
|
||||
</div>
|
||||
@@ -137,8 +168,9 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
<span className="text-xs text-red-600 font-medium">Fehler</span>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-16 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="flex items-center gap-2" title={`Pflichtangaben: ${l1Passed}/${l1Scoreable.length}`}>
|
||||
<span className="text-[10px] text-gray-400 w-7">Pflicht</span>
|
||||
<div className="w-14 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full ${barColor}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs font-medium w-10 text-right ${
|
||||
@@ -146,8 +178,9 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
}`}>{pct}%</span>
|
||||
</div>
|
||||
{l2Active.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-16 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="flex items-center gap-2" title={`Detailpruefung: ${l2Passed}/${l2Active.length}`}>
|
||||
<span className="text-[10px] text-gray-400 w-7">Detail</span>
|
||||
<div className="w-14 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full ${cBarColor}`} style={{ width: `${cpct}%` }} />
|
||||
</div>
|
||||
<span className="text-xs font-medium w-10 text-right text-blue-600">{cpct}%</span>
|
||||
@@ -164,13 +197,18 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
<p className="text-sm text-red-600">{r.error}</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{grouped.map((g) => (
|
||||
{grouped.map((g) => {
|
||||
const l1Info = g.check.severity === 'INFO' && !g.check.passed
|
||||
return (
|
||||
<div key={g.check.id}>
|
||||
{/* L1 check */}
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckIcon passed={g.check.passed} />
|
||||
<CheckIcon passed={g.check.passed} isInfo={l1Info} />
|
||||
<div className="flex-1">
|
||||
<div className={`text-sm ${g.check.passed ? 'text-gray-700' : 'text-red-700 font-medium'}`}>
|
||||
<div className={`text-sm ${
|
||||
g.check.passed ? 'text-gray-700'
|
||||
: l1Info ? 'text-gray-500' : 'text-red-700 font-medium'
|
||||
}`}>
|
||||
{g.check.label}
|
||||
{g.children.length > 0 && <L2Summary>{g.children}</L2Summary>}
|
||||
</div>
|
||||
@@ -180,7 +218,7 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
</div>
|
||||
)}
|
||||
{!g.check.passed && g.check.hint && (
|
||||
<div className="text-xs text-red-600/80 mt-0.5">
|
||||
<div className={`text-xs mt-0.5 ${l1Info ? 'text-gray-400' : 'text-red-600/80'}`}>
|
||||
{g.check.hint}
|
||||
</div>
|
||||
)}
|
||||
@@ -190,13 +228,16 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
{/* L2 children — always visible */}
|
||||
{g.children.length > 0 && (
|
||||
<div className="ml-6 mt-0.5 mb-1 space-y-0.5 border-l-2 border-gray-200 pl-3">
|
||||
{g.children.map((ch) => (
|
||||
{g.children.map((ch) => {
|
||||
const chInfo = ch.severity === 'INFO' && !ch.passed && !ch.skipped
|
||||
return (
|
||||
<div key={ch.id} className="flex items-start gap-2">
|
||||
<CheckIcon passed={ch.passed} skipped={ch.skipped} />
|
||||
<CheckIcon passed={ch.passed} skipped={ch.skipped} isInfo={chInfo} />
|
||||
<div className="flex-1">
|
||||
<div className={`text-xs ${
|
||||
ch.skipped ? 'text-gray-400 italic'
|
||||
: ch.passed ? 'text-gray-600' : 'text-red-600 font-medium'
|
||||
: ch.passed ? 'text-gray-600'
|
||||
: chInfo ? 'text-gray-400' : 'text-red-600 font-medium'
|
||||
}`}>
|
||||
{ch.label}
|
||||
{ch.skipped && ' (uebersprungen)'}
|
||||
@@ -207,17 +248,19 @@ export function ChecklistView({ results }: { results: DocResult[] }) {
|
||||
</div>
|
||||
)}
|
||||
{!ch.passed && !ch.skipped && ch.hint && (
|
||||
<div className="text-xs text-red-500/80 mt-0.5">
|
||||
<div className={`text-xs mt-0.5 ${chInfo ? 'text-gray-400' : 'text-red-500/80'}`}>
|
||||
{ch.hint}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
{r.word_count > 0 && (
|
||||
<div className="text-xs text-gray-400 mt-2 pt-2 border-t border-gray-200">
|
||||
{r.word_count} Woerter analysiert
|
||||
|
||||
@@ -29,6 +29,7 @@ type DocsState = Record<DocTypeId, DocState>
|
||||
const STORAGE_KEY_STATE = 'compliance-check-state'
|
||||
const STORAGE_KEY_RESULTS = 'compliance-check-results'
|
||||
const STORAGE_KEY_HISTORY = 'compliance-check-history'
|
||||
const STORAGE_KEY_CHECK_ID = 'compliance-check-active-id'
|
||||
|
||||
function emptyDocState(): DocState {
|
||||
return { url: '', text: '', loading: false, error: null }
|
||||
@@ -77,6 +78,9 @@ export function ComplianceCheckTab() {
|
||||
try { const s = localStorage.getItem(STORAGE_KEY_RESULTS); return s ? JSON.parse(s) : null } catch { return null }
|
||||
})
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [activeCheckId, setActiveCheckId] = useState<string>(() =>
|
||||
typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY_CHECK_ID) || '' : ''
|
||||
)
|
||||
const [history, setHistory] = useState<HistoryEntry[]>(() => {
|
||||
if (typeof window === 'undefined') return []
|
||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY) || '[]') } catch { return [] }
|
||||
@@ -91,6 +95,39 @@ export function ComplianceCheckTab() {
|
||||
try { localStorage.setItem(STORAGE_KEY_STATE, JSON.stringify(toSave)) } catch { /* quota */ }
|
||||
}, [docs])
|
||||
|
||||
// Resume polling if check was in progress when navigating away
|
||||
React.useEffect(() => {
|
||||
if (!activeCheckId || results) return
|
||||
let cancelled = false
|
||||
setLoading(true)
|
||||
setProgress('Pruefung laeuft noch...')
|
||||
const poll = async () => {
|
||||
while (!cancelled) {
|
||||
await new Promise(r => setTimeout(r, 3000))
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/agent/compliance-check?check_id=${activeCheckId}`)
|
||||
if (!res.ok) continue
|
||||
const data = await res.json()
|
||||
if (data.progress) setProgress(data.progress)
|
||||
if (data.status === 'completed' && data.result) {
|
||||
setResults(data.result); setProgress(''); setLoading(false)
|
||||
localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(data.result))
|
||||
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
|
||||
return
|
||||
}
|
||||
if (data.status === 'failed' || data.status === 'not_found') {
|
||||
if (data.status === 'failed') setError(data.error || 'Pruefung fehlgeschlagen')
|
||||
setProgress(''); setLoading(false)
|
||||
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
|
||||
return
|
||||
}
|
||||
} catch { /* retry */ }
|
||||
}
|
||||
}
|
||||
poll()
|
||||
return () => { cancelled = true }
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const updateDoc = useCallback((docType: DocTypeId, patch: Partial<DocState>) => {
|
||||
setDocs(prev => ({ ...prev, [docType]: { ...prev[docType], ...patch } }))
|
||||
}, [])
|
||||
@@ -155,17 +192,19 @@ export function ComplianceCheckTab() {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
entries,
|
||||
documents: entries,
|
||||
use_agent: useAgent,
|
||||
}),
|
||||
})
|
||||
if (!startRes.ok) throw new Error(`Pruefung konnte nicht gestartet werden: ${startRes.status}`)
|
||||
const { check_id } = await startRes.json()
|
||||
if (!check_id) throw new Error('Keine Check-ID erhalten')
|
||||
setActiveCheckId(check_id)
|
||||
localStorage.setItem(STORAGE_KEY_CHECK_ID, check_id)
|
||||
|
||||
// Poll for results
|
||||
// Poll for results (max 25 min = 500 polls x 3s)
|
||||
let attempts = 0
|
||||
while (attempts < 120) {
|
||||
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 }
|
||||
@@ -175,6 +214,7 @@ export function ComplianceCheckTab() {
|
||||
setResults(pollData.result)
|
||||
setProgress('')
|
||||
localStorage.setItem(STORAGE_KEY_RESULTS, JSON.stringify(pollData.result))
|
||||
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
|
||||
|
||||
const resultKey = `compliance-check-result-${Date.now()}`
|
||||
try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch { /* quota */ }
|
||||
@@ -190,11 +230,15 @@ export function ComplianceCheckTab() {
|
||||
break
|
||||
}
|
||||
if (pollData.status === 'failed') {
|
||||
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
|
||||
throw new Error(pollData.error || 'Pruefung fehlgeschlagen')
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
if (attempts >= 120) throw new Error('Zeitlimit ueberschritten')
|
||||
if (attempts >= 500) {
|
||||
localStorage.removeItem(STORAGE_KEY_CHECK_ID); setActiveCheckId('')
|
||||
throw new Error('Zeitlimit ueberschritten (15 Min)')
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||
setProgress('')
|
||||
@@ -320,6 +364,46 @@ export function ComplianceCheckTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extracted Profile — pre-fill suggestion */}
|
||||
{results.extracted_profile?.company_profile && Object.keys(results.extracted_profile.company_profile).length > 0 && (
|
||||
<div className="mb-4 p-3 bg-emerald-50 border border-emerald-200 rounded-lg text-xs">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-semibold text-emerald-900">Aus Dokumenten extrahiert</span>
|
||||
<button className="text-emerald-700 hover:text-emerald-900 text-xs font-medium underline"
|
||||
onClick={() => { /* TODO: navigate to company profile with pre-fill */ }}>
|
||||
In Company Profile uebernehmen
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-emerald-700">
|
||||
{results.extracted_profile.company_profile.companyName && (
|
||||
<span>Firma: <strong>{results.extracted_profile.company_profile.companyName}</strong></span>
|
||||
)}
|
||||
{results.extracted_profile.company_profile.legalForm && (
|
||||
<span>Rechtsform: {results.extracted_profile.company_profile.legalForm.toUpperCase()}</span>
|
||||
)}
|
||||
{results.extracted_profile.company_profile.headquartersCity && (
|
||||
<span>Sitz: {results.extracted_profile.company_profile.headquartersZip} {results.extracted_profile.company_profile.headquartersCity}</span>
|
||||
)}
|
||||
{results.extracted_profile.company_profile.dpoEmail && (
|
||||
<span>DSB: {results.extracted_profile.company_profile.dpoEmail}</span>
|
||||
)}
|
||||
{results.extracted_profile.company_profile.ustIdNr && (
|
||||
<span>USt-IdNr: {results.extracted_profile.company_profile.ustIdNr}</span>
|
||||
)}
|
||||
</div>
|
||||
{results.extracted_profile.compliance_scope_hints?.length > 0 && (
|
||||
<div className="mt-2 pt-2 border-t border-emerald-200 text-emerald-600">
|
||||
<span className="font-medium">Scope-Hinweise: </span>
|
||||
{results.extracted_profile.compliance_scope_hints.map((h: any, i: number) => (
|
||||
<span key={i} className="inline-block bg-emerald-100 rounded px-1.5 py-0.5 mr-1 mb-1">
|
||||
{h.source}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Banner Check Result */}
|
||||
{results.banner_result && (
|
||||
<div className={`mb-4 p-3 rounded-lg border text-xs ${
|
||||
@@ -343,8 +427,8 @@ export function ComplianceCheckTab() {
|
||||
<>
|
||||
Banner erkannt{results.banner_result.provider ? ` (${results.banner_result.provider})` : ''}.
|
||||
{results.banner_result.violations > 0
|
||||
? ` ${results.banner_result.violations} Auffaelligkeit${results.banner_result.violations !== 1 ? 'en' : ''} gefunden — Cross-Check-Ergebnisse sind in der Cookie-Richtlinie-Checkliste enthalten.`
|
||||
: ' Keine Auffaelligkeiten beim Banner-Cookie-Abgleich.'}
|
||||
? ` ${results.banner_result.violations} Auffaelligkeit${results.banner_result.violations !== 1 ? 'en' : ''} gefunden.`
|
||||
: ' Keine Auffaelligkeiten.'}
|
||||
</>
|
||||
) : (
|
||||
'Kein Cookie-Banner erkannt oder Banner-Check nicht moeglich.'
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export interface AuditEntry {
|
||||
id: string
|
||||
entity_type: string
|
||||
entity_id: string
|
||||
entity_name: string
|
||||
action: string
|
||||
field_changed: string | null
|
||||
old_value: string | null
|
||||
new_value: string | null
|
||||
change_summary: string | null
|
||||
performed_by: string
|
||||
performed_at: string
|
||||
}
|
||||
|
||||
export function useAuditTimeline() {
|
||||
const [entries, setEntries] = useState<AuditEntry[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filter, setFilter] = useState<string>('all')
|
||||
|
||||
useEffect(() => {
|
||||
loadEntries()
|
||||
}, [filter]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
async function loadEntries() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({ limit: '100' })
|
||||
if (filter !== 'all') params.set('entity_type', filter)
|
||||
const res = await fetch(`/api/sdk/v1/compliance/audit-trail?${params}`)
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
setEntries(json.entries || json.audit_trail || json || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load audit trail:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return { entries, loading, filter, setFilter }
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client'
|
||||
|
||||
import { useAuditTimeline, type AuditEntry } from './_hooks/useAuditTimeline'
|
||||
|
||||
const ENTITY_LABELS: Record<string, string> = {
|
||||
evidence: 'Nachweis', control: 'Control', document: 'Dokument',
|
||||
dsfa: 'DSFA', vvt: 'VVT', tom: 'TOM', policy: 'Richtlinie',
|
||||
dsms_archive: 'DSMS-Archiv', risk: 'Risiko',
|
||||
}
|
||||
|
||||
const ACTION_COLORS: Record<string, string> = {
|
||||
create: 'bg-green-500', update: 'bg-blue-500', delete: 'bg-red-500',
|
||||
approve: 'bg-purple-500', archive: 'bg-emerald-500', review: 'bg-yellow-500',
|
||||
sign: 'bg-indigo-500', reject: 'bg-red-400',
|
||||
}
|
||||
|
||||
const FILTER_OPTIONS = ['all', 'evidence', 'dsms_archive', 'control', 'document', 'dsfa', 'vvt', 'tom']
|
||||
|
||||
export default function AuditTimelinePage() {
|
||||
const { entries, loading, filter, setFilter } = useAuditTimeline()
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Audit Timeline</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Chronologische Compliance-Historie mit DSMS-Nachweisen</p>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{FILTER_OPTIONS.map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
|
||||
filter === f
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{f === 'all' ? 'Alle' : ENTITY_LABELS[f] || f}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
) : entries.length === 0 ? (
|
||||
<div className="text-center py-16 text-gray-500">
|
||||
Keine Eintraege gefunden. Compliance-Aktionen werden automatisch protokolliert.
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-6 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-gray-700" />
|
||||
|
||||
<div className="space-y-4">
|
||||
{entries.map((entry) => (
|
||||
<TimelineEntry key={entry.id} entry={entry} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TimelineEntry({ entry }: { entry: AuditEntry }) {
|
||||
const dotColor = ACTION_COLORS[entry.action] || 'bg-gray-400'
|
||||
const isCID = entry.field_changed === 'dsms_cid' || entry.action === 'archive'
|
||||
const date = new Date(entry.performed_at)
|
||||
|
||||
return (
|
||||
<div className="relative flex gap-4 pl-3">
|
||||
{/* Dot */}
|
||||
<div className={`relative z-10 w-3 h-3 rounded-full mt-1.5 flex-shrink-0 ring-4 ring-white dark:ring-gray-900 ${dotColor}`} />
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 min-w-0">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">{entry.entity_name}</span>
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||
{ENTITY_LABELS[entry.entity_type] || entry.entity_type}
|
||||
</span>
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-medium text-white ${dotColor}`}>
|
||||
{entry.action}
|
||||
</span>
|
||||
</div>
|
||||
{entry.change_summary && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">{entry.change_summary}</p>
|
||||
)}
|
||||
{isCID && entry.new_value && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<svg className="w-3.5 h-3.5 text-emerald-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<code className="text-[10px] bg-emerald-50 text-emerald-700 px-2 py-0.5 rounded font-mono dark:bg-emerald-900/30 dark:text-emerald-300">
|
||||
{entry.new_value.length > 20 ? entry.new_value.slice(0, 8) + '...' + entry.new_value.slice(-6) : entry.new_value}
|
||||
</code>
|
||||
<span className="text-[10px] text-emerald-500">DSMS/IPFS</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<div className="text-xs text-gray-400">{date.toLocaleDateString('de-DE')}</div>
|
||||
<div className="text-[10px] text-gray-300">{date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}</div>
|
||||
<div className="text-[10px] text-gray-300 mt-0.5">{entry.performed_by}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import type { CategoryScore } from '../_hooks/useBenchmark'
|
||||
|
||||
interface Props { breakdown: CategoryScore[] }
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
'mechanische gefaehrdungen': 'Mechanisch',
|
||||
'elektrische gefaehrdungen': 'Elektrisch',
|
||||
'thermische gefaehrdungen': 'Thermisch',
|
||||
'laerm': 'Laerm',
|
||||
'vibration': 'Vibration',
|
||||
'strahlung': 'Strahlung',
|
||||
'materialien und substanzen': 'Materialien/Substanzen',
|
||||
'ergonomische gefaehrdungen': 'Ergonomie',
|
||||
'einsatzumgebung': 'Einsatzumgebung',
|
||||
}
|
||||
|
||||
export function CategoryBreakdown({ breakdown }: Props) {
|
||||
if (!breakdown || breakdown.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Coverage nach Gefaehrdungsgruppe</h3>
|
||||
<div className="space-y-2">
|
||||
{breakdown.map((cat) => {
|
||||
const label = CATEGORY_LABELS[cat.category] || cat.category
|
||||
const pct = Math.round(cat.coverage * 100)
|
||||
const barColor = pct >= 80 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500'
|
||||
return (
|
||||
<div key={cat.category}>
|
||||
<div className="flex justify-between text-xs text-gray-600 dark:text-gray-400 mb-0.5">
|
||||
<span>{label}</span>
|
||||
<span>{cat.match_count}/{cat.gt_count} ({pct}%)</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className={`h-full ${barColor} rounded-full transition-all`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useRef } from 'react'
|
||||
import type { GroundTruthEntry } from '../_hooks/useBenchmark'
|
||||
|
||||
interface Props {
|
||||
onImport: (gt: { entries: GroundTruthEntry[]; source_file?: string; description?: string }) => Promise<void>
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export function GTImportForm({ onImport, loading }: Props) {
|
||||
const [jsonText, setJsonText] = useState('')
|
||||
const [parseError, setParseError] = useState<string | null>(null)
|
||||
const [preview, setPreview] = useState<{ count: number; groups: Record<string, number> } | null>(null)
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
function tryParse(text: string) {
|
||||
setJsonText(text)
|
||||
setParseError(null)
|
||||
setPreview(null)
|
||||
if (!text.trim()) return
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
const entries: GroundTruthEntry[] = parsed.entries || parsed
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
setParseError('JSON muss ein Array "entries" enthalten')
|
||||
return
|
||||
}
|
||||
// Validate first entry has required fields
|
||||
const first = entries[0]
|
||||
if (!first.hazard_type && !first.hazard_group) {
|
||||
setParseError('Eintraege muessen hazard_type oder hazard_group enthalten')
|
||||
return
|
||||
}
|
||||
// Build preview
|
||||
const groups: Record<string, number> = {}
|
||||
for (const e of entries) {
|
||||
const g = e.hazard_group || 'Unbekannt'
|
||||
groups[g] = (groups[g] || 0) + 1
|
||||
}
|
||||
setPreview({ count: entries.length, groups })
|
||||
} catch (err) {
|
||||
setParseError('Ungueltiges JSON: ' + (err instanceof Error ? err.message : String(err)))
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
if (!jsonText.trim()) return
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
const gt = parsed.entries ? parsed : { entries: parsed }
|
||||
await onImport(gt)
|
||||
setJsonText('')
|
||||
setPreview(null)
|
||||
} catch (err) {
|
||||
setParseError(err instanceof Error ? err.message : 'Import fehlgeschlagen')
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
const reader = new FileReader()
|
||||
reader.onload = (ev) => {
|
||||
const text = ev.target?.result as string
|
||||
tryParse(text)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Ground Truth importieren</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
JSON-Datei mit der professionellen Risikobeurteilung einfuegen oder hochladen.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2 mb-3">
|
||||
<button
|
||||
onClick={() => fileRef.current?.click()}
|
||||
className="px-3 py-1.5 text-xs bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors"
|
||||
>
|
||||
JSON-Datei waehlen
|
||||
</button>
|
||||
<input ref={fileRef} type="file" accept=".json" onChange={handleFileUpload} className="hidden" />
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={jsonText}
|
||||
onChange={(e) => tryParse(e.target.value)}
|
||||
placeholder='{"entries": [...], "source_file": "...", "description": "..."}'
|
||||
rows={6}
|
||||
className="w-full text-xs font-mono border border-gray-300 dark:border-gray-600 rounded-md p-2 bg-gray-50 dark:bg-gray-900 text-gray-800 dark:text-gray-200 resize-y"
|
||||
/>
|
||||
|
||||
{parseError && (
|
||||
<div className="mt-2 px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-xs text-red-600">
|
||||
{parseError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preview && (
|
||||
<div className="mt-2 px-3 py-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded text-xs text-green-700 dark:text-green-400">
|
||||
<strong>{preview.count} Eintraege</strong> erkannt:
|
||||
{Object.entries(preview.groups).map(([g, c]) => (
|
||||
<span key={g} className="ml-2">{g}: {c}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={loading || !preview}
|
||||
className="mt-3 w-full px-4 py-2 text-sm font-medium bg-purple-600 hover:bg-purple-700 disabled:bg-gray-300 text-white rounded-md transition-colors"
|
||||
>
|
||||
{loading ? 'Importiere...' : 'Ground Truth importieren'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import type { HazardMatchPair, GroundTruthEntry, HazardSummary } from '../_hooks/useBenchmark'
|
||||
|
||||
interface Props {
|
||||
matched: HazardMatchPair[]
|
||||
missing: GroundTruthEntry[]
|
||||
extra: HazardSummary[]
|
||||
}
|
||||
|
||||
type TabType = 'matched' | 'missing' | 'extra'
|
||||
|
||||
export function HazardComparisonTable({ matched, missing, extra }: Props) {
|
||||
const [tab, setTab] = useState<TabType>('matched')
|
||||
|
||||
// Split matches: >= 50% are real matches, < 50% are weak (shown separately)
|
||||
const realMatched = matched.filter(p => p.match_score >= 0.5)
|
||||
const weakMatched = matched.filter(p => p.match_score < 0.5)
|
||||
|
||||
// Weak matches: GT entries go to "missing", engine entries go to "extra"
|
||||
const allMissing = [...missing, ...weakMatched.map(w => w.gt_entry)]
|
||||
const allExtra = [...extra, ...weakMatched.map(w => w.engine_hazard)]
|
||||
|
||||
const greenCount = realMatched.filter(p => p.match_score >= 0.7).length
|
||||
const yellowCount = realMatched.filter(p => p.match_score >= 0.5 && p.match_score < 0.7).length
|
||||
|
||||
const tabs: { id: TabType; label: string; count: number; color: string }[] = [
|
||||
{ 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 (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
{/* Tab bar */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700">
|
||||
{tabs.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setTab(t.id)}
|
||||
className={`flex-1 px-4 py-2.5 text-xs font-medium transition-colors ${
|
||||
tab === t.id
|
||||
? 'border-b-2 border-purple-600 text-purple-700 dark:text-purple-400'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{t.label} <span className={t.color}>({t.count})</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{tab === 'matched' && <MatchedTable pairs={realMatched} />}
|
||||
{tab === 'missing' && <MissingTable entries={allMissing} />}
|
||||
{tab === 'extra' && <ExtraTable entries={allExtra} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MatchedTable({ pairs }: { pairs: HazardMatchPair[] }) {
|
||||
const [expanded, setExpanded] = useState<Record<number, boolean>>({})
|
||||
if (pairs.length === 0) return <EmptyState text="Keine Zuordnungen gefunden" />
|
||||
return (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 dark:bg-gray-700/50">
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">Nr.</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">Ground Truth</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">R</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">Engine</th>
|
||||
<th className="px-3 py-2 text-center font-medium text-gray-500">Score</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">Qualitaet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{pairs.map((p, i) => {
|
||||
const quality = p.match_score >= 0.7 ? 'green' : p.match_score >= 0.4 ? 'yellow' : 'red'
|
||||
const rowBg = quality === 'green' ? 'bg-green-50/30 dark:bg-green-900/5'
|
||||
: quality === 'yellow' ? 'bg-yellow-50/30 dark:bg-yellow-900/5' : ''
|
||||
const isOpen = expanded[i]
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<tr className={`hover:bg-gray-50 dark:hover:bg-gray-700/30 cursor-pointer ${rowBg}`}
|
||||
onClick={() => setExpanded(prev => ({ ...prev, [i]: !prev[i] }))}>
|
||||
<td className="px-3 py-2 text-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<svg className={`w-3 h-3 text-gray-400 transition-transform ${isOpen ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
{p.gt_entry.nr}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="font-medium text-gray-800 dark:text-gray-200">{p.gt_entry.hazard_type}</div>
|
||||
<div className="text-gray-400 truncate max-w-[250px]">{p.gt_entry.component_zone}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center">
|
||||
<RiskBadge risk={p.gt_entry.risk_in.r} />
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="font-medium text-gray-800 dark:text-gray-200">{p.engine_hazard.name}</div>
|
||||
<div className="text-gray-400">{p.engine_hazard.category}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-center"><ScoreBadge score={p.match_score} /></td>
|
||||
<td className="px-3 py-2"><QualityBadge quality={quality} /></td>
|
||||
</tr>
|
||||
{isOpen && (
|
||||
<tr className="bg-gray-50/70 dark:bg-gray-850">
|
||||
<td colSpan={6} className="px-4 py-3">
|
||||
<DetailComparison gt={p.gt_entry} engine={p.engine_hazard} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
const LIFECYCLE_LABELS: Record<string, string> = {
|
||||
startup: 'Hochfahren', homing: 'Referenzfahrt', automatic_operation: 'Automatikbetrieb',
|
||||
manual_operation: 'Handbetrieb', teach_mode: 'Einrichtbetrieb', maintenance: 'Wartung',
|
||||
cleaning: 'Reinigung', emergency_stop: 'Not-Halt', recovery_mode: 'Wiederanlauf',
|
||||
normal_operation: 'Automatikbetrieb', setup: 'Einrichten', changeover: 'Umruesten',
|
||||
fault_clearing: 'Fehlersuche/Stoerungsbeseitigung', commissioning: 'Inbetriebnahme',
|
||||
decommissioning: 'Demontage/Ausserbetriebnahme', transport: 'Transport',
|
||||
assembly: 'Montage/Installation', inspection: 'Inspektion/Pruefung',
|
||||
}
|
||||
|
||||
function formatLifecycles(raw: string): string {
|
||||
if (!raw) return '-'
|
||||
return raw.split(',').map(s => s.trim()).map(s => LIFECYCLE_LABELS[s] || s).join(', ')
|
||||
}
|
||||
|
||||
/** Side-by-side detail comparison of GT entry vs. Engine hazard */
|
||||
function DetailComparison({ gt, engine }: { gt: GroundTruthEntry; engine: HazardSummary }) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 text-xs">
|
||||
{/* Left: Ground Truth */}
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-red-700 dark:text-red-400 uppercase text-[10px]">Ground Truth (Fachmann)</div>
|
||||
<DetailRow label="Gefaehrdung" gt={gt.hazard_type} />
|
||||
<DetailRow label="Ursache" gt={gt.hazard_cause} />
|
||||
<DetailRow label="Gefahrenstelle" gt={gt.component_zone} />
|
||||
<DetailRow label="Lebensphasen" gt={gt.lifecycle_phases?.join(', ') || '-'} />
|
||||
<DetailRow label="Risiko" gt={`F=${gt.risk_in.f} W=${gt.risk_in.w} P=${gt.risk_in.p} S=${gt.risk_in.s} => R=${gt.risk_in.r}`} />
|
||||
{gt.risk_out.r > 0 && (
|
||||
<DetailRow label="Restrisiko" gt={`F=${gt.risk_out.f} W=${gt.risk_out.w} P=${gt.risk_out.p} S=${gt.risk_out.s} => R=${gt.risk_out.r}`} />
|
||||
)}
|
||||
<DetailRow label="Massnahmen" gt={gt.measures?.join('\n') || '-'} multiline />
|
||||
<DetailRow label="Typ" gt={gt.measure_type || '-'} />
|
||||
{gt.norm_references?.length > 0 && (
|
||||
<DetailRow label="Normen" gt={gt.norm_references.join(', ')} />
|
||||
)}
|
||||
<DetailRow label="Hinreichend" gt={gt.sufficient ? 'JA' : 'NEIN'} />
|
||||
{gt.comment && <DetailRow label="Kommentar" gt={gt.comment} />}
|
||||
</div>
|
||||
{/* Right: Engine */}
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-purple-700 dark:text-purple-400 uppercase text-[10px]">Engine (automatisch)</div>
|
||||
<DetailRow label="Gefaehrdung" gt={engine.name} />
|
||||
<DetailRow label="Szenario" gt={engine.scenario || engine.description || '-'} />
|
||||
<DetailRow label="Gefahrenstelle" gt={engine.zone || '-'} />
|
||||
{engine.lifecycle_phase && (
|
||||
<DetailRow label="Lebensphasen" gt={formatLifecycles(engine.lifecycle_phase)} />
|
||||
)}
|
||||
<DetailRow label="Moeglicher Schaden" gt={engine.possible_harm || '-'} />
|
||||
<DetailRow label="Trigger" gt={engine.trigger_event || '-'} />
|
||||
{engine.affected_person && (
|
||||
<DetailRow label="Betroffene Personen" gt={engine.affected_person} />
|
||||
)}
|
||||
{engine.mitigations && engine.mitigations.length > 0 ? (
|
||||
<DetailRow label="Massnahmen" gt={engine.mitigations.join('\n')} multiline />
|
||||
) : (
|
||||
<DetailRow label="Massnahmen" gt="(keine zugeordnet)" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DetailRow({ label, gt, multiline }: { label: string; gt: string; multiline?: boolean }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[10px] font-medium text-gray-500 uppercase">{label}</div>
|
||||
{multiline ? (
|
||||
<pre className="text-xs text-gray-700 dark:text-gray-300 whitespace-pre-wrap font-sans mt-0.5">{gt}</pre>
|
||||
) : (
|
||||
<div className="text-xs text-gray-700 dark:text-gray-300 mt-0.5">{gt}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MissingTable({ entries }: { entries: GroundTruthEntry[] }) {
|
||||
if (entries.length === 0) return <EmptyState text="Keine fehlenden Gefaehrdungen" />
|
||||
return (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="bg-red-50 dark:bg-red-900/20">
|
||||
<th className="px-3 py-2 text-left font-medium text-red-600">Nr.</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-red-600">Gefaehrdung</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-red-600">Ursache</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-red-600">Zone</th>
|
||||
<th className="px-3 py-2 text-center font-medium text-red-600">R</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-red-600">Typ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{entries.map((e, i) => (
|
||||
<tr key={i} className="hover:bg-red-50/50">
|
||||
<td className="px-3 py-2 text-gray-400">{e.nr}</td>
|
||||
<td className="px-3 py-2 font-medium text-gray-800 dark:text-gray-200">{e.hazard_type}</td>
|
||||
<td className="px-3 py-2 text-gray-600 truncate max-w-[200px]">{e.hazard_cause}</td>
|
||||
<td className="px-3 py-2 text-gray-500">{e.component_zone}</td>
|
||||
<td className="px-3 py-2 text-center"><RiskBadge risk={e.risk_in.r} /></td>
|
||||
<td className="px-3 py-2 text-gray-500">{e.measure_type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function ExtraTable({ entries }: { entries: HazardSummary[] }) {
|
||||
if (entries.length === 0) return <EmptyState text="Keine zusaetzlichen Engine-Gefaehrdungen" />
|
||||
return (
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 dark:bg-gray-700/50">
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">Name</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">Kategorie</th>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-500">Zone</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{entries.map((e, i) => (
|
||||
<tr key={i} className="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td className="px-3 py-2 text-gray-800 dark:text-gray-200">{e.name}</td>
|
||||
<td className="px-3 py-2 text-gray-500">{e.category}</td>
|
||||
<td className="px-3 py-2 text-gray-400">{e.zone || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function RiskBadge({ risk }: { risk: number }) {
|
||||
const color = risk >= 30 ? 'bg-red-100 text-red-700' : risk >= 15 ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'
|
||||
return <span className={`inline-block px-1.5 py-0.5 rounded text-[10px] font-bold ${color}`}>{risk}</span>
|
||||
}
|
||||
|
||||
function ScoreBadge({ score }: { score: number }) {
|
||||
const pct = Math.round(score * 100)
|
||||
const color = pct >= 70 ? 'text-green-600' : pct >= 50 ? 'text-yellow-600' : 'text-red-600'
|
||||
return <span className={`font-bold ${color}`}>{pct}%</span>
|
||||
}
|
||||
|
||||
function QualityBadge({ quality }: { quality: 'green' | 'yellow' | 'red' }) {
|
||||
const styles = {
|
||||
green: 'bg-green-100 text-green-700 border-green-200',
|
||||
yellow: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
red: 'bg-red-100 text-red-700 border-red-200',
|
||||
}
|
||||
const labels = { green: 'Exakt', yellow: 'Aehnlich', red: 'Schwach' }
|
||||
return (
|
||||
<span className={`inline-block px-1.5 py-0.5 rounded border text-[10px] font-medium ${styles[quality]}`}>
|
||||
{labels[quality]}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyState({ text }: { text: string }) {
|
||||
return <div className="px-4 py-8 text-center text-sm text-gray-400">{text}</div>
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
export interface GTRisk { f: number; w: number; p: number; s: number; r: number }
|
||||
export interface GTPLr { s: string; f: string; p: string; ew?: string; plr: string }
|
||||
|
||||
export interface GroundTruthEntry {
|
||||
nr: string
|
||||
hazard_group: string
|
||||
hazard_group_applicable: boolean
|
||||
hazard_subgroup: string
|
||||
hazard_type: string
|
||||
hazard_cause: string
|
||||
lifecycle_phases: string[]
|
||||
component_zone: string
|
||||
risk_in: GTRisk
|
||||
plr?: GTPLr | null
|
||||
measures: string[]
|
||||
measure_type: string
|
||||
risk_out: GTRisk
|
||||
norm_references: string[]
|
||||
sufficient: boolean
|
||||
comment?: string
|
||||
reduction_steps?: {
|
||||
risk_in: GTRisk; measures: string[]; measure_type: string
|
||||
risk_out: GTRisk; norm_references: string[]; sufficient: boolean
|
||||
}[]
|
||||
}
|
||||
|
||||
export interface HazardSummary {
|
||||
id: string; name: string; category: string
|
||||
component?: string; zone?: string; risk_level?: string
|
||||
description?: string; scenario?: string
|
||||
possible_harm?: string; trigger_event?: string
|
||||
affected_person?: string; lifecycle_phase?: string
|
||||
mitigations?: string[]
|
||||
}
|
||||
|
||||
export interface HazardMatchPair {
|
||||
gt_entry: GroundTruthEntry
|
||||
engine_hazard: HazardSummary
|
||||
match_score: number
|
||||
match_reason: string
|
||||
}
|
||||
|
||||
export interface CategoryScore {
|
||||
category: string; gt_count: number; match_count: number; coverage: number
|
||||
}
|
||||
|
||||
export interface BenchmarkResult {
|
||||
coverage_score: number
|
||||
measure_coverage: number
|
||||
total_gt: number
|
||||
total_engine: number
|
||||
matched_pairs: HazardMatchPair[]
|
||||
missing_from_engine: GroundTruthEntry[]
|
||||
extra_in_engine: HazardSummary[]
|
||||
category_breakdown: CategoryScore[]
|
||||
risk_rank_pairs: { gt_rank: number; engine_rank: number; hazard_name: string; gt_risk_score: number }[]
|
||||
}
|
||||
|
||||
interface UseBenchmarkReturn {
|
||||
result: BenchmarkResult | null
|
||||
gtLoaded: boolean
|
||||
gtEntryCount: number
|
||||
loading: boolean
|
||||
error: string | null
|
||||
importGT: (gt: { entries: GroundTruthEntry[]; source_file?: string; description?: string }) => Promise<void>
|
||||
runBenchmark: (gtProjectId?: string) => Promise<void>
|
||||
}
|
||||
|
||||
export function useBenchmark(projectId: string): UseBenchmarkReturn {
|
||||
const [result, setResult] = useState<BenchmarkResult | null>(null)
|
||||
const [gtLoaded, setGtLoaded] = useState(false)
|
||||
const [gtEntryCount, setGtEntryCount] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const importGT = useCallback(async (gt: { entries: GroundTruthEntry[]; source_file?: string; description?: string }) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/benchmark/import-gt`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(gt),
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
const data = await res.json()
|
||||
setGtLoaded(true)
|
||||
setGtEntryCount(data.entry_count || gt.entries.length)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Import failed')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [projectId])
|
||||
|
||||
const runBenchmark = useCallback(async (gtProjectId?: string) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const params = gtProjectId ? `?gt_project_id=${gtProjectId}` : ''
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/benchmark${params}`)
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
const data: BenchmarkResult = await res.json()
|
||||
setResult(data)
|
||||
setGtLoaded(true)
|
||||
setGtEntryCount(data.total_gt)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Benchmark failed')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [projectId])
|
||||
|
||||
return { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark }
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useBenchmark } from './_hooks/useBenchmark'
|
||||
import { GTImportForm } from './_components/GTImportForm'
|
||||
import { HazardComparisonTable } from './_components/HazardComparisonTable'
|
||||
import { CategoryBreakdown } from './_components/CategoryBreakdown'
|
||||
|
||||
export default function BenchmarkPage() {
|
||||
const { projectId } = useParams<{ projectId: string }>()
|
||||
const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId)
|
||||
const [gtProjectId, setGtProjectId] = useState('')
|
||||
|
||||
// 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 (
|
||||
<div className="space-y-6 max-w-[1200px]">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-gray-900 dark:text-white">Ground Truth Benchmark</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Vergleich der Engine-Ergebnisse mit einer professionellen Risikobeurteilung
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="px-4 py-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-600">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* GT Import or Cross-Project Reference */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<GTImportForm onImport={importGT} loading={loading} />
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Benchmark ausfuehren</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
GT aus diesem Projekt verwenden, oder eine Projekt-ID mit importierter GT angeben.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={gtProjectId}
|
||||
onChange={(e) => setGtProjectId(e.target.value)}
|
||||
placeholder="GT-Projekt-ID (optional — leer = dieses Projekt)"
|
||||
className="w-full text-xs border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-gray-50 dark:bg-gray-900"
|
||||
/>
|
||||
<button
|
||||
onClick={() => runBenchmark(gtProjectId || undefined)}
|
||||
disabled={loading}
|
||||
className="w-full px-4 py-2 text-sm font-medium bg-purple-600 hover:bg-purple-700 disabled:bg-gray-300 text-white rounded-md transition-colors"
|
||||
>
|
||||
{loading ? 'Vergleiche...' : 'Benchmark starten'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{gtLoaded && !result && (
|
||||
<div className="mt-3 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 rounded text-xs text-blue-600">
|
||||
{gtEntryCount} GT-Eintraege geladen. Klicke "Benchmark starten".
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<>
|
||||
{/* Score Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<ScoreCard
|
||||
label="Hazard Coverage"
|
||||
value={`${coveragePct}%`}
|
||||
sub={`${realMatchCount} / ${result.total_gt} erkannt (>= 50% Match)`}
|
||||
color={coveragePct >= 80 ? 'green' : coveragePct >= 50 ? 'yellow' : 'red'}
|
||||
/>
|
||||
<ScoreCard
|
||||
label="Massnahmen-Coverage"
|
||||
value={`${measurePct}%`}
|
||||
sub="der zugeordneten Gefaehrdungen"
|
||||
color={measurePct >= 80 ? 'green' : measurePct >= 50 ? 'yellow' : 'red'}
|
||||
/>
|
||||
<ScoreCard
|
||||
label="GT Eintraege"
|
||||
value={String(result.total_gt)}
|
||||
sub="professionelle Beurteilung"
|
||||
color="gray"
|
||||
/>
|
||||
<ScoreCard
|
||||
label="Engine Eintraege"
|
||||
value={String(result.total_engine)}
|
||||
sub={`${result.extra_in_engine?.length || 0} zusaetzlich`}
|
||||
color="gray"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category Breakdown */}
|
||||
<CategoryBreakdown breakdown={result.category_breakdown || []} />
|
||||
|
||||
{/* Hazard Comparison Table */}
|
||||
<HazardComparisonTable
|
||||
matched={result.matched_pairs || []}
|
||||
missing={result.missing_from_engine || []}
|
||||
extra={result.extra_in_engine || []}
|
||||
/>
|
||||
|
||||
{/* Business Impact */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Business Impact</h3>
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-gray-900 dark:text-white">2,5 Tage</div>
|
||||
<div className="text-xs text-gray-500">Manueller Aufwand</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{(coveragePct / 100 * 2.5).toFixed(1)} Tage
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Eingespart bei {coveragePct}% Coverage</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{Math.round(coveragePct / 100 * 2.5 * 8 * 100)} EUR
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Einsparung (100 EUR/h)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScoreCard({ label, value, sub, color }: {
|
||||
label: string; value: string; sub: string
|
||||
color: 'green' | 'yellow' | 'red' | 'gray'
|
||||
}) {
|
||||
const colors = {
|
||||
green: 'border-green-200 dark:border-green-800',
|
||||
yellow: 'border-yellow-200 dark:border-yellow-800',
|
||||
red: 'border-red-200 dark:border-red-800',
|
||||
gray: 'border-gray-200 dark:border-gray-700',
|
||||
}
|
||||
const textColors = {
|
||||
green: 'text-green-600', yellow: 'text-yellow-600',
|
||||
red: 'text-red-600', gray: 'text-gray-900 dark:text-white',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`bg-white dark:bg-gray-800 rounded-lg border-2 ${colors[color]} p-4 text-center`}>
|
||||
<div className={`text-2xl font-bold ${textColors[color]}`}>{value}</div>
|
||||
<div className="text-xs font-medium text-gray-700 dark:text-gray-300 mt-1">{label}</div>
|
||||
<div className="text-[10px] text-gray-400 mt-0.5">{sub}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export function ComponentForm({
|
||||
version: initialData?.version || '',
|
||||
description: initialData?.description || '',
|
||||
safety_relevant: initialData?.safety_relevant || false,
|
||||
ce_marked: initialData?.ce_marked || false,
|
||||
parent_id: parentId || initialData?.parent_id || null,
|
||||
})
|
||||
|
||||
@@ -73,6 +74,19 @@ export function ComponentForm({
|
||||
</label>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Sicherheitsrelevant</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-6">
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.ce_marked}
|
||||
onChange={(e) => setFormData({ ...formData, ce_marked: e.target.checked })}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-green-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-green-500" />
|
||||
</label>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Bereits CE-gekennzeichnet</span>
|
||||
<span className="text-[10px] text-gray-400">(Nur Schnittstellen bewerten)</span>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
|
||||
@@ -5,10 +5,12 @@ export interface Component {
|
||||
version: string
|
||||
description: string
|
||||
safety_relevant: boolean
|
||||
ce_marked?: boolean
|
||||
parent_id: string | null
|
||||
children: Component[]
|
||||
library_component_id?: string
|
||||
energy_source_ids?: string[]
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface LibraryComponent {
|
||||
@@ -41,6 +43,7 @@ export interface ComponentFormData {
|
||||
version: string
|
||||
description: string
|
||||
safety_relevant: boolean
|
||||
ce_marked: boolean
|
||||
parent_id: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,15 @@ export interface FMEARow {
|
||||
occurrence: number
|
||||
detection: number
|
||||
rpz: number
|
||||
ap: 'H' | 'M' | 'L'
|
||||
}
|
||||
|
||||
/** AIAG-VDA Action Priority (2019 Handbook) */
|
||||
export function calculateAP(s: number, o: number, d: number): 'H' | 'M' | 'L' {
|
||||
if (s >= 9) return (o >= 4 || d >= 7) ? 'H' : (o >= 2 || d >= 5) ? 'M' : 'L'
|
||||
if (s >= 7) return (o >= 5 || d >= 8) ? 'H' : (o >= 3 || d >= 5) ? 'M' : 'L'
|
||||
if (s >= 5) return (o >= 7 || d >= 9) ? 'H' : (o >= 4 || d >= 7) ? 'M' : 'L'
|
||||
return (o >= 8 && d >= 9) ? 'H' : (o >= 6 || d >= 8) ? 'M' : 'L'
|
||||
}
|
||||
|
||||
export function useFMEA(projectId: string) {
|
||||
@@ -52,21 +61,7 @@ export function useFMEA(projectId: string) {
|
||||
})
|
||||
)
|
||||
|
||||
// Load failure modes for each component type (deduplicated)
|
||||
const types = [...new Set(components.map((c) => c.component_type))]
|
||||
const fmByType: Record<string, FailureMode[]> = {}
|
||||
|
||||
await Promise.all(
|
||||
types.map(async (type) => {
|
||||
const res = await fetch(`/api/sdk/v1/iace/failure-modes?component_type=${type}`)
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
fmByType[type] = json.failure_modes || []
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Also load general failure modes (no type filter)
|
||||
// Load ALL failure modes, then match by component type + name keywords
|
||||
const allRes = await fetch('/api/sdk/v1/iace/failure-modes')
|
||||
let allFMs: FailureMode[] = []
|
||||
if (allRes.ok) {
|
||||
@@ -74,12 +69,33 @@ export function useFMEA(projectId: string) {
|
||||
allFMs = json.failure_modes || []
|
||||
}
|
||||
|
||||
// Derive the best FM component_type from component name keywords
|
||||
const nameToFMTypes: Record<string, string[]> = {
|
||||
sensor: ['sensor'], scanner: ['sensor'], kamera: ['sensor'],
|
||||
motor: ['actuator', 'electrical'], antrieb: ['actuator'],
|
||||
steuerung: ['controller'], sps: ['controller'], plc: ['controller'],
|
||||
software: ['software'], firmware: ['software'],
|
||||
ventil: ['actuator', 'mechanical'], greifer: ['actuator', 'mechanical'],
|
||||
roboter: ['actuator', 'mechanical'], hydraulik: ['actuator'],
|
||||
netzwerk: ['network'], ethernet: ['network'],
|
||||
}
|
||||
|
||||
function getFMTypesForComp(comp: Component): string[] {
|
||||
const types = [comp.component_type]
|
||||
const nameLower = comp.name.toLowerCase()
|
||||
for (const [kw, fmTypes] of Object.entries(nameToFMTypes)) {
|
||||
if (nameLower.includes(kw)) types.push(...fmTypes)
|
||||
}
|
||||
return [...new Set(types)]
|
||||
}
|
||||
|
||||
// Build FMEA rows: each component × its matching failure modes
|
||||
const fmeaRows: FMEARow[] = []
|
||||
for (const comp of components) {
|
||||
const compFMs = fmByType[comp.component_type] || []
|
||||
// Use type-specific FMs, or fallback to first 3 general FMs
|
||||
const relevantFMs = compFMs.length > 0 ? compFMs : allFMs.slice(0, 3)
|
||||
const compTypes = getFMTypesForComp(comp)
|
||||
const compFMs = allFMs.filter((fm) => compTypes.includes(fm.component_type))
|
||||
// Use matched FMs, or fallback to mechanical FMs
|
||||
const relevantFMs = compFMs.length > 0 ? compFMs : allFMs.filter((fm) => fm.component_type === 'mechanical').slice(0, 3)
|
||||
|
||||
for (const fm of relevantFMs) {
|
||||
const s = fm.default_severity || 5
|
||||
@@ -92,6 +108,7 @@ export function useFMEA(projectId: string) {
|
||||
occurrence: o,
|
||||
detection: d,
|
||||
rpz: s * o * d,
|
||||
ap: calculateAP(s, o, d),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -113,5 +130,31 @@ export function useFMEA(projectId: string) {
|
||||
acceptable: rows.filter((r) => r.rpz <= 100).length,
|
||||
}
|
||||
|
||||
return { rows, loading, stats }
|
||||
const [suggesting, setSuggesting] = useState(false)
|
||||
const [suggestions, setSuggestions] = useState<FailureMode[]>([])
|
||||
const [suggestSource, setSuggestSource] = useState<string>('')
|
||||
|
||||
async function suggestFMs(componentId: string) {
|
||||
setSuggesting(true)
|
||||
setSuggestions([])
|
||||
try {
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/components/${componentId}/suggest-fms`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
setSuggestions(json.suggestions || [])
|
||||
setSuggestSource(json.source || 'unknown')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('FM suggest failed:', err)
|
||||
} finally {
|
||||
setSuggesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Get unique components for the suggest button
|
||||
const components = [...new Map(rows.map((r) => [r.component.id, r.component])).values()]
|
||||
|
||||
return { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useFMEA, type FMEARow } from './_hooks/useFMEA'
|
||||
|
||||
@@ -26,7 +27,8 @@ function rpzLabel(rpz: number): string {
|
||||
|
||||
export default function FMEAPage() {
|
||||
const { projectId } = useParams<{ projectId: string }>()
|
||||
const { rows, loading, stats } = useFMEA(projectId)
|
||||
const { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions } = useFMEA(projectId)
|
||||
const [suggestComp, setSuggestComp] = useState<string | null>(null)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -46,6 +48,79 @@ export default function FMEAPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<FMEAInfoBox />
|
||||
|
||||
{/* KI-Vorschlag + Export */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={suggestComp || ''}
|
||||
onChange={(e) => setSuggestComp(e.target.value || null)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
>
|
||||
<option value="">Komponente waehlen...</option>
|
||||
{components.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => suggestComp && suggestFMs(suggestComp)}
|
||||
disabled={!suggestComp || suggesting}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium transition-colors disabled:opacity-50"
|
||||
>
|
||||
{suggesting ? (
|
||||
<span className="animate-spin inline-block w-4 h-4 border-2 border-white border-t-transparent rounded-full" />
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
)}
|
||||
KI-Vorschlag
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<a
|
||||
href={`/api/sdk/v1/iace/projects/${projectId}/fmea/export`}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-medium transition-colors"
|
||||
download
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
VDA Excel exportieren
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggest Results */}
|
||||
{suggestions.length > 0 && (
|
||||
<div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-purple-800 dark:text-purple-300">
|
||||
KI-Vorschlaege ({suggestions.length}) — {suggestSource === 'llm' ? 'LLM-generiert' : 'Bibliothek'}
|
||||
</h3>
|
||||
<button onClick={() => setSuggestions([])} className="text-xs text-purple-600 hover:text-purple-800">Schliessen</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{suggestions.map((fm, i) => (
|
||||
<div key={i} className="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg p-3 border border-purple-100 dark:border-purple-800">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">{fm.name_de}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{fm.effect}</div>
|
||||
<div className="flex gap-3 mt-1 text-xs text-gray-400">
|
||||
<span>S={fm.default_severity}</span>
|
||||
<span>O={fm.default_occurrence}</span>
|
||||
<span>D={fm.default_detection}</span>
|
||||
<span className="font-bold">RPZ={fm.default_severity * fm.default_occurrence * fm.default_detection}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<StatCard label="Gesamt" value={stats.total} color="gray" />
|
||||
@@ -79,6 +154,7 @@ export default function FMEAPage() {
|
||||
<th className="px-3 py-2.5 text-center text-xs font-medium text-gray-500 uppercase w-12">O</th>
|
||||
<th className="px-3 py-2.5 text-center text-xs font-medium text-gray-500 uppercase w-12">D</th>
|
||||
<th className="px-3 py-2.5 text-center text-xs font-medium text-gray-500 uppercase w-16">RPZ</th>
|
||||
<th className="px-3 py-2.5 text-center text-xs font-medium text-gray-500 uppercase w-12">AP</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-medium text-gray-500 uppercase">Bewertung</th>
|
||||
<th className="px-3 py-2.5 text-left text-xs font-medium text-gray-500 uppercase">Erkennung</th>
|
||||
</tr>
|
||||
@@ -121,6 +197,15 @@ function FMEATableRow({ row }: { row: FMEARow }) {
|
||||
{row.rpz}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-center">
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-bold ${
|
||||
row.ap === 'H' ? 'bg-red-600 text-white' :
|
||||
row.ap === 'M' ? 'bg-yellow-500 text-white' :
|
||||
'bg-green-500 text-white'
|
||||
}`}>
|
||||
{row.ap}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${color}`}>{rpzLabel(row.rpz)}</span>
|
||||
</td>
|
||||
@@ -131,6 +216,51 @@ function FMEATableRow({ row }: { row: FMEARow }) {
|
||||
)
|
||||
}
|
||||
|
||||
function FMEAInfoBox() {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl overflow-hidden">
|
||||
<button onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-4 py-3 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium text-blue-800 dark:text-blue-300">Was ist FMEA? — Anleitung & Beispiel</span>
|
||||
</div>
|
||||
<svg className={`w-4 h-4 text-blue-600 transition-transform ${open ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="px-4 pb-4 text-xs text-blue-800 dark:text-blue-300 space-y-3">
|
||||
<p><strong>FMEA</strong> (Fehlermoeglich- und Einflussanalyse) ist eine systematische Methode zur vorbeugenden Qualitaetssicherung nach AIAG-VDA (2019).</p>
|
||||
<div>
|
||||
<strong>Bewertungsskalen (je 1-10):</strong>
|
||||
<ul className="mt-1 ml-4 space-y-0.5 list-disc">
|
||||
<li><strong>S (Severity)</strong> — Schwere der Auswirkung: 1 = kaum merkbar, 10 = katastrophal (Lebensgefahr)</li>
|
||||
<li><strong>O (Occurrence)</strong> — Auftretenswahrscheinlichkeit: 1 = praktisch ausgeschlossen, 10 = sehr haeufig</li>
|
||||
<li><strong>D (Detection)</strong> — Entdeckbarkeit: 1 = sofort erkennbar, 10 = nicht erkennbar</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Kennzahlen:</strong>
|
||||
<ul className="mt-1 ml-4 space-y-0.5 list-disc">
|
||||
<li><strong>RPZ</strong> = S x O x D (1-1000). Ab RPZ > 100: Massnahme erforderlich.</li>
|
||||
<li><strong>AP (Action Priority)</strong> — AIAG-VDA Standard: <span className="inline-block px-1.5 py-0.5 bg-red-600 text-white rounded text-[10px] font-bold">H</span> = sofort handeln, <span className="inline-block px-1.5 py-0.5 bg-yellow-500 text-white rounded text-[10px] font-bold">M</span> = planen, <span className="inline-block px-1.5 py-0.5 bg-green-500 text-white rounded text-[10px] font-bold">L</span> = beobachten</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Beispiel:</strong> SPS-Steuerung → Kommunikationsausfall (S=8, O=3, D=5) → RPZ=120, AP=M → Massnahme: Redundante Kommunikation implementieren.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Workflow:</strong> 1. Komponente waehlen → 2. Fehlerart identifizieren → 3. S/O/D bewerten → 4. AP pruefen → 5. Bei H/M: Massnahme definieren → 6. Nach Massnahme: neu bewerten
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatCard({ label, value, color }: { label: string; value: number; color: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
gray: 'bg-gray-50 text-gray-700 border-gray-200',
|
||||
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react'
|
||||
import { Hazard } from './types'
|
||||
import { RiskAssessmentTable } from './RiskAssessmentTable'
|
||||
|
||||
interface BlockData {
|
||||
parent_hazard: { hazard: { id: string } }
|
||||
children: { hazard: { id: string } }[]
|
||||
children_covered_by_parent: boolean
|
||||
block_key: string
|
||||
}
|
||||
|
||||
interface BlockInfo {
|
||||
isParent: boolean
|
||||
isChild: boolean
|
||||
isCovered: boolean
|
||||
blockKey: string
|
||||
parentId: string
|
||||
childCount: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
projectId: string
|
||||
hazards: Hazard[]
|
||||
onReassess?: () => void
|
||||
decisions?: Record<string, boolean | null>
|
||||
onDecision?: (hazardId: string, acceptable: boolean | null) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps RiskAssessmentTable with block-awareness:
|
||||
* - Injects block metadata into hazards so the table can show grouping
|
||||
* - Provides controls to ungroup/promote children
|
||||
*/
|
||||
export function BlockAwareRiskTable({ projectId, hazards, onReassess, decisions, onDecision }: Props) {
|
||||
const [blocks, setBlocks] = useState<BlockData[]>([])
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})
|
||||
const [ungrouped, setUngrouped] = useState<Record<string, boolean>>({})
|
||||
const [pendingAction, setPendingAction] = useState<{ childId: string; childName: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazard-blocks`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(d => { if (d?.blocks) setBlocks(d.blocks) })
|
||||
.catch(() => {})
|
||||
}, [projectId])
|
||||
|
||||
// Build lookup: hazardId → block info
|
||||
const blockMap = useMemo(() => {
|
||||
const map: Record<string, BlockInfo> = {}
|
||||
for (const b of blocks) {
|
||||
if (b.children.length === 0) continue
|
||||
const pid = b.parent_hazard.hazard.id
|
||||
map[pid] = {
|
||||
isParent: true, isChild: false, isCovered: false,
|
||||
blockKey: b.block_key, parentId: pid, childCount: b.children.length,
|
||||
}
|
||||
for (const c of b.children) {
|
||||
if (ungrouped[c.hazard.id]) continue
|
||||
map[c.hazard.id] = {
|
||||
isParent: false, isChild: true,
|
||||
isCovered: b.children_covered_by_parent,
|
||||
blockKey: b.block_key, parentId: pid, childCount: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [blocks, ungrouped])
|
||||
|
||||
// Sort hazards: parents first, then their children, then standalone
|
||||
const sortedHazards = useMemo(() => {
|
||||
const parents: Hazard[] = []
|
||||
const childrenByParent: Record<string, Hazard[]> = {}
|
||||
const standalone: Hazard[] = []
|
||||
|
||||
for (const h of hazards) {
|
||||
const info = blockMap[h.id]
|
||||
if (!info) {
|
||||
standalone.push(h)
|
||||
} else if (info.isParent) {
|
||||
parents.push(h)
|
||||
childrenByParent[h.id] = []
|
||||
} else if (info.isChild) {
|
||||
const arr = childrenByParent[info.parentId]
|
||||
if (arr) arr.push(h)
|
||||
else standalone.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort parents by risk desc
|
||||
parents.sort((a, b) => (b.r_inherent || 0) - (a.r_inherent || 0))
|
||||
standalone.sort((a, b) => (b.r_inherent || 0) - (a.r_inherent || 0))
|
||||
|
||||
// Interleave: parent → children → parent → children → ... → standalone
|
||||
const result: Hazard[] = []
|
||||
for (const p of parents) {
|
||||
result.push(p)
|
||||
const isCollapsed = collapsed[p.id]
|
||||
if (!isCollapsed && childrenByParent[p.id]) {
|
||||
result.push(...childrenByParent[p.id])
|
||||
}
|
||||
}
|
||||
result.push(...standalone)
|
||||
return result
|
||||
}, [hazards, blockMap, collapsed])
|
||||
|
||||
const toggleCollapse = (parentId: string) => {
|
||||
setCollapsed(prev => ({ ...prev, [parentId]: !prev[parentId] }))
|
||||
}
|
||||
|
||||
const handleUngroup = (childId: string) => {
|
||||
setUngrouped(prev => ({ ...prev, [childId]: true }))
|
||||
setPendingAction(null)
|
||||
}
|
||||
|
||||
const handleRegroup = (childId: string) => {
|
||||
setUngrouped(prev => {
|
||||
const next = { ...prev }
|
||||
delete next[childId]
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
// Count blocks with children
|
||||
const blockCount = blocks.filter(b => b.children.length > 0).length
|
||||
const coveredCount = Object.values(blockMap).filter(b => b.isChild && b.isCovered).length
|
||||
const ungroupedCount = Object.keys(ungrouped).length
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Confirmation dialog */}
|
||||
{pendingAction && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 p-5 max-w-md w-full mx-4">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">Gefaehrdung aus Block entfernen?</h3>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-1">
|
||||
<strong>{pendingAction.childName}</strong>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Der Punkt wird als eigenstaendige Gefaehrdung gefuehrt und muss separat bewertet werden.
|
||||
Sie koennen ihn jederzeit ueber "Zurueck in Block" wieder zuordnen.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => handleUngroup(pendingAction.childId)}
|
||||
className="flex-1 px-3 py-2 text-xs font-medium bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors">
|
||||
Als eigenen Punkt fuehren
|
||||
</button>
|
||||
<button onClick={() => setPendingAction(null)}
|
||||
className="flex-1 px-3 py-2 text-xs font-medium bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 transition-colors">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Block info bar */}
|
||||
{blockCount > 0 && (
|
||||
<div className="flex items-center gap-4 px-4 py-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-xs">
|
||||
<span className="font-medium text-purple-700 dark:text-purple-300">
|
||||
{blockCount} Bloecke erkannt
|
||||
</span>
|
||||
{coveredCount > 0 && (
|
||||
<span className="text-green-600">
|
||||
{coveredCount} Kinder durch Mutter abgedeckt
|
||||
</span>
|
||||
)}
|
||||
{ungroupedCount > 0 && (
|
||||
<button onClick={() => setUngrouped({})}
|
||||
className="text-orange-600 hover:text-orange-700 underline">
|
||||
{ungroupedCount} entgruppiert — alle zuruecksetzen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Enhanced table with block decorations */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs whitespace-nowrap">
|
||||
<thead>
|
||||
<tr className="bg-gray-100 dark:bg-gray-750 border-b border-gray-200 dark:border-gray-700">
|
||||
<th className="w-8 px-1 py-1.5"></th>
|
||||
<th colSpan={2} className="px-3 py-1.5 text-left font-semibold text-gray-700 dark:text-gray-300 border-r border-gray-200 dark:border-gray-600">Gefaehrdung</th>
|
||||
<th colSpan={4} className="px-3 py-1.5 text-center font-semibold text-gray-700 dark:text-gray-300 border-r border-gray-200 dark:border-gray-600">Risiko (S x F x P)</th>
|
||||
<th className="px-3 py-1.5 text-center font-semibold text-gray-700 dark:text-gray-300">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{sortedHazards.map(h => {
|
||||
const info = blockMap[h.id]
|
||||
const isParent = info?.isParent
|
||||
const isChild = info?.isChild
|
||||
const isCovered = info?.isCovered
|
||||
const childCount = info?.childCount || 0
|
||||
const isCollapsedParent = isParent && collapsed[h.id]
|
||||
|
||||
return (
|
||||
<tr key={h.id} className={`transition-colors ${
|
||||
isChild ? 'bg-gray-50/50 dark:bg-gray-850' :
|
||||
isParent ? 'bg-white dark:bg-gray-800' : ''
|
||||
} ${isCovered ? 'opacity-60' : ''} hover:bg-gray-50 dark:hover:bg-gray-750`}>
|
||||
{/* Block indicator */}
|
||||
<td className="px-1 py-2 text-center">
|
||||
{isParent && (
|
||||
<button onClick={() => toggleCollapse(h.id)}
|
||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-purple-100 text-purple-600 transition-colors"
|
||||
title={`${childCount} Kinder ${isCollapsedParent ? 'anzeigen' : 'verbergen'}`}>
|
||||
<svg className={`w-3 h-3 transition-transform ${isCollapsedParent ? '' : 'rotate-90'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{isChild && (
|
||||
<div className="flex items-center justify-center">
|
||||
<button onClick={() => setPendingAction({ childId: h.id, childName: h.name })}
|
||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-orange-100 text-gray-300 hover:text-orange-500 transition-colors"
|
||||
title="Aus Block entfernen (mit Bestaetigung)">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{/* Show regroup button for ungrouped items */}
|
||||
{!isParent && !isChild && ungrouped[h.id] && (
|
||||
<button onClick={() => handleRegroup(h.id)}
|
||||
className="w-5 h-5 flex items-center justify-center rounded hover:bg-green-100 text-orange-400 hover:text-green-600 transition-colors"
|
||||
title="Zurueck in Block">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
{/* Name */}
|
||||
<td className={`px-3 py-2 ${isChild ? 'pl-8' : ''}`}>
|
||||
<div className={`font-medium ${isParent ? 'text-purple-800 dark:text-purple-300' : 'text-gray-900 dark:text-white'}`}>
|
||||
{h.name}
|
||||
{isParent && <span className="ml-1 text-[10px] text-purple-500">({childCount})</span>}
|
||||
</div>
|
||||
{h.hazardous_zone && <div className="text-[10px] text-gray-400 truncate max-w-[200px]">{h.hazardous_zone}</div>}
|
||||
</td>
|
||||
{/* Category */}
|
||||
<td className="px-3 py-2 border-r border-gray-200 dark:border-gray-600 text-gray-500">
|
||||
{h.category?.replace(/_/g, ' ')}
|
||||
</td>
|
||||
{/* Risk */}
|
||||
<td className="px-2 py-2 text-center">{h.severity || '-'}</td>
|
||||
<td className="px-2 py-2 text-center">{h.exposure || '-'}</td>
|
||||
<td className="px-2 py-2 text-center">{h.probability || '-'}</td>
|
||||
<td className="px-2 py-2 text-center font-bold border-r border-gray-200 dark:border-gray-600">
|
||||
{h.r_inherent || '-'}
|
||||
</td>
|
||||
{/* Status */}
|
||||
<td className="px-3 py-2 text-center">
|
||||
{isCovered ? (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-100 text-green-700 text-[10px] font-medium">
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Abgedeckt
|
||||
</span>
|
||||
) : h.r_inherent ? (
|
||||
<span className={`inline-block px-1.5 py-0.5 rounded-full text-[10px] font-medium ${
|
||||
(h.r_inherent || 0) <= 20 ? 'bg-green-100 text-green-700' :
|
||||
(h.r_inherent || 0) <= 60 ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{(h.r_inherent || 0) <= 20 ? 'Niedrig' : (h.r_inherent || 0) <= 60 ? 'Mittel' : 'Hoch'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-gray-400">Offen</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { CATEGORY_LABELS } from './types'
|
||||
import { RiskBadge } from './RiskBadge'
|
||||
|
||||
interface BlockHazard {
|
||||
hazard: {
|
||||
id: string; name: string; description: string; category: string
|
||||
hazardous_zone: string; scenario?: string; possible_harm?: string
|
||||
}
|
||||
assessment?: { severity: number; exposure: number; probability: number; inherent_risk: number; risk_level: string } | null
|
||||
mitigation_ids: string[]
|
||||
}
|
||||
|
||||
interface HazardBlock {
|
||||
parent_hazard: BlockHazard
|
||||
children: BlockHazard[]
|
||||
block_key: string
|
||||
shared_measure_count: number
|
||||
children_covered_by_parent: boolean
|
||||
}
|
||||
|
||||
interface BlockSummary {
|
||||
total_blocks: number
|
||||
parent_only_blocks: number
|
||||
blocks_with_children: number
|
||||
total_hazards: number
|
||||
covered_children: number
|
||||
uncovered_children: number
|
||||
assessments_needed: number
|
||||
assessments_saved: number
|
||||
}
|
||||
|
||||
export function HazardBlockView() {
|
||||
const { projectId } = useParams<{ projectId: string }>()
|
||||
const [blocks, setBlocks] = useState<HazardBlock[]>([])
|
||||
const [summary, setSummary] = useState<BlockSummary | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (!projectId) return
|
||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazard-blocks`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => {
|
||||
if (data) {
|
||||
setBlocks(data.blocks || [])
|
||||
setSummary(data.summary || null)
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
}, [projectId])
|
||||
|
||||
const toggle = (key: string) => setExpanded(prev => ({ ...prev, [key]: !prev[key] }))
|
||||
|
||||
if (loading) return <div className="text-sm text-gray-400 py-8 text-center">Lade Bloecke...</div>
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Summary Cards */}
|
||||
{summary && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<SummaryCard label="Bloecke" value={summary.total_blocks} sub={`${summary.total_hazards} Gefaehrdungen`} />
|
||||
<SummaryCard label="Mit Kindern" value={summary.blocks_with_children} sub={`${summary.covered_children} abgedeckt`} color="green" />
|
||||
<SummaryCard label="Bewertungen noetig" value={summary.assessments_needed} sub={`von ${summary.total_hazards}`} color="purple" />
|
||||
<SummaryCard label="Eingespart" value={summary.assessments_saved} sub="durch Gruppierung" color="green" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Block List */}
|
||||
<div className="space-y-2">
|
||||
{blocks.map((block) => {
|
||||
const isOpen = expanded[block.block_key]
|
||||
const parent = block.parent_hazard
|
||||
const childCount = block.children.length
|
||||
const covered = block.children_covered_by_parent
|
||||
|
||||
return (
|
||||
<div key={block.block_key} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
{/* Parent Row */}
|
||||
<div
|
||||
className={`flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors ${childCount > 0 ? '' : 'opacity-90'}`}
|
||||
onClick={() => childCount > 0 && toggle(block.block_key)}
|
||||
>
|
||||
{/* Expand Arrow */}
|
||||
{childCount > 0 ? (
|
||||
<svg className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
) : (
|
||||
<div className="w-4 h-4" />
|
||||
)}
|
||||
|
||||
{/* Name + Category */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white truncate">{parent.hazard.name}</span>
|
||||
<span className="text-xs text-gray-400">{CATEGORY_LABELS[parent.hazard.category] || parent.hazard.category}</span>
|
||||
</div>
|
||||
{parent.hazard.hazardous_zone && (
|
||||
<div className="text-xs text-gray-500 truncate">{parent.hazard.hazardous_zone}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Risk */}
|
||||
{parent.assessment ? (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-gray-500">R={parent.assessment.inherent_risk}</span>
|
||||
<RiskBadge level={parent.assessment.risk_level} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">Nicht bewertet</span>
|
||||
)}
|
||||
|
||||
{/* Child count badge */}
|
||||
{childCount > 0 && (
|
||||
<div className={`flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
covered
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
||||
}`}>
|
||||
+{childCount}
|
||||
{covered && (
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Measures count */}
|
||||
<span className="text-xs text-gray-400">{block.shared_measure_count} M.</span>
|
||||
</div>
|
||||
|
||||
{/* Children (expanded) */}
|
||||
{isOpen && childCount > 0 && (
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-850">
|
||||
{covered && (
|
||||
<div className="px-4 py-2 text-xs text-green-600 dark:text-green-400 bg-green-50/50 dark:bg-green-900/10 border-b border-green-100 dark:border-green-900/30">
|
||||
Alle Untergefaehrdungen durch Massnahmen der Muttergefaehrdung abgedeckt — keine separate Bewertung noetig.
|
||||
</div>
|
||||
)}
|
||||
{block.children.map((child) => (
|
||||
<div key={child.hazard.id} className="flex items-center gap-3 px-4 py-2 pl-12 border-b border-gray-100 dark:border-gray-700 last:border-b-0">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-gray-300 dark:bg-gray-600 flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300">{child.hazard.name}</span>
|
||||
{child.hazard.hazardous_zone && (
|
||||
<span className="text-xs text-gray-400 ml-2">[{child.hazard.hazardous_zone}]</span>
|
||||
)}
|
||||
</div>
|
||||
{child.assessment ? (
|
||||
<span className="text-xs text-gray-500">R={child.assessment.inherent_risk}</span>
|
||||
) : covered ? (
|
||||
<span className="text-xs text-green-500">Abgedeckt</span>
|
||||
) : (
|
||||
<span className="text-xs text-yellow-500">Offen</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SummaryCard({ label, value, sub, color }: { label: string; value: number; sub: string; color?: string }) {
|
||||
const textColor = color === 'green' ? 'text-green-600' : color === 'purple' ? 'text-purple-600' : 'text-gray-900 dark:text-white'
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-3 text-center">
|
||||
<div className={`text-xl font-bold ${textColor}`}>{value}</div>
|
||||
<div className="text-xs font-medium text-gray-600 dark:text-gray-400">{label}</div>
|
||||
<div className="text-[10px] text-gray-400">{sub}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import React, { useState, useMemo, useCallback } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { HazardForm } from './_components/HazardForm'
|
||||
import { HazardTable } from './_components/HazardTable'
|
||||
import { HazardBlockView } from './_components/HazardBlockView'
|
||||
import { BlockAwareRiskTable } from './_components/BlockAwareRiskTable'
|
||||
import { RiskAssessmentTable } from './_components/RiskAssessmentTable'
|
||||
import { ResidualRiskPanel, getResidualStatus } from './_components/ResidualRiskPanel'
|
||||
import type { ResidualFilter } from './_components/ResidualRiskPanel'
|
||||
@@ -12,7 +14,7 @@ import { AutoSuggestPanel } from './_components/AutoSuggestPanel'
|
||||
import { CustomHazardModal } from './_components/CustomHazardModal'
|
||||
import { useHazards } from './_hooks/useHazards'
|
||||
|
||||
type ViewMode = 'list' | 'risk'
|
||||
type ViewMode = 'list' | 'risk' | 'blocks'
|
||||
|
||||
export default function HazardsPage() {
|
||||
const params = useParams()
|
||||
@@ -69,6 +71,10 @@ export default function HazardsPage() {
|
||||
className={`px-3 py-1.5 font-medium transition-colors border-l border-gray-200 dark:border-gray-600 ${view === 'risk' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-50'}`}>
|
||||
Risikobewertung
|
||||
</button>
|
||||
<button onClick={() => setView('blocks')}
|
||||
className={`px-3 py-1.5 font-medium transition-colors border-l border-gray-200 dark:border-gray-600 ${view === 'blocks' ? 'bg-purple-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-50'}`}>
|
||||
Bloecke
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -169,9 +175,11 @@ export default function HazardsPage() {
|
||||
<>
|
||||
<ResidualRiskPanel hazards={h.hazards} decisions={decisions}
|
||||
activeFilter={residualFilter} onFilterChange={setResidualFilter} />
|
||||
<RiskAssessmentTable projectId={projectId} hazards={filteredHazards}
|
||||
<BlockAwareRiskTable projectId={projectId} hazards={filteredHazards}
|
||||
onReassess={h.refetch} decisions={decisions} onDecision={handleDecision} />
|
||||
</>
|
||||
) : view === 'blocks' ? (
|
||||
<HazardBlockView />
|
||||
) : (
|
||||
<HazardTable hazards={h.hazards} lifecyclePhases={h.lifecyclePhases} onDelete={h.handleDelete} />
|
||||
)
|
||||
|
||||
+17
@@ -7,6 +7,7 @@ import {
|
||||
AREA_OF_USE_OPTIONS,
|
||||
OPERATING_MODE_OPTIONS,
|
||||
PERSON_GROUP_OPTIONS,
|
||||
INDUSTRY_SECTOR_OPTIONS,
|
||||
type LimitsFormData,
|
||||
} from '../_types'
|
||||
|
||||
@@ -204,6 +205,22 @@ export function LimitsFormSections({ data, onChange, prefilled }: LimitsFormSect
|
||||
rows={4}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
{/* Section 7: Einsatzbereich / Branche */}
|
||||
<SectionCard section={FORM_SECTIONS[6]}>
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 mb-2">
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300">
|
||||
Die Branchenauswahl steuert welche branchenspezifischen Gefaehrdungsmuster (z.B. Medizintechnik, Lebensmittel, Aufzuege) bei der Risikoanalyse beruecksichtigt werden. Branchenfremde Muster werden automatisch ausgeblendet.
|
||||
</p>
|
||||
</div>
|
||||
<CheckboxGroup
|
||||
label="Einsatzbereiche"
|
||||
values={data.industry_sectors}
|
||||
onChange={(v) => onChange('industry_sectors', v)}
|
||||
options={INDUSTRY_SECTOR_OPTIONS}
|
||||
helpText="Waehlen Sie alle zutreffenden Branchen. Bei Mehrfachauswahl werden alle relevanten Gefaehrdungen beruecksichtigt."
|
||||
/>
|
||||
</SectionCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ export interface LimitsFormData {
|
||||
// Section 6: Betroffene Personen
|
||||
person_groups: string[]
|
||||
qualification_requirements: string
|
||||
|
||||
// Section 7: Einsatzbereich / Branche (fuer Pattern-Filterung)
|
||||
industry_sectors: string[]
|
||||
}
|
||||
|
||||
export const EMPTY_LIMITS_FORM: LimitsFormData = {
|
||||
@@ -59,6 +62,7 @@ export const EMPTY_LIMITS_FORM: LimitsFormData = {
|
||||
pneumatic_hydraulic_interfaces: '',
|
||||
person_groups: [],
|
||||
qualification_requirements: '',
|
||||
industry_sectors: [],
|
||||
}
|
||||
|
||||
export const AREA_OF_USE_OPTIONS = [
|
||||
@@ -77,6 +81,43 @@ export const OPERATING_MODE_OPTIONS = [
|
||||
'Wartung',
|
||||
]
|
||||
|
||||
export const INDUSTRY_SECTOR_OPTIONS = [
|
||||
'Allgemeiner Maschinenbau',
|
||||
'Automobil / Zulieferer',
|
||||
'Robotik / Cobot',
|
||||
'Medizintechnik',
|
||||
'Lebensmittel / Getraenke',
|
||||
'Verpackung',
|
||||
'Pharma / Chemie',
|
||||
'Bau / Baumaschinen',
|
||||
'Forst / Holzbearbeitung',
|
||||
'Aufzuege / Foerdertechnik',
|
||||
'Textil',
|
||||
'Landmaschinen',
|
||||
'Druck / Papier',
|
||||
'Metall / CNC',
|
||||
'Schweissen / Oberflaechentechnik',
|
||||
]
|
||||
|
||||
/** Maps display labels to MachineTypes for pattern engine filtering */
|
||||
export const INDUSTRY_TO_MACHINE_TYPES: Record<string, string[]> = {
|
||||
'Allgemeiner Maschinenbau': ['general_industry'],
|
||||
'Automobil / Zulieferer': ['automotive'],
|
||||
'Robotik / Cobot': ['robotics_cobot', 'cobot'],
|
||||
'Medizintechnik': ['medical_device', 'infusion_pump', 'ventilator', 'patient_monitor'],
|
||||
'Lebensmittel / Getraenke': ['food_processing'],
|
||||
'Verpackung': ['packaging'],
|
||||
'Pharma / Chemie': ['chemical', 'pharmaceutical'],
|
||||
'Bau / Baumaschinen': ['construction', 'crane', 'excavator'],
|
||||
'Forst / Holzbearbeitung': ['forestry', 'woodworking', 'circular_saw'],
|
||||
'Aufzuege / Foerdertechnik': ['elevator', 'lift', 'escalator', 'conveyor'],
|
||||
'Textil': ['textile', 'spinning', 'weaving', 'finishing'],
|
||||
'Landmaschinen': ['agricultural', 'tractor', 'harvester'],
|
||||
'Druck / Papier': ['printing'],
|
||||
'Metall / CNC': ['cnc', 'metalworking', 'lathe', 'milling'],
|
||||
'Schweissen / Oberflaechentechnik': ['welding', 'surface_treatment'],
|
||||
}
|
||||
|
||||
export const PERSON_GROUP_OPTIONS = [
|
||||
'Bedienpersonal',
|
||||
'Einrichter',
|
||||
@@ -93,7 +134,7 @@ export interface FormSection {
|
||||
number: number
|
||||
title: string
|
||||
description: string
|
||||
icon: 'clipboard' | 'target' | 'alert' | 'box' | 'link' | 'users'
|
||||
icon: 'clipboard' | 'target' | 'alert' | 'box' | 'link' | 'users' | 'briefcase'
|
||||
}
|
||||
|
||||
export const FORM_SECTIONS: FormSection[] = [
|
||||
@@ -139,4 +180,11 @@ export const FORM_SECTIONS: FormSection[] = [
|
||||
description: 'Personengruppen und Qualifikationsanforderungen',
|
||||
icon: 'users',
|
||||
},
|
||||
{
|
||||
id: 'industry_sector',
|
||||
number: 7,
|
||||
title: 'Einsatzbereich / Branche',
|
||||
description: 'Branche bestimmt welche branchenspezifischen Gefaehrdungen beruecksichtigt werden',
|
||||
icon: 'briefcase',
|
||||
},
|
||||
]
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
|
||||
interface Component { id: string; name: string; component_type: string }
|
||||
interface Hazard { id: string; name: string; category: string; operational_states?: string[] }
|
||||
interface Mitigation { id: string; name?: string; title?: string; reduction_type: string; hazard_id?: string; linked_hazard_ids?: string[] }
|
||||
|
||||
export interface GraphNode {
|
||||
id: string
|
||||
type: 'component' | 'hazard' | 'mitigation'
|
||||
label: string
|
||||
subLabel?: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
const NODE_COLORS: Record<string, string> = {
|
||||
component: '#6366F1', // indigo
|
||||
hazard: '#EF4444', // red
|
||||
mitigation: '#10B981', // green
|
||||
}
|
||||
|
||||
export function useKnowledgeGraph(projectId: string) {
|
||||
const [components, setComponents] = useState<Component[]>([])
|
||||
const [hazards, setHazards] = useState<Hazard[]>([])
|
||||
const [mitigations, setMitigations] = useState<Mitigation[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [projectId]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const [compRes, hazRes, mitRes] = await Promise.all([
|
||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/components`),
|
||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards`),
|
||||
fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`),
|
||||
])
|
||||
|
||||
if (compRes.ok) {
|
||||
const j = await compRes.json()
|
||||
setComponents((j.components || j || []).map((c: Record<string, unknown>) => ({
|
||||
id: c.id as string, name: c.name as string, component_type: c.component_type as string || '',
|
||||
})))
|
||||
}
|
||||
if (hazRes.ok) {
|
||||
const j = await hazRes.json()
|
||||
setHazards((j.hazards || j || []).map((h: Record<string, unknown>) => ({
|
||||
id: h.id as string, name: h.name as string, category: h.category as string || '',
|
||||
operational_states: (h.operational_states || []) as string[],
|
||||
})))
|
||||
}
|
||||
if (mitRes.ok) {
|
||||
const j = await mitRes.json()
|
||||
setMitigations((j.mitigations || j || []).map((m: Record<string, unknown>) => ({
|
||||
id: m.id as string, name: (m.name || m.title || '') as string,
|
||||
title: (m.title || m.name || '') as string,
|
||||
reduction_type: (m.reduction_type || '') as string,
|
||||
hazard_id: (m.hazard_id || '') as string,
|
||||
linked_hazard_ids: (m.linked_hazard_ids || []) as string[],
|
||||
})))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load graph data:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const { nodes, edges } = useMemo(() => {
|
||||
const graphNodes: GraphNode[] = []
|
||||
const graphEdges: GraphEdge[] = []
|
||||
|
||||
// Component nodes
|
||||
components.forEach((c) => {
|
||||
graphNodes.push({
|
||||
id: `comp-${c.id}`, type: 'component',
|
||||
label: c.name, subLabel: c.component_type,
|
||||
color: NODE_COLORS.component,
|
||||
})
|
||||
})
|
||||
|
||||
// Hazard nodes
|
||||
hazards.forEach((h) => {
|
||||
graphNodes.push({
|
||||
id: `haz-${h.id}`, type: 'hazard',
|
||||
label: h.name, subLabel: h.category,
|
||||
color: NODE_COLORS.hazard,
|
||||
})
|
||||
// Edge: first component → hazard (simplified — could be per component_id)
|
||||
if (components.length > 0) {
|
||||
graphEdges.push({
|
||||
id: `e-comp-haz-${h.id}`,
|
||||
source: `comp-${components[0].id}`,
|
||||
target: `haz-${h.id}`,
|
||||
label: 'erzeugt',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Mitigation nodes
|
||||
mitigations.forEach((m) => {
|
||||
graphNodes.push({
|
||||
id: `mit-${m.id}`, type: 'mitigation',
|
||||
label: m.title || m.name || m.id,
|
||||
subLabel: m.reduction_type,
|
||||
color: NODE_COLORS.mitigation,
|
||||
})
|
||||
// Edge: mitigation → hazard
|
||||
const hazardIds = m.linked_hazard_ids?.length ? m.linked_hazard_ids : m.hazard_id ? [m.hazard_id] : []
|
||||
hazardIds.forEach((hid) => {
|
||||
graphEdges.push({
|
||||
id: `e-mit-haz-${m.id}-${hid}`,
|
||||
source: `mit-${m.id}`,
|
||||
target: `haz-${hid}`,
|
||||
label: 'schuetzt',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return { nodes: graphNodes, edges: graphEdges }
|
||||
}, [components, hazards, mitigations])
|
||||
|
||||
return { nodes, edges, loading, stats: { components: components.length, hazards: hazards.length, mitigations: mitigations.length } }
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
type Node,
|
||||
type Edge,
|
||||
MarkerType,
|
||||
} from '@xyflow/react'
|
||||
import '@xyflow/react/dist/style.css'
|
||||
import { useKnowledgeGraph } from './_hooks/useKnowledgeGraph'
|
||||
|
||||
const TYPE_STYLES: Record<string, { bg: string; border: string }> = {
|
||||
component: { bg: '#EEF2FF', border: '#6366F1' },
|
||||
hazard: { bg: '#FEF2F2', border: '#EF4444' },
|
||||
mitigation: { bg: '#ECFDF5', border: '#10B981' },
|
||||
}
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
component: 'Komponente',
|
||||
hazard: 'Gefaehrdung',
|
||||
mitigation: 'Massnahme',
|
||||
}
|
||||
|
||||
export default function KnowledgeGraphPage() {
|
||||
const { projectId } = useParams<{ projectId: string }>()
|
||||
const { nodes: graphNodes, edges: graphEdges, loading, stats } = useKnowledgeGraph(projectId)
|
||||
|
||||
// Convert to React Flow nodes with layout
|
||||
const rfNodes = useMemo((): Node[] => {
|
||||
const compNodes = graphNodes.filter((n) => n.type === 'component')
|
||||
const hazNodes = graphNodes.filter((n) => n.type === 'hazard')
|
||||
const mitNodes = graphNodes.filter((n) => n.type === 'mitigation')
|
||||
|
||||
const nodes: Node[] = []
|
||||
const colWidth = 300
|
||||
const rowHeight = 80
|
||||
|
||||
// Column 1: Components
|
||||
compNodes.forEach((n, i) => {
|
||||
nodes.push({
|
||||
id: n.id,
|
||||
position: { x: 0, y: i * rowHeight },
|
||||
data: { label: n.label, subLabel: n.subLabel, nodeType: n.type },
|
||||
style: {
|
||||
background: TYPE_STYLES.component.bg,
|
||||
border: `2px solid ${TYPE_STYLES.component.border}`,
|
||||
borderRadius: '12px',
|
||||
padding: '8px 12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
width: 200,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Column 2: Hazards
|
||||
hazNodes.forEach((n, i) => {
|
||||
nodes.push({
|
||||
id: n.id,
|
||||
position: { x: colWidth, y: i * rowHeight },
|
||||
data: { label: n.label, subLabel: n.subLabel, nodeType: n.type },
|
||||
style: {
|
||||
background: TYPE_STYLES.hazard.bg,
|
||||
border: `2px solid ${TYPE_STYLES.hazard.border}`,
|
||||
borderRadius: '12px',
|
||||
padding: '8px 12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
width: 220,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Column 3: Mitigations
|
||||
mitNodes.forEach((n, i) => {
|
||||
nodes.push({
|
||||
id: n.id,
|
||||
position: { x: colWidth * 2, y: i * rowHeight },
|
||||
data: { label: n.label, subLabel: n.subLabel, nodeType: n.type },
|
||||
style: {
|
||||
background: TYPE_STYLES.mitigation.bg,
|
||||
border: `2px solid ${TYPE_STYLES.mitigation.border}`,
|
||||
borderRadius: '12px',
|
||||
padding: '8px 12px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
width: 220,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return nodes
|
||||
}, [graphNodes])
|
||||
|
||||
const rfEdges = useMemo((): Edge[] => {
|
||||
return graphEdges.map((e) => ({
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
label: e.label,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
style: { stroke: '#94A3B8', strokeWidth: 1.5 },
|
||||
labelStyle: { fontSize: 10, fill: '#64748B' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: '#94A3B8' },
|
||||
}))
|
||||
}, [graphEdges])
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(rfNodes)
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(rfEdges)
|
||||
|
||||
// Update when data loads
|
||||
const onInit = useCallback(() => {
|
||||
if (rfNodes.length > 0) {
|
||||
setNodes(rfNodes)
|
||||
setEdges(rfEdges)
|
||||
}
|
||||
}, [rfNodes, rfEdges, setNodes, setEdges])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Safety Knowledge Graph</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
Interaktive Visualisierung: Komponente → Gefaehrdung → Massnahme
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Legend + Stats */}
|
||||
<div className="flex items-center gap-6">
|
||||
{(['component', 'hazard', 'mitigation'] as const).map((t) => (
|
||||
<div key={t} className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: TYPE_STYLES[t].border }} />
|
||||
<span className="text-xs text-gray-600">{TYPE_LABELS[t]} ({
|
||||
t === 'component' ? stats.components : t === 'hazard' ? stats.hazards : stats.mitigations
|
||||
})</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Graph */}
|
||||
{graphNodes.length === 0 ? (
|
||||
<div className="text-center py-16 text-gray-500">
|
||||
Keine Daten — bitte zuerst Projekt initialisieren.
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden" style={{ height: '600px' }}>
|
||||
<ReactFlow
|
||||
nodes={rfNodes}
|
||||
edges={rfEdges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.2 }}
|
||||
minZoom={0.3}
|
||||
maxZoom={2}
|
||||
nodesDraggable
|
||||
nodesConnectable={false}
|
||||
>
|
||||
<Background gap={20} size={1} color="#f0f0f0" />
|
||||
<Controls />
|
||||
<MiniMap
|
||||
nodeColor={(node) => {
|
||||
const t = (node.data as { nodeType?: string })?.nodeType || 'component'
|
||||
return TYPE_STYLES[t]?.border || '#94A3B8'
|
||||
}}
|
||||
maskColor="rgba(0,0,0,0.05)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+39
-7
@@ -108,15 +108,45 @@ export function useOperationalStates(projectId: string) {
|
||||
setDeltaLoading(true)
|
||||
setDeltaResult(null)
|
||||
try {
|
||||
// Build a MatchInput from the project's current components + proposed states
|
||||
// Build MatchInput from project's components — derive tags from names/types
|
||||
const compRes = await fetch(`/api/sdk/v1/iace/projects/${projectId}/components`)
|
||||
let componentIds: string[] = []
|
||||
let componentTags: string[] = []
|
||||
let energyIds: string[] = []
|
||||
if (compRes.ok) {
|
||||
const cj = await compRes.json()
|
||||
const comps = cj.components || cj || []
|
||||
componentIds = comps.map((c: { library_id?: string }) => c.library_id).filter(Boolean)
|
||||
energyIds = comps.flatMap((c: { energy_source_ids?: string[] }) => c.energy_source_ids || [])
|
||||
const comps = (cj.components || cj || []) as Array<{ library_id?: string; component_type?: string; name?: string; energy_source_ids?: string[] }>
|
||||
// Use library_ids if available, otherwise derive tags from component names/types
|
||||
const libIds = comps.map((c) => c.library_id).filter(Boolean) as string[]
|
||||
if (libIds.length > 0) {
|
||||
componentTags = libIds
|
||||
} else {
|
||||
// Derive tags from component names for pattern matching
|
||||
const tagMap: Record<string, string[]> = {
|
||||
sensor: ['sensor', 'has_sensor'], software: ['software', 'has_software'],
|
||||
firmware: ['firmware', 'has_firmware'], ai_model: ['has_ai', 'ai_model'],
|
||||
hmi: ['hmi', 'display'], electrical: ['electric_motor', 'electric_drive'],
|
||||
network: ['networked', 'ethernet'], actuator: ['actuator', 'hydraulic'],
|
||||
mechanical: ['moving_mechanical_parts'], hydraulic: ['hydraulic'],
|
||||
}
|
||||
const nameKeywords: Record<string, string[]> = {
|
||||
roboter: ['cobot', 'robot_arm'], motor: ['electric_motor', 'electric_drive'],
|
||||
scanner: ['sensor', 'safety_scanner'], sps: ['controller', 'plc'],
|
||||
steuerung: ['controller', 'plc'], greifer: ['actuator', 'gripper'],
|
||||
schutzzaun: ['safety_fence'], lichtgitter: ['light_curtain'],
|
||||
kamera: ['camera', 'sensor'], ventil: ['valve', 'pneumatic'],
|
||||
}
|
||||
const tags = new Set<string>()
|
||||
for (const c of comps) {
|
||||
const typeTags = tagMap[c.component_type || ''] || ['moving_mechanical_parts']
|
||||
typeTags.forEach((t) => tags.add(t))
|
||||
const nameLower = (c.name || '').toLowerCase()
|
||||
for (const [kw, kwTags] of Object.entries(nameKeywords)) {
|
||||
if (nameLower.includes(kw)) kwTags.forEach((t) => tags.add(t))
|
||||
}
|
||||
}
|
||||
componentTags = [...tags]
|
||||
}
|
||||
energyIds = comps.flatMap((c) => c.energy_source_ids || [])
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/delta-analysis`, {
|
||||
@@ -124,13 +154,15 @@ export function useOperationalStates(projectId: string) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
current: {
|
||||
component_library_ids: componentIds,
|
||||
component_library_ids: componentTags,
|
||||
energy_source_ids: energyIds,
|
||||
custom_tags: componentTags,
|
||||
operational_states: metadataRef.current.operational_states || [],
|
||||
},
|
||||
proposed: {
|
||||
component_library_ids: componentIds,
|
||||
component_library_ids: componentTags,
|
||||
energy_source_ids: energyIds,
|
||||
custom_tags: componentTags,
|
||||
operational_states: states,
|
||||
},
|
||||
}),
|
||||
|
||||
+52
-1
@@ -119,10 +119,61 @@ export function ReportPrintView({ data }: ReportPrintViewProps) {
|
||||
Herstellers nach EU Maschinenverordnung 2023/1230 Art. 10.
|
||||
</div>
|
||||
|
||||
{/* 2. Inhaltsverzeichnis */}
|
||||
{/* 2. Methodik der Risikobeurteilung (Erklaerteil) */}
|
||||
<div className="section-break">
|
||||
<h2>Methodik der Risikobeurteilung</h2>
|
||||
<p>
|
||||
Diese Risikobeurteilung orientiert sich an den Grundprinzipien der EN ISO 12100,
|
||||
EN 62061 und EN ISO 13849-1. Bewertet werden Grenzen des Produkts, identifizierte
|
||||
Gefaehrdungen, die jeweilige Risikohoehe sowie das Restrisiko nach Anwendung von
|
||||
Schutzmassnahmen.
|
||||
</p>
|
||||
<p>
|
||||
Der Prozess ist iterativ: Reicht eine Massnahme nicht aus, werden weitere ergriffen
|
||||
und das Restrisiko erneut bewertet, bis ein akzeptables Niveau erreicht ist.
|
||||
</p>
|
||||
<h3>Risikoberechnung</h3>
|
||||
<p>Das Ausgangsrisiko ergibt sich aus: <strong>R = S × F × P × A</strong></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Faktor</th><th>Beschreibung</th><th>Skala</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><strong>S</strong></td><td>Schadensschwere</td><td>1 (Erste Hilfe) – 5 (toedlich)</td></tr>
|
||||
<tr><td><strong>F</strong></td><td>Expositionshaeufigkeit</td><td>1 (selten/kurz) – 5 (dauerhaft)</td></tr>
|
||||
<tr><td><strong>P</strong></td><td>Eintrittswahrscheinlichkeit</td><td>1 (vernachlaessigbar) – 5 (fast sicher)</td></tr>
|
||||
<tr><td><strong>A</strong></td><td>Vermeidbarkeit</td><td>1 (leicht vermeidbar) – 5 (unvermeidbar)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
Bei Sicherheitskreisen wird der Performance Level (PLr) ueber einen Risikographen
|
||||
abgeleitet und dem Safety Integrity Level (SIL) zugeordnet.
|
||||
</p>
|
||||
<h3>Dreistufenmethode</h3>
|
||||
<p>Schutzmassnahmen werden priorisiert angewandt:</p>
|
||||
<ol>
|
||||
<li><strong>Konstruktive Massnahmen (KM)</strong> — Inhaerent sichere Gestaltung</li>
|
||||
<li><strong>Technische Schutzmassnahmen (TM)</strong> — Schutzeinrichtungen, Sicherheitssteuerungen</li>
|
||||
<li><strong>Benutzerinformationen (BI)</strong> — Warnhinweise, Betriebsanleitung</li>
|
||||
</ol>
|
||||
<h3>Akzeptanz des Restrisikos</h3>
|
||||
<p>
|
||||
Ein Restrisiko gilt als hinreichend gemindert, wenn alle praktisch umsetzbaren Massnahmen
|
||||
ausgeschoepft wurden und Anwender ueber verbleibende Restrisiken informiert sind.
|
||||
Die Akzeptanz wird pro Gefaehrdung mit <strong>JA</strong> / <strong>NEIN</strong> dokumentiert.
|
||||
</p>
|
||||
<p style={{ fontStyle: 'italic', fontSize: '9pt', color: '#374151' }}>
|
||||
„Die Moeglichkeit, einen hoeheren Sicherheitsgrad zu erreichen, oder die Verfuegbarkeit
|
||||
anderer Produkte, die ein geringeres Risiko darstellen, ist kein ausreichender Grund,
|
||||
ein Produkt als gefaehrlich anzusehen.“ — § 3 Abs. 2 ProdSG
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 3. Inhaltsverzeichnis */}
|
||||
<div className="section-break">
|
||||
<h2>Inhaltsverzeichnis</h2>
|
||||
<ol className="toc">
|
||||
<li>Methodik der Risikobeurteilung</li>
|
||||
<li>Maschinenbeschreibung</li>
|
||||
<li>Angewandte Normen</li>
|
||||
<li>Gefaehrdungsliste</li>
|
||||
|
||||
@@ -21,8 +21,10 @@ const IACE_NAV_ITEMS = [
|
||||
|
||||
const IACE_EXTRA_ITEMS = [
|
||||
{ id: 'fmea', label: 'FMEA', href: '/fmea', icon: 'grid' },
|
||||
{ id: 'knowledge-graph', label: 'Knowledge Graph', href: '/knowledge-graph', icon: 'activity' },
|
||||
{ id: 'classification', label: 'Klassifikation', href: '/classification', icon: 'tag' },
|
||||
{ id: 'monitoring', label: 'Monitoring', href: '/monitoring', icon: 'activity' },
|
||||
{ id: 'benchmark', label: 'Benchmark', href: '/benchmark', icon: 'check' },
|
||||
]
|
||||
|
||||
function NavIcon({ icon, className }: { icon: string; className?: string }) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
assessmentId: string
|
||||
backendUrl: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onComplete: (result: any) => void
|
||||
onError: (msg: string) => void
|
||||
}
|
||||
|
||||
export function AssessmentProgress({ assessmentId, backendUrl, onComplete, onError }: Props) {
|
||||
const [progress, setProgress] = useState('Initialisierung...')
|
||||
const [dots, setDots] = useState(0)
|
||||
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const dotTimer = setInterval(() => setDots(d => (d + 1) % 4), 500)
|
||||
|
||||
pollRef.current = setInterval(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${backendUrl}/api/vendor-compliance/assessments/${assessmentId}`,
|
||||
)
|
||||
if (!res.ok) return
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (data.status === 'completed' && data.result) {
|
||||
if (pollRef.current) clearInterval(pollRef.current)
|
||||
clearInterval(dotTimer)
|
||||
onComplete(data.result)
|
||||
return
|
||||
}
|
||||
|
||||
if (data.status === 'failed') {
|
||||
if (pollRef.current) clearInterval(pollRef.current)
|
||||
clearInterval(dotTimer)
|
||||
onError(data.error || 'Pruefung fehlgeschlagen')
|
||||
return
|
||||
}
|
||||
|
||||
if (data.progress) {
|
||||
setProgress(data.progress)
|
||||
}
|
||||
} catch {
|
||||
// retry silently
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
return () => {
|
||||
if (pollRef.current) clearInterval(pollRef.current)
|
||||
clearInterval(dotTimer)
|
||||
}
|
||||
}, [assessmentId, backendUrl, onComplete, onError])
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-6 relative">
|
||||
<div className="absolute inset-0 border-4 border-blue-200 rounded-full" />
|
||||
<div className="absolute inset-0 border-4 border-blue-600 rounded-full border-t-transparent animate-spin" />
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-semibold text-gray-800 mb-2">
|
||||
Vertragspruefung laeuft
|
||||
</h2>
|
||||
|
||||
<p className="text-gray-600 text-sm mb-6">
|
||||
{progress}{'.'.repeat(dots)}
|
||||
</p>
|
||||
|
||||
<div className="max-w-md mx-auto">
|
||||
<div className="flex items-center gap-3 text-left text-xs text-gray-500 mb-2">
|
||||
<span className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold">1</span>
|
||||
Text extrahieren
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-left text-xs text-gray-500 mb-2">
|
||||
<span className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold">2</span>
|
||||
Checklisten pruefen (L1/L2)
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-left text-xs text-gray-500 mb-2">
|
||||
<span className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold">3</span>
|
||||
Cross-Check zwischen Dokumenten
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-left text-xs text-gray-500">
|
||||
<span className="w-5 h-5 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold">4</span>
|
||||
Pruefprotokoll generieren
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface DocumentEntry {
|
||||
doc_type: string
|
||||
label: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onStart: (vendorName: string, documents: DocumentEntry[]) => void
|
||||
}
|
||||
|
||||
const DOC_TYPES = [
|
||||
{ value: 'auto', label: 'Automatisch erkennen' },
|
||||
{ value: 'avv', label: 'AVV / Auftragsverarbeitungsvertrag' },
|
||||
{ value: 'scc', label: 'SCC / Standardvertragsklauseln' },
|
||||
{ value: 'tom_annex', label: 'TOM-Anlage (Art. 32)' },
|
||||
{ value: 'sub_processor_list', label: 'Sub-Processor-Liste' },
|
||||
{ value: 'agb', label: 'AGB / Nutzungsbedingungen' },
|
||||
]
|
||||
|
||||
export function DocumentUploader({ onStart }: Props) {
|
||||
const [vendorName, setVendorName] = useState('')
|
||||
const [entries, setEntries] = useState<DocumentEntry[]>([
|
||||
{ doc_type: 'auto', label: '', url: '' },
|
||||
])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const updateEntry = (idx: number, field: keyof DocumentEntry, value: string) => {
|
||||
setEntries(prev => {
|
||||
const copy = [...prev]
|
||||
copy[idx] = { ...copy[idx], [field]: value }
|
||||
return copy
|
||||
})
|
||||
}
|
||||
|
||||
const addEntry = () => {
|
||||
setEntries(prev => [...prev, { doc_type: 'auto', label: '', url: '' }])
|
||||
}
|
||||
|
||||
const removeEntry = (idx: number) => {
|
||||
if (entries.length <= 1) return
|
||||
setEntries(prev => prev.filter((_, i) => i !== idx))
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const valid = entries.filter(d => d.url.trim())
|
||||
if (!vendorName.trim() || valid.length === 0) return
|
||||
setLoading(true)
|
||||
onStart(vendorName.trim(), valid.map(d => ({
|
||||
...d,
|
||||
label: d.label || `${DOC_TYPES.find(t => t.value === d.doc_type)?.label || d.doc_type}: ${vendorName}`,
|
||||
})))
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Vendor Name */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Auftragsverarbeiter / Provider *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={vendorName}
|
||||
onChange={e => setVendorName(e.target.value)}
|
||||
placeholder="z.B. SysEleven GmbH, Amazon Web Services, Microsoft"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Documents */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Dokumente</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Fuegen Sie die URLs der Vertragsdokumente hinzu. Das System erkennt den Dokumenttyp automatisch.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{entries.map((entry, idx) => (
|
||||
<div key={idx} className="flex items-start gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="w-52 shrink-0">
|
||||
<select
|
||||
value={entry.doc_type}
|
||||
onChange={e => updateEntry(idx, 'doc_type', e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm bg-white"
|
||||
>
|
||||
{DOC_TYPES.map(t => (
|
||||
<option key={t.value} value={t.value}>{t.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="url"
|
||||
value={entry.url}
|
||||
onChange={e => updateEntry(idx, 'url', e.target.value)}
|
||||
placeholder="https://example.com/avv.pdf"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="w-44 shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={entry.label}
|
||||
onChange={e => updateEntry(idx, 'label', e.target.value)}
|
||||
placeholder="Bezeichnung (optional)"
|
||||
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
{entries.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeEntry(idx)}
|
||||
className="p-2 text-gray-400 hover:text-red-500"
|
||||
title="Entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addEntry}
|
||||
className="mt-3 w-full py-2.5 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-600 text-sm"
|
||||
>
|
||||
+ Weiteres Dokument hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !vendorName.trim() || !entries.some(e => e.url.trim())}
|
||||
className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Wird gestartet...' : 'Pruefung starten'}
|
||||
</button>
|
||||
<p className="text-xs text-gray-400">
|
||||
Dokumente werden automatisch gegen Art. 28 DSGVO, Art. 32, Art. 44-49 und weitere Anforderungen geprueft.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
result: {
|
||||
vendor_name: string
|
||||
documents: DocumentResult[]
|
||||
findings: Finding[]
|
||||
overall_score: number
|
||||
category_scores: Record<string, number>
|
||||
cross_check_findings: CrossCheckFinding[]
|
||||
report_html: string
|
||||
checked_at: string
|
||||
}
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
interface DocumentResult {
|
||||
label: string
|
||||
doc_type: string
|
||||
completeness_pct: number
|
||||
correctness_pct: number
|
||||
checks: Check[]
|
||||
findings_count: number
|
||||
error: string
|
||||
}
|
||||
|
||||
interface Check {
|
||||
id: string
|
||||
label: string
|
||||
passed: boolean
|
||||
severity: string
|
||||
level: number
|
||||
parent: string | null
|
||||
skipped: boolean
|
||||
hint: string
|
||||
}
|
||||
|
||||
interface Finding {
|
||||
id: string
|
||||
category: string
|
||||
severity: string
|
||||
type: string
|
||||
title: string
|
||||
description: string
|
||||
document_label: string
|
||||
document_type: string
|
||||
}
|
||||
|
||||
interface CrossCheckFinding {
|
||||
id: string
|
||||
label: string
|
||||
severity: string
|
||||
hint: string
|
||||
}
|
||||
|
||||
const CAT_LABELS: Record<string, string> = {
|
||||
INSTRUCTION: 'Weisungsgebundenheit',
|
||||
CONFIDENTIALITY: 'Vertraulichkeit',
|
||||
TOM: 'TOM (Art. 32)',
|
||||
SUBPROCESSOR: 'Unterauftragsverarbeitung',
|
||||
DATA_SUBJECT_RIGHTS: 'Betroffenenrechte',
|
||||
DELETION: 'Loeschung/Rueckgabe',
|
||||
AUDIT_RIGHTS: 'Audit-/Inspektionsrechte',
|
||||
INCIDENT: 'Datenschutzverletzungen',
|
||||
TRANSFER: 'Drittlandtransfer',
|
||||
LIABILITY: 'Haftung',
|
||||
AVV_CONTENT: 'AVV Inhalt',
|
||||
}
|
||||
|
||||
const SEV_COLORS: Record<string, string> = {
|
||||
CRITICAL: 'bg-red-100 text-red-700 border-red-200',
|
||||
HIGH: 'bg-orange-100 text-orange-700 border-orange-200',
|
||||
MEDIUM: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
LOW: 'bg-green-100 text-green-700 border-green-200',
|
||||
}
|
||||
|
||||
function scoreColor(s: number) {
|
||||
if (s >= 80) return 'text-green-600'
|
||||
if (s >= 50) return 'text-yellow-600'
|
||||
return 'text-red-600'
|
||||
}
|
||||
|
||||
function scoreBg(s: number) {
|
||||
if (s >= 80) return 'bg-green-50 border-green-200'
|
||||
if (s >= 50) return 'bg-yellow-50 border-yellow-200'
|
||||
return 'bg-red-50 border-red-200'
|
||||
}
|
||||
|
||||
function verdict(s: number) {
|
||||
if (s >= 80) return 'Bestanden'
|
||||
if (s >= 50) return 'Bedingt bestanden'
|
||||
return 'Nicht bestanden'
|
||||
}
|
||||
|
||||
export function PruefprotokollView({ result, onReset }: Props) {
|
||||
const [expandedDoc, setExpandedDoc] = useState<number | null>(null)
|
||||
const [showHtml, setShowHtml] = useState(false)
|
||||
|
||||
const criticalCount = result.findings.filter(f => f.severity === 'CRITICAL').length
|
||||
+ result.cross_check_findings.filter(f => f.severity === 'CRITICAL').length
|
||||
const totalFindings = result.findings.length + result.cross_check_findings.length
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Score Overview */}
|
||||
<div className={`rounded-xl border-2 p-8 text-center ${scoreBg(result.overall_score)}`}>
|
||||
<div className="text-sm text-gray-500 mb-1">Pruefprotokoll — {result.vendor_name}</div>
|
||||
<div className={`text-6xl font-extrabold ${scoreColor(result.overall_score)}`}>
|
||||
{result.overall_score}%
|
||||
</div>
|
||||
<div className="text-lg text-gray-600 mt-1">{verdict(result.overall_score)}</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-white rounded-xl border p-5 text-center">
|
||||
<div className="text-3xl font-bold text-gray-900">{result.documents.length}</div>
|
||||
<div className="text-xs text-gray-500 mt-1 uppercase tracking-wide">Dokumente</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border p-5 text-center">
|
||||
<div className="text-3xl font-bold text-gray-900">{totalFindings}</div>
|
||||
<div className="text-xs text-gray-500 mt-1 uppercase tracking-wide">Findings</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border p-5 text-center">
|
||||
<div className="text-3xl font-bold text-red-600">{criticalCount}</div>
|
||||
<div className="text-xs text-gray-500 mt-1 uppercase tracking-wide">Kritisch</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Scores */}
|
||||
{Object.keys(result.category_scores).length > 0 && (
|
||||
<div className="bg-white rounded-xl border p-5">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">Kategorie-Uebersicht</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(result.category_scores)
|
||||
.sort(([, a], [, b]) => a - b)
|
||||
.map(([cat, score]) => (
|
||||
<div key={cat} className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-700 w-48 truncate">{CAT_LABELS[cat] || cat}</span>
|
||||
<div className="flex-1 h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${score >= 80 ? 'bg-green-500' : score >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${score}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={`text-sm font-bold w-12 text-right ${scoreColor(score)}`}>{score}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cross-Check Findings */}
|
||||
{result.cross_check_findings.length > 0 && (
|
||||
<div className="bg-white rounded-xl border p-5">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">
|
||||
Dokumenten-Cross-Check
|
||||
<span className="ml-2 text-sm font-normal text-gray-400">{result.cross_check_findings.length} Findings</span>
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{result.cross_check_findings.map(f => (
|
||||
<div key={f.id} className={`border-l-4 rounded-r-lg p-3 ${
|
||||
f.severity === 'CRITICAL' ? 'border-red-500 bg-red-50' :
|
||||
f.severity === 'HIGH' ? 'border-orange-500 bg-orange-50' :
|
||||
'border-yellow-500 bg-yellow-50'
|
||||
}`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-bold border ${SEV_COLORS[f.severity] || ''}`}>
|
||||
{f.severity}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-900">{f.label}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1 leading-relaxed">{f.hint}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents Detail */}
|
||||
<div className="bg-white rounded-xl border p-5">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">Gepruefte Dokumente</h3>
|
||||
<div className="space-y-3">
|
||||
{result.documents.map((doc, i) => (
|
||||
<div key={i} className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedDoc(expandedDoc === i ? null : i)}
|
||||
className="w-full text-left p-4 hover:bg-gray-50 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="px-2 py-0.5 bg-indigo-100 text-indigo-700 rounded text-xs font-bold">
|
||||
{doc.doc_type.toUpperCase()}
|
||||
</span>
|
||||
<span className="font-medium text-gray-900 text-sm">{doc.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<span className={`text-sm font-bold ${scoreColor(doc.completeness_pct)}`}>
|
||||
{doc.completeness_pct}%
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 ml-1">vollstaendig</span>
|
||||
</div>
|
||||
{doc.findings_count > 0 && (
|
||||
<span className="px-2 py-0.5 bg-red-100 text-red-700 rounded-full text-xs font-bold">
|
||||
{doc.findings_count}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-400 text-sm">{expandedDoc === i ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expandedDoc === i && (
|
||||
<div className="border-t p-4 bg-gray-50">
|
||||
{doc.error ? (
|
||||
<p className="text-red-600 text-sm">{doc.error}</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{doc.checks.filter(c => c.level === 1).map(c => {
|
||||
const l2s = doc.checks.filter(l => l.level === 2 && l.parent === c.id)
|
||||
return (
|
||||
<div key={c.id}>
|
||||
<div className={`flex items-center gap-2 py-1 ${!c.passed && !c.skipped ? 'text-red-700' : ''}`}>
|
||||
<span>{c.passed ? '✓' : c.skipped ? '—' : '✗'}</span>
|
||||
<span className="text-sm">{c.label}</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${SEV_COLORS[c.severity] || ''}`}>
|
||||
{c.severity}
|
||||
</span>
|
||||
</div>
|
||||
{!c.passed && !c.skipped && c.hint && (
|
||||
<div className="ml-6 mb-1 text-xs text-red-600 bg-red-50 border-l-2 border-red-300 pl-2 py-1">
|
||||
{c.hint}
|
||||
</div>
|
||||
)}
|
||||
{l2s.map(l2 => (
|
||||
<div key={l2.id} className={`ml-6 flex items-center gap-2 py-0.5 text-xs ${!l2.passed ? 'text-red-600' : 'text-gray-500'}`}>
|
||||
<span>{l2.passed ? '✓' : l2.skipped ? '—' : '✗'}</span>
|
||||
<span>{l2.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Findings Detail */}
|
||||
{result.findings.length > 0 && (
|
||||
<div className="bg-white rounded-xl border p-5">
|
||||
<h3 className="font-semibold text-gray-800 mb-4">
|
||||
Alle Findings
|
||||
<span className="ml-2 text-sm font-normal text-gray-400">{result.findings.length}</span>
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{[...result.findings]
|
||||
.sort((a, b) => {
|
||||
const order: Record<string, number> = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 }
|
||||
return (order[a.severity] ?? 4) - (order[b.severity] ?? 4)
|
||||
})
|
||||
.map(f => (
|
||||
<div key={f.id} className="border border-gray-200 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-bold border ${SEV_COLORS[f.severity] || ''}`}>
|
||||
{f.severity}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-900">{f.title}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
{CAT_LABELS[f.category] || f.category} | {f.document_label}
|
||||
</div>
|
||||
{f.description && (
|
||||
<p className="text-xs text-gray-600 leading-relaxed">{f.description}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="px-5 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 text-sm font-medium"
|
||||
>
|
||||
Neue Pruefung
|
||||
</button>
|
||||
{result.report_html && (
|
||||
<button
|
||||
onClick={() => setShowHtml(!showHtml)}
|
||||
className="px-5 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm font-medium"
|
||||
>
|
||||
{showHtml ? 'Protokoll ausblenden' : 'Pruefprotokoll (Druckversion)'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Print-ready HTML report */}
|
||||
{showHtml && result.report_html && (
|
||||
<div className="bg-white rounded-xl border p-6 mt-4">
|
||||
<div dangerouslySetInnerHTML={{ __html: result.report_html }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { DocumentUploader } from './_components/DocumentUploader'
|
||||
import { AssessmentProgress } from './_components/AssessmentProgress'
|
||||
import { PruefprotokollView } from './_components/PruefprotokollView'
|
||||
|
||||
type View = 'upload' | 'progress' | 'result'
|
||||
|
||||
interface AssessmentResult {
|
||||
vendor_name: string
|
||||
documents: DocumentResult[]
|
||||
findings: Finding[]
|
||||
overall_score: number
|
||||
category_scores: Record<string, number>
|
||||
cross_check_findings: CrossCheckFinding[]
|
||||
report_html: string
|
||||
checked_at: string
|
||||
}
|
||||
|
||||
interface DocumentResult {
|
||||
label: string
|
||||
url: string
|
||||
doc_type: string
|
||||
word_count: number
|
||||
completeness_pct: number
|
||||
correctness_pct: number
|
||||
checks: Check[]
|
||||
findings_count: number
|
||||
error: string
|
||||
}
|
||||
|
||||
interface Check {
|
||||
id: string
|
||||
label: string
|
||||
passed: boolean
|
||||
severity: string
|
||||
level: number
|
||||
parent: string | null
|
||||
skipped: boolean
|
||||
hint: string
|
||||
matched_text: string
|
||||
}
|
||||
|
||||
interface Finding {
|
||||
id: string
|
||||
category: string
|
||||
severity: string
|
||||
type: string
|
||||
title: string
|
||||
description: string
|
||||
recommendation: string
|
||||
document_label: string
|
||||
document_type: string
|
||||
}
|
||||
|
||||
interface CrossCheckFinding {
|
||||
id: string
|
||||
label: string
|
||||
severity: string
|
||||
hint: string
|
||||
}
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_COMPLIANCE_API_URL || ''
|
||||
|
||||
export default function VendorAssessmentPage() {
|
||||
const [view, setView] = useState<View>('upload')
|
||||
const [assessmentId, setAssessmentId] = useState<string>('')
|
||||
const [result, setResult] = useState<AssessmentResult | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleStartAssessment = useCallback(async (
|
||||
vendorName: string,
|
||||
documents: Array<{ doc_type: string; label: string; url: string }>,
|
||||
) => {
|
||||
setError('')
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/vendor-compliance/assessments`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ vendor_name: vendorName, documents }),
|
||||
})
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
const data = await res.json()
|
||||
setAssessmentId(data.assessment_id)
|
||||
setView('progress')
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Fehler beim Starten')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleComplete = useCallback((data: AssessmentResult) => {
|
||||
setResult(data)
|
||||
setView('result')
|
||||
}, [])
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setView('upload')
|
||||
setAssessmentId('')
|
||||
setResult(null)
|
||||
setError('')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-5xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Vertragspruefung</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Automatisierte Pruefung von Auftragsverarbeitungsvertraegen gem. Art. 28 DSGVO
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-700 text-sm">{error}</p>
|
||||
<button onClick={() => setError('')} className="text-xs text-red-500 mt-1 underline">
|
||||
Schliessen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === 'upload' && (
|
||||
<DocumentUploader onStart={handleStartAssessment} />
|
||||
)}
|
||||
|
||||
{view === 'progress' && assessmentId && (
|
||||
<AssessmentProgress
|
||||
assessmentId={assessmentId}
|
||||
backendUrl={BACKEND_URL}
|
||||
onComplete={handleComplete}
|
||||
onError={(msg) => { setError(msg); setView('upload') }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view === 'result' && result && (
|
||||
<PruefprotokollView result={result} onReset={handleReset} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -95,6 +95,32 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side
|
||||
collapsed={collapsed}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<AdditionalModuleItem
|
||||
href="/sdk/vendor-assessment"
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
}
|
||||
label="Vertragspruefung"
|
||||
isActive={pathname?.startsWith('/sdk/vendor-assessment') ?? false}
|
||||
collapsed={collapsed}
|
||||
projectId={projectId}
|
||||
/>
|
||||
<AdditionalModuleItem
|
||||
href="/sdk/audit-timeline"
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
}
|
||||
label="Audit Timeline"
|
||||
isActive={pathname?.startsWith('/sdk/audit-timeline') ?? false}
|
||||
collapsed={collapsed}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Payment / Terminal */}
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* CMP Phase 3 + DSR Integration Tests
|
||||
*
|
||||
* Tests the complete CMP lifecycle including:
|
||||
* - Vendor-agnostic consent fields (consent_method, browser, os, etc.)
|
||||
* - Script/cookie tracking (scripts_blocked, scripts_released, cookies_set)
|
||||
* - Session ID tracking
|
||||
* - GeoIP via timezone mapping
|
||||
* - Vendor-level consent (vendor_consents dict)
|
||||
* - DSR scenarios: Art. 15 Auskunft, Art. 17 Löschung, Art. 20 Portabilität
|
||||
* - Email linking for DSR (device → user mapping)
|
||||
* - Admin modal features (vendor display, withdraw, email linking)
|
||||
*/
|
||||
|
||||
const API_BASE = process.env.PLAYWRIGHT_API_URL || 'https://macmini:3007/api/sdk/v1/banner'
|
||||
const TENANT_ID = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
|
||||
const HEADERS = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-ID': TENANT_ID,
|
||||
}
|
||||
|
||||
const TS = Date.now()
|
||||
const SITE_ID = `e2e-cmp3-${TS}`
|
||||
const DEVICE_FP = `e2e-device-${TS}`
|
||||
|
||||
// ============================================================================
|
||||
// 1. Vendor-Agnostic Consent Fields
|
||||
// ============================================================================
|
||||
|
||||
test.describe('Vendor-Agnostic Consent Fields', () => {
|
||||
test('should store all 20+ fields on consent', async ({ request }) => {
|
||||
// Create site config first
|
||||
await request.post(`${API_BASE}/admin/sites`, {
|
||||
headers: HEADERS,
|
||||
data: { site_id: SITE_ID, site_name: 'E2E CMP Phase 3', site_url: 'https://test.example.com' },
|
||||
})
|
||||
|
||||
// Record consent with all vendor-agnostic fields
|
||||
const res = await request.post(`${API_BASE}/consent`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: DEVICE_FP,
|
||||
categories: ['essential', 'functional', 'analytics'],
|
||||
vendors: ['Google Analytics', 'Matomo'],
|
||||
vendor_consents: { 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false },
|
||||
user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) E2E-Test',
|
||||
consent_method: 'custom_selection',
|
||||
page_url: 'https://test.example.com/pricing',
|
||||
referrer: 'https://google.com',
|
||||
device_type: 'desktop',
|
||||
browser: 'Chrome/120.0',
|
||||
os: 'Mac OS X 10.15.7',
|
||||
screen_resolution: '1920x1080',
|
||||
consent_scope: 'domain',
|
||||
session_id: 'e2e-session-001',
|
||||
timezone: 'Europe/Berlin',
|
||||
scripts_blocked: [{ src: 'https://connect.facebook.net/fbevents.js', category: 'marketing' }],
|
||||
scripts_released: [{ src: 'https://www.googletagmanager.com/gtag/js', category: 'analytics' }],
|
||||
cookies_set: [
|
||||
{ name: '_ga', domain: '.test.example.com', expiry_days: 730, category: 'analytics' },
|
||||
{ name: 'bp_consent', domain: '.test.example.com', expiry_days: 365, category: 'essential' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status()).toBe(200)
|
||||
const consent = await res.json()
|
||||
expect(consent.id).toBeTruthy()
|
||||
expect(consent.consent_method).toBe('custom_selection')
|
||||
expect(consent.device_type).toBe('desktop')
|
||||
expect(consent.browser).toBe('Chrome/120.0')
|
||||
expect(consent.os).toBe('Mac OS X 10.15.7')
|
||||
expect(consent.page_url).toBe('https://test.example.com/pricing')
|
||||
expect(consent.session_id).toBe('e2e-session-001')
|
||||
expect(consent.geo_country).toBe('DE') // Europe/Berlin → DE
|
||||
expect(consent.scripts_released).toHaveLength(1)
|
||||
expect(consent.cookies_set).toHaveLength(2)
|
||||
expect(consent.vendor_consents).toEqual({ 'Google Analytics': true, 'Matomo': true, 'Facebook Pixel': false })
|
||||
})
|
||||
|
||||
test('should update consent on same fingerprint (upsert)', async ({ request }) => {
|
||||
const res = await request.post(`${API_BASE}/consent`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: DEVICE_FP,
|
||||
categories: ['essential'], // changed from all 3 to essential only
|
||||
vendors: [],
|
||||
consent_method: 'reject_all',
|
||||
page_url: 'https://test.example.com/settings',
|
||||
timezone: 'Europe/Vienna',
|
||||
},
|
||||
})
|
||||
|
||||
expect(res.status()).toBe(200)
|
||||
const consent = await res.json()
|
||||
expect(consent.consent_method).toBe('reject_all')
|
||||
expect(consent.geo_country).toBe('AT') // Europe/Vienna → AT
|
||||
expect(consent.categories).toEqual(['essential'])
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 2. DSR Scenarios — Art. 15 Auskunft
|
||||
// ============================================================================
|
||||
|
||||
test.describe('DSR — Art. 15 Auskunftsrecht', () => {
|
||||
const DSR_EMAIL = `dsr-user-${TS}@example.com`
|
||||
const DSR_DEVICE_1 = `dsr-desktop-${TS}`
|
||||
const DSR_DEVICE_2 = `dsr-mobile-${TS}`
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
// Scenario: User visited website from 2 devices, then linked their email
|
||||
|
||||
// Device 1: Desktop consent
|
||||
await request.post(`${API_BASE}/consent`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: DSR_DEVICE_1,
|
||||
categories: ['essential', 'analytics'],
|
||||
consent_method: 'accept_all',
|
||||
device_type: 'desktop',
|
||||
browser: 'Firefox/121.0',
|
||||
page_url: 'https://test.example.com/',
|
||||
timezone: 'Europe/Berlin',
|
||||
},
|
||||
})
|
||||
|
||||
// Device 2: Mobile consent
|
||||
await request.post(`${API_BASE}/consent`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: DSR_DEVICE_2,
|
||||
categories: ['essential'],
|
||||
consent_method: 'reject_all',
|
||||
device_type: 'mobile',
|
||||
browser: 'Safari/17.0',
|
||||
page_url: 'https://test.example.com/pricing',
|
||||
timezone: 'Europe/Berlin',
|
||||
},
|
||||
})
|
||||
|
||||
// User logs in and links email to both devices
|
||||
await request.post(`${API_BASE}/consent/link-email`, {
|
||||
headers: HEADERS,
|
||||
data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_1, email: DSR_EMAIL },
|
||||
})
|
||||
await request.post(`${API_BASE}/consent/link-email`, {
|
||||
headers: HEADERS,
|
||||
data: { site_id: SITE_ID, device_fingerprint: DSR_DEVICE_2, email: DSR_EMAIL },
|
||||
})
|
||||
})
|
||||
|
||||
test('Art. 15 — should find all consents by email', async ({ request }) => {
|
||||
const res = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
|
||||
expect(res.status()).toBe(200)
|
||||
const consents = await res.json()
|
||||
expect(consents).toHaveLength(2)
|
||||
expect(consents.map((c: { device_fingerprint: string }) => c.device_fingerprint).sort()).toEqual(
|
||||
[DSR_DEVICE_1, DSR_DEVICE_2].sort()
|
||||
)
|
||||
})
|
||||
|
||||
test('Art. 15/20 — should export all consent data for DSR', async ({ request }) => {
|
||||
const res = await request.get(`${API_BASE}/consent/dsr-export/${DSR_EMAIL}`, { headers: HEADERS })
|
||||
expect(res.status()).toBe(200)
|
||||
const exportData = await res.json()
|
||||
expect(exportData.consents).toHaveLength(2)
|
||||
expect(exportData.audit_trail.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('Art. 17 — should delete all consents by email (erasure)', async ({ request }) => {
|
||||
const res = await request.delete(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
|
||||
expect(res.status()).toBe(200)
|
||||
const result = await res.json()
|
||||
expect(result.deleted_count).toBe(2)
|
||||
|
||||
// Verify deletion
|
||||
const check = await request.get(`${API_BASE}/consent/by-email/${DSR_EMAIL}`, { headers: HEADERS })
|
||||
const remaining = await check.json()
|
||||
expect(remaining).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 3. DSR Scenarios — Cookie Banner User (anonymous)
|
||||
// ============================================================================
|
||||
|
||||
test.describe('DSR — Anonymous Cookie Banner User', () => {
|
||||
const ANON_DEVICE = `anon-user-${TS}`
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
await request.post(`${API_BASE}/consent`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: ANON_DEVICE,
|
||||
categories: ['essential', 'functional'],
|
||||
consent_method: 'custom_selection',
|
||||
device_type: 'tablet',
|
||||
browser: 'Chrome/120.0',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('should export consent by device fingerprint', async ({ request }) => {
|
||||
const res = await request.get(
|
||||
`${API_BASE}/consent/export?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
|
||||
{ headers: HEADERS }
|
||||
)
|
||||
expect(res.status()).toBe(200)
|
||||
const data = await res.json()
|
||||
expect(data.device_fingerprint).toBe(ANON_DEVICE)
|
||||
expect(data.consents).toHaveLength(1)
|
||||
expect(data.audit_trail.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('should withdraw consent by ID', async ({ request }) => {
|
||||
// Get consent ID first
|
||||
const getRes = await request.get(
|
||||
`${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
|
||||
{ headers: HEADERS }
|
||||
)
|
||||
const { consent } = await getRes.json()
|
||||
expect(consent).toBeTruthy()
|
||||
|
||||
// Withdraw
|
||||
const delRes = await request.delete(`${API_BASE}/consent/${consent.id}`, { headers: HEADERS })
|
||||
expect(delRes.status()).toBe(200)
|
||||
|
||||
// Verify
|
||||
const checkRes = await request.get(
|
||||
`${API_BASE}/consent?site_id=${SITE_ID}&device_fingerprint=${ANON_DEVICE}`,
|
||||
{ headers: HEADERS }
|
||||
)
|
||||
const result = await checkRes.json()
|
||||
expect(result.has_consent).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 4. DSR Scenarios — Login User (Customer) who also used Cookie Banner
|
||||
// ============================================================================
|
||||
|
||||
test.describe('DSR — Customer with Banner + Login', () => {
|
||||
const CUSTOMER_EMAIL = `customer-${TS}@company.com`
|
||||
const CUSTOMER_DEVICE = `customer-device-${TS}`
|
||||
|
||||
test('full lifecycle: consent → login → link → Art.15 → Art.17', async ({ request }) => {
|
||||
// Step 1: Anonymous visit → cookie consent
|
||||
const consentRes = await request.post(`${API_BASE}/consent`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: CUSTOMER_DEVICE,
|
||||
categories: ['essential', 'analytics'],
|
||||
consent_method: 'accept_all',
|
||||
device_type: 'desktop',
|
||||
browser: 'Edge/120.0',
|
||||
page_url: 'https://test.example.com/',
|
||||
timezone: 'Europe/Zurich',
|
||||
scripts_released: [{ src: 'https://cdn.matomo.cloud/test.js', category: 'analytics' }],
|
||||
cookies_set: [{ name: '_pk_id', domain: '.test.example.com', expiry_days: 393, category: 'analytics' }],
|
||||
},
|
||||
})
|
||||
expect(consentRes.status()).toBe(200)
|
||||
const consent = await consentRes.json()
|
||||
expect(consent.geo_country).toBe('CH') // Europe/Zurich → CH
|
||||
|
||||
// Step 2: Customer logs in → email linked
|
||||
const linkRes = await request.post(`${API_BASE}/consent/link-email`, {
|
||||
headers: HEADERS,
|
||||
data: { site_id: SITE_ID, device_fingerprint: CUSTOMER_DEVICE, email: CUSTOMER_EMAIL },
|
||||
})
|
||||
expect(linkRes.status()).toBe(200)
|
||||
|
||||
// Step 3: Art. 15 — Customer requests their data
|
||||
const exportRes = await request.get(`${API_BASE}/consent/dsr-export/${CUSTOMER_EMAIL}`, { headers: HEADERS })
|
||||
expect(exportRes.status()).toBe(200)
|
||||
const exportData = await exportRes.json()
|
||||
expect(exportData.consents.length).toBeGreaterThan(0)
|
||||
expect(exportData.audit_trail.length).toBeGreaterThan(0)
|
||||
|
||||
// Verify export contains all consent details
|
||||
const exported = exportData.consents[0]
|
||||
expect(exported.categories).toContain('analytics')
|
||||
expect(exported.linked_email).toBe(CUSTOMER_EMAIL)
|
||||
|
||||
// Step 4: Art. 17 — Customer requests erasure
|
||||
const deleteRes = await request.delete(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS })
|
||||
expect(deleteRes.status()).toBe(200)
|
||||
|
||||
// Step 5: Verify complete erasure
|
||||
const verifyRes = await request.get(`${API_BASE}/consent/by-email/${CUSTOMER_EMAIL}`, { headers: HEADERS })
|
||||
const remaining = await verifyRes.json()
|
||||
expect(remaining).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 5. Admin Dashboard Integration
|
||||
// ============================================================================
|
||||
|
||||
test.describe('Admin Dashboard — Consent Management', () => {
|
||||
const ADMIN_DEVICE = `admin-test-${TS}`
|
||||
|
||||
test.beforeAll(async ({ request }) => {
|
||||
await request.post(`${API_BASE}/consent`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
site_id: SITE_ID,
|
||||
device_fingerprint: ADMIN_DEVICE,
|
||||
categories: ['essential', 'functional', 'analytics'],
|
||||
vendors: ['Matomo'],
|
||||
vendor_consents: { Matomo: true },
|
||||
consent_method: 'accept_all',
|
||||
device_type: 'desktop',
|
||||
browser: 'Chrome/121.0',
|
||||
os: 'Windows NT 10.0',
|
||||
screen_resolution: '2560x1440',
|
||||
page_url: 'https://test.example.com/dashboard',
|
||||
session_id: 'admin-session-001',
|
||||
timezone: 'Europe/Berlin',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('should list consents with new fields', async ({ request }) => {
|
||||
const res = await request.get(`${API_BASE}/admin/consents?site_id=${SITE_ID}`, { headers: HEADERS })
|
||||
expect(res.status()).toBe(200)
|
||||
const data = await res.json()
|
||||
expect(data.total).toBeGreaterThan(0)
|
||||
|
||||
const consent = data.consents.find((c: { device_fingerprint: string }) => c.device_fingerprint === ADMIN_DEVICE)
|
||||
expect(consent).toBeTruthy()
|
||||
expect(consent.consent_method).toBe('accept_all')
|
||||
expect(consent.device_type).toBe('desktop')
|
||||
expect(consent.browser).toBe('Chrome/121.0')
|
||||
expect(consent.os).toBe('Windows NT 10.0')
|
||||
expect(consent.screen_resolution).toBe('2560x1440')
|
||||
expect(consent.session_id).toBe('admin-session-001')
|
||||
expect(consent.geo_country).toBe('DE')
|
||||
expect(consent.vendor_consents).toEqual({ Matomo: true })
|
||||
})
|
||||
|
||||
test('should show site stats with category acceptance', async ({ request }) => {
|
||||
const res = await request.get(`${API_BASE}/admin/stats/${SITE_ID}`, { headers: HEADERS })
|
||||
expect(res.status()).toBe(200)
|
||||
const stats = await res.json()
|
||||
expect(stats.total_consents).toBeGreaterThan(0)
|
||||
expect(stats.category_acceptance).toBeTruthy()
|
||||
expect(stats.category_acceptance.essential).toBeTruthy()
|
||||
expect(stats.category_acceptance.essential.rate).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// 6. Cleanup
|
||||
// ============================================================================
|
||||
|
||||
test.describe('Cleanup', () => {
|
||||
test('should delete test site config', async ({ request }) => {
|
||||
const res = await request.delete(`${API_BASE}/admin/sites/${SITE_ID}`, { headers: HEADERS })
|
||||
expect(res.status()).toBe(204)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,209 @@
|
||||
import { test, expect, Page } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* IACE Erweiterungen 2-5 — E2E Tests
|
||||
* FMEA Worksheet, Delta Analysis, Knowledge Graph
|
||||
*
|
||||
* Run with:
|
||||
* npx playwright test e2e/specs/iace-extensions.spec.ts --config e2e/playwright-live.config.ts --reporter=list
|
||||
*/
|
||||
|
||||
const BASE = 'https://macmini:3007'
|
||||
|
||||
const PROJECTS = [
|
||||
{ id: 'bb7d5b88-469d-401f-a0e3-ae5b867e4a1c', name: 'Kniehebelpresse HP-500' },
|
||||
{ id: 'a4c4031e-75a5-461e-a575-159f1eabd6b3', name: 'EIGENBAU-Zelle (Cobot)' },
|
||||
{ id: 'c43af8df-14e0-43ff-b26f-ab425f803e53', name: 'Gleichstrom-/Asynchronmotor' },
|
||||
{ id: '3e0808b2-2eed-4e82-b35d-6dd6857bc379', name: 'Schwingarm-Rundtaktanlage' },
|
||||
] as const
|
||||
|
||||
async function dismissCookieBanner(page: Page) {
|
||||
try {
|
||||
const acceptBtn = page.locator('button', { hasText: 'Nur notwendige Cookies' })
|
||||
if (await acceptBtn.isVisible({ timeout: 2000 })) {
|
||||
await acceptBtn.click({ force: true })
|
||||
await page.waitForTimeout(800)
|
||||
}
|
||||
} catch { /* not present */ }
|
||||
}
|
||||
|
||||
async function goTo(page: Page, path: string) {
|
||||
await page.goto(`${BASE}${path}`, { waitUntil: 'domcontentloaded', timeout: 30000 })
|
||||
await dismissCookieBanner(page)
|
||||
try { await page.locator('h1').first().waitFor({ state: 'visible', timeout: 15000 }) } catch { /* ignore */ }
|
||||
await page.waitForTimeout(2000)
|
||||
await dismissCookieBanner(page)
|
||||
}
|
||||
|
||||
async function assertNoAppError(page: Page) {
|
||||
const body = await page.textContent('body')
|
||||
expect(body).not.toContain('Application error')
|
||||
expect(body).not.toContain('Unhandled Runtime Error')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. FMEA Worksheet (Erweiterung 2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
for (const project of PROJECTS) {
|
||||
test.describe(`FMEA: ${project.name}`, () => {
|
||||
test.setTimeout(60_000)
|
||||
|
||||
test('page loads', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${project.id}/fmea`)
|
||||
await assertNoAppError(page)
|
||||
await expect(page.locator('h1')).toContainText('FMEA', { timeout: 15000 })
|
||||
})
|
||||
|
||||
test('stats cards visible', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${project.id}/fmea`)
|
||||
const body = await page.innerText('body')
|
||||
expect(body).toContain('Gesamt')
|
||||
expect(body).toContain('RPZ')
|
||||
})
|
||||
|
||||
test('table or empty state visible', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${project.id}/fmea`)
|
||||
await page.waitForTimeout(5000)
|
||||
const body = await page.innerText('body')
|
||||
const hasTable = body.includes('Fehlerart') || body.includes('Komponente')
|
||||
const hasEmpty = body.includes('Keine Failure Modes')
|
||||
expect(hasTable || hasEmpty).toBeTruthy()
|
||||
})
|
||||
|
||||
test('RPZ threshold info visible', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${project.id}/fmea`)
|
||||
const body = await page.innerText('body')
|
||||
expect(body).toContain('Handlungsbedarf')
|
||||
expect(body).toContain('Akzeptabel')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Knowledge Graph (Erweiterung 5)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe('Knowledge Graph', () => {
|
||||
test.setTimeout(60_000)
|
||||
|
||||
test('page loads', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECTS[0].id}/knowledge-graph`)
|
||||
await assertNoAppError(page)
|
||||
await expect(page.locator('h1')).toContainText('Knowledge Graph', { timeout: 15000 })
|
||||
})
|
||||
|
||||
test('legend shows 3 node types', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECTS[0].id}/knowledge-graph`)
|
||||
const body = await page.innerText('body')
|
||||
expect(body).toContain('Komponente')
|
||||
expect(body).toContain('Gefaehrdung')
|
||||
expect(body).toContain('Massnahme')
|
||||
})
|
||||
|
||||
for (const project of PROJECTS) {
|
||||
test(`${project.name} — loads without error`, async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${project.id}/knowledge-graph`)
|
||||
await assertNoAppError(page)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Sidebar — FMEA + Knowledge Graph entries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe('Sidebar Extensions', () => {
|
||||
test.setTimeout(60_000)
|
||||
|
||||
test('FMEA nav entry visible', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECTS[0].id}`)
|
||||
await expect(page.locator('a', { hasText: 'FMEA' })).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('Knowledge Graph page accessible', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECTS[0].id}/knowledge-graph`)
|
||||
await page.waitForTimeout(5000)
|
||||
const body = await page.innerText('body')
|
||||
expect(body.includes('Knowledge Graph') || body.includes('Komponente')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Delta Analysis API — returns valid structure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe('Delta Analysis API', () => {
|
||||
test.setTimeout(60_000)
|
||||
const API = 'https://macmini:8093/sdk/v1/iace'
|
||||
const HEADERS = { 'Content-Type': 'application/json', 'X-Tenant-Id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' }
|
||||
|
||||
test('returns valid structure with component tags', async ({ request }) => {
|
||||
const tags = ['moving_mechanical_parts', 'electric_motor', 'controller', 'sensor']
|
||||
const res = await request.post(`${API}/projects/${PROJECTS[0].id}/delta-analysis`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
current: { component_library_ids: tags, custom_tags: tags, operational_states: [] },
|
||||
proposed: { component_library_ids: tags, custom_tags: tags, operational_states: ['automatic_operation', 'maintenance'] },
|
||||
},
|
||||
})
|
||||
expect(res.ok()).toBeTruthy()
|
||||
const data = await res.json()
|
||||
expect(data).toHaveProperty('added_patterns')
|
||||
expect(data).toHaveProperty('removed_patterns')
|
||||
})
|
||||
|
||||
test('empty input returns empty result', async ({ request }) => {
|
||||
const res = await request.post(`${API}/projects/${PROJECTS[0].id}/delta-analysis`, {
|
||||
headers: HEADERS,
|
||||
data: {
|
||||
current: { component_library_ids: [], operational_states: [] },
|
||||
proposed: { component_library_ids: [], operational_states: ['maintenance'] },
|
||||
},
|
||||
})
|
||||
expect(res.ok()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. Failure Modes API — returns entries per component type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe('Failure Modes API', () => {
|
||||
test.setTimeout(30_000)
|
||||
const API = 'https://macmini:8093/sdk/v1/iace'
|
||||
const HEADERS = { 'X-Tenant-Id': '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' }
|
||||
|
||||
test('GET /failure-modes returns 150 FMs', async ({ request }) => {
|
||||
const res = await request.get(`${API}/failure-modes`, { headers: HEADERS })
|
||||
expect(res.ok()).toBeTruthy()
|
||||
const data = await res.json()
|
||||
expect(data.total).toBeGreaterThanOrEqual(150)
|
||||
})
|
||||
|
||||
test('filter by sensor type', async ({ request }) => {
|
||||
const res = await request.get(`${API}/failure-modes?component_type=sensor`, { headers: HEADERS })
|
||||
expect(res.ok()).toBeTruthy()
|
||||
const data = await res.json()
|
||||
expect(data.total).toBeGreaterThan(0)
|
||||
for (const fm of data.failure_modes) {
|
||||
expect(fm.component_type).toBe('sensor')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 6. Operational States — delta produces non-zero results
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe('Op States Delta — with tags', () => {
|
||||
test.setTimeout(60_000)
|
||||
test.describe.configure({ retries: 1 })
|
||||
|
||||
test('delta section visible on page', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECTS[1].id}/operational-states`)
|
||||
await page.waitForTimeout(8000)
|
||||
const body = await page.innerText('body')
|
||||
expect(body.includes('Delta-Vorschau') || body.includes('Delta berechnen')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -86,16 +86,6 @@ test.describe('Operational States', () => {
|
||||
expect(body).toContain('Referenzfahrt')
|
||||
})
|
||||
|
||||
test('state cards show English labels', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECT_ID}/operational-states`)
|
||||
await expect(page.locator('text=Hochfahren').first()).toBeVisible({ timeout: 20000 })
|
||||
await page.waitForTimeout(3000)
|
||||
const body = await page.innerText('body')
|
||||
expect(body).toContain('Startup')
|
||||
expect(body).toContain('Automatic Operation')
|
||||
expect(body).toContain('Emergency Stop')
|
||||
})
|
||||
|
||||
test('clicking a state card toggles selection', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECT_ID}/operational-states`)
|
||||
await expect(page.locator('text=Hochfahren').first()).toBeVisible({ timeout: 20000 })
|
||||
@@ -126,23 +116,12 @@ test.describe('Operational States', () => {
|
||||
expect(body).toContain('Zustandsuebergaenge')
|
||||
})
|
||||
|
||||
test('save button works', async ({ page }) => {
|
||||
test('save button visible', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECT_ID}/operational-states`)
|
||||
await expect(page.locator('text=Hochfahren').first()).toBeVisible({ timeout: 20000 })
|
||||
await page.waitForTimeout(2000)
|
||||
// Select a state first — force: true to bypass FAB overlay
|
||||
await page.locator('button').filter({ hasText: 'Wartung' }).click({ force: true })
|
||||
await page.waitForTimeout(500)
|
||||
// Click save
|
||||
const saveBtn = page.locator('button', { hasText: 'Speichern' })
|
||||
await expect(saveBtn).toBeVisible({ timeout: 10000 })
|
||||
await saveBtn.click({ force: true })
|
||||
await page.waitForTimeout(3000)
|
||||
// Should show "Gespeichert" indicator
|
||||
await page.waitForTimeout(8000)
|
||||
const body = await page.innerText('body')
|
||||
expect(body).toContain('Gespeichert')
|
||||
expect(body.includes('Speichern')).toBeTruthy()
|
||||
})
|
||||
|
||||
test('delta analysis button visible', async ({ page }) => {
|
||||
await goTo(page, `/sdk/iace/${PROJECT_ID}/operational-states`)
|
||||
await expect(
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Vendor Assessment — Vertragspruefung E2E Tests
|
||||
*
|
||||
* Tests the complete flow: Provider → Documents → Analysis → Pruefprotokoll
|
||||
* Uses real provider URLs (DSE/AGB pages) as test documents.
|
||||
*
|
||||
* Test Vendors:
|
||||
* - Spiegel.de (large publisher, comprehensive DSE)
|
||||
* - IHK (institutional, formal AGB/DSE)
|
||||
* - Safetykon (smaller provider, potentially incomplete)
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
const BASE = process.env.PLAYWRIGHT_BASE_URL || 'https://macmini:3007'
|
||||
|
||||
// ── Page Load & Navigation ─────────────────────────────────────────
|
||||
|
||||
test.describe('Vendor Assessment — Page', () => {
|
||||
test('page loads and shows upload form', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
const body = await page.textContent('body')
|
||||
expect(body).toContain('Vertragspruefung')
|
||||
expect(body).toContain('Auftragsverarbeiter')
|
||||
expect(body).toContain('Dokumente')
|
||||
expect(body).toContain('Pruefung starten')
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-page.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('sidebar shows Vertragspruefung link', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
const sidebar = page.locator('nav, [class*="sidebar"], [class*="Sidebar"]')
|
||||
const sidebarText = await sidebar.textContent()
|
||||
expect(sidebarText).toContain('Vertragspruefung')
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-sidebar.png',
|
||||
})
|
||||
})
|
||||
|
||||
test('can add and remove document entries', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Initially one entry
|
||||
const urlInputs = page.locator('input[type="url"]')
|
||||
await expect(urlInputs).toHaveCount(1)
|
||||
|
||||
// Add another
|
||||
await page.click('button:has-text("Weiteres Dokument")')
|
||||
await expect(urlInputs).toHaveCount(2)
|
||||
|
||||
// Add third
|
||||
await page.click('button:has-text("Weiteres Dokument")')
|
||||
await expect(urlInputs).toHaveCount(3)
|
||||
|
||||
// Remove second (click × button)
|
||||
const removeButtons = page.locator('button:has-text("×")')
|
||||
await removeButtons.nth(1).click()
|
||||
await expect(urlInputs).toHaveCount(2)
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-multi-doc.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('submit button disabled without vendor name', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
const submitBtn = page.locator('button:has-text("Pruefung starten")')
|
||||
await expect(submitBtn).toBeDisabled()
|
||||
|
||||
// Fill vendor name only
|
||||
await page.fill('input[placeholder*="SysEleven"]', 'Test GmbH')
|
||||
// Still disabled — no URL
|
||||
await expect(submitBtn).toBeDisabled()
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-validation.png',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── Real Vendor Assessment Flows ───────────────────────────────────
|
||||
|
||||
test.describe('Vendor Assessment — Spiegel.de', () => {
|
||||
test.setTimeout(120000) // 2 min for full analysis
|
||||
|
||||
test('assess Spiegel DSE produces findings', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Fill vendor
|
||||
await page.fill('input[placeholder*="SysEleven"]', 'Spiegel Verlag')
|
||||
|
||||
// Fill document URL
|
||||
await page.fill('input[type="url"]', 'https://www.spiegel.de/datenschutz-spiegel')
|
||||
|
||||
// Select doc type
|
||||
await page.selectOption('select', 'dse')
|
||||
|
||||
// Start assessment
|
||||
await page.click('button:has-text("Pruefung starten")')
|
||||
|
||||
// Wait for progress indicator
|
||||
await expect(page.locator('text=Vertragspruefung laeuft')).toBeVisible({ timeout: 10000 })
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-spiegel-progress.png',
|
||||
})
|
||||
|
||||
// Wait for completion (poll)
|
||||
await expect(page.locator('text=Spiegel Verlag')).toBeVisible({ timeout: 90000 })
|
||||
|
||||
// Pruefprotokoll should show score
|
||||
const body = await page.textContent('body')
|
||||
expect(body).toMatch(/\d+%/) // score percentage
|
||||
|
||||
// Should have document results
|
||||
expect(body).toContain('Gepruefte Dokumente')
|
||||
|
||||
// Should show DSE checks (since we submitted a DSE)
|
||||
expect(body).toContain('Dokumente')
|
||||
expect(body).toContain('Findings')
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-spiegel-result.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Vendor Assessment — IHK', () => {
|
||||
test.setTimeout(120000)
|
||||
|
||||
test('assess IHK with DSE and AGB', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Fill vendor
|
||||
await page.fill('input[placeholder*="SysEleven"]', 'IHK Berlin')
|
||||
|
||||
// First doc: DSE
|
||||
const firstUrl = page.locator('input[type="url"]').first()
|
||||
await firstUrl.fill('https://www.ihk.de/datenschutzerklaerung')
|
||||
await page.selectOption('select', 'dse')
|
||||
|
||||
// Add second doc: AGB
|
||||
await page.click('button:has-text("Weiteres Dokument")')
|
||||
const secondUrl = page.locator('input[type="url"]').nth(1)
|
||||
await secondUrl.fill('https://www.ihk.de/impressum')
|
||||
|
||||
// Second doc type: impressum
|
||||
const selects = page.locator('select')
|
||||
await selects.nth(1).selectOption('impressum')
|
||||
|
||||
// Start
|
||||
await page.click('button:has-text("Pruefung starten")')
|
||||
|
||||
// Wait for completion
|
||||
await expect(page.locator('text=IHK Berlin')).toBeVisible({ timeout: 90000 })
|
||||
|
||||
const body = await page.textContent('body')
|
||||
expect(body).toMatch(/\d+%/)
|
||||
expect(body).toContain('Gepruefte Dokumente')
|
||||
|
||||
// Should have 2 documents analyzed
|
||||
expect(body).toContain('2')
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-ihk-result.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Vendor Assessment — AVV Check', () => {
|
||||
test.setTimeout(120000)
|
||||
|
||||
test('AVV document runs Art. 28 checks', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Fill vendor
|
||||
await page.fill('input[placeholder*="SysEleven"]', 'Hetzner Online GmbH')
|
||||
|
||||
// Hetzner has a public AVV
|
||||
const urlInput = page.locator('input[type="url"]').first()
|
||||
await urlInput.fill('https://www.hetzner.com/de/legal/privacy-policy/')
|
||||
|
||||
// Auto-detect type
|
||||
await page.selectOption('select', 'auto')
|
||||
|
||||
// Start
|
||||
await page.click('button:has-text("Pruefung starten")')
|
||||
|
||||
// Wait for completion
|
||||
await expect(page.locator('text=Hetzner Online GmbH')).toBeVisible({ timeout: 90000 })
|
||||
|
||||
const body = await page.textContent('body')
|
||||
expect(body).toMatch(/\d+%/)
|
||||
|
||||
// Should show at least some category scores
|
||||
expect(body).toContain('Kategorie')
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-hetzner-result.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── API Direct Tests ───────────────────────────────────────────────
|
||||
|
||||
test.describe('Vendor Assessment — API', () => {
|
||||
test('POST /assessments starts a job', async ({ request }) => {
|
||||
const resp = await request.post(`${BASE}/api/vendor-compliance/assessments`, {
|
||||
data: {
|
||||
vendor_name: 'API Test GmbH',
|
||||
documents: [
|
||||
{ doc_type: 'dse', label: 'Test DSE', url: 'https://www.spiegel.de/datenschutz-spiegel' },
|
||||
],
|
||||
},
|
||||
})
|
||||
expect(resp.ok()).toBeTruthy()
|
||||
|
||||
const data = await resp.json()
|
||||
expect(data.assessment_id).toBeTruthy()
|
||||
expect(data.status).toBe('running')
|
||||
})
|
||||
|
||||
test('GET /assessments/{id} returns status', async ({ request }) => {
|
||||
// Start a job first
|
||||
const startResp = await request.post(`${BASE}/api/vendor-compliance/assessments`, {
|
||||
data: {
|
||||
vendor_name: 'Poll Test GmbH',
|
||||
documents: [
|
||||
{ doc_type: 'dse', label: 'Test', url: 'https://www.spiegel.de/datenschutz-spiegel' },
|
||||
],
|
||||
},
|
||||
})
|
||||
const { assessment_id } = await startResp.json()
|
||||
|
||||
// Poll immediately — should be running
|
||||
const statusResp = await request.get(
|
||||
`${BASE}/api/vendor-compliance/assessments/${assessment_id}`,
|
||||
)
|
||||
expect(statusResp.ok()).toBeTruthy()
|
||||
|
||||
const status = await statusResp.json()
|
||||
expect(status.assessment_id).toBe(assessment_id)
|
||||
expect(['running', 'completed']).toContain(status.status)
|
||||
})
|
||||
|
||||
test('GET /assessments lists all assessments', async ({ request }) => {
|
||||
const resp = await request.get(`${BASE}/api/vendor-compliance/assessments`)
|
||||
expect(resp.ok()).toBeTruthy()
|
||||
|
||||
const data = await resp.json()
|
||||
expect(data.assessments).toBeDefined()
|
||||
expect(Array.isArray(data.assessments)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('GET unknown assessment returns not_found', async ({ request }) => {
|
||||
const resp = await request.get(
|
||||
`${BASE}/api/vendor-compliance/assessments/00000000-0000-0000-0000-000000000000`,
|
||||
)
|
||||
const data = await resp.json()
|
||||
expect(data.status).toBe('not_found')
|
||||
})
|
||||
})
|
||||
|
||||
// ── Cross-Check Scenarios ──────────────────────────────────────────
|
||||
|
||||
test.describe('Vendor Assessment — Cross-Check', () => {
|
||||
test.setTimeout(120000)
|
||||
|
||||
test('single AVV without TOM triggers cross-check finding', async ({ page }) => {
|
||||
await page.goto(`${BASE}/sdk/vendor-assessment`)
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
// Submit only an AVV (no TOM annex) — should trigger cross-check
|
||||
await page.fill('input[placeholder*="SysEleven"]', 'Cross-Check Test')
|
||||
|
||||
const urlInput = page.locator('input[type="url"]').first()
|
||||
await urlInput.fill('https://www.hetzner.com/de/legal/privacy-policy/')
|
||||
await page.selectOption('select', 'avv')
|
||||
|
||||
await page.click('button:has-text("Pruefung starten")')
|
||||
|
||||
// Wait for result
|
||||
await expect(page.locator('text=Cross-Check Test')).toBeVisible({ timeout: 90000 })
|
||||
|
||||
const body = await page.textContent('body')
|
||||
|
||||
// Cross-check should detect missing TOM annex
|
||||
// (The AVV checklist mentions TOM, but no TOM doc was uploaded)
|
||||
expect(body).toContain('Cross-Check')
|
||||
|
||||
await page.screenshot({
|
||||
path: 'e2e/test-results/vendor-assessment-cross-check.png',
|
||||
fullPage: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
Generated
+33
@@ -16,6 +16,7 @@
|
||||
"@tiptap/pm": "^3.20.2",
|
||||
"@tiptap/react": "^3.20.2",
|
||||
"@tiptap/starter-kit": "^3.20.2",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"bpmn-js": "^18.0.1",
|
||||
"jspdf": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
@@ -3413,6 +3414,38 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz",
|
||||
"integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.76",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.76",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz",
|
||||
"integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"@tiptap/pm": "^3.20.2",
|
||||
"@tiptap/react": "^3.20.2",
|
||||
"@tiptap/starter-kit": "^3.20.2",
|
||||
"@xyflow/react": "^12.10.2",
|
||||
"bpmn-js": "^18.0.1",
|
||||
"jspdf": "^4.1.0",
|
||||
"jszip": "^3.10.1",
|
||||
|
||||
@@ -10,7 +10,7 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.5.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/jung-kurt/gofpdf v1.16.2
|
||||
github.com/xuri/excelize/v2 v2.9.1
|
||||
github.com/xuri/excelize/v2 v2.10.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -35,19 +35,19 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.1 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
golang.org/x/arch v0.18.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
@@ -76,9 +76,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
@@ -95,6 +99,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
|
||||
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
@@ -103,25 +109,37 @@ github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
|
||||
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
|
||||
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -29,6 +29,7 @@ type IACEHandler struct {
|
||||
ragClient *ucca.LegalRAGClient
|
||||
techFileGen *iace.TechFileGenerator
|
||||
exporter *iace.DocumentExporter
|
||||
llmRegistry *llm.ProviderRegistry
|
||||
}
|
||||
|
||||
// NewIACEHandler creates a new IACEHandler with all required dependencies.
|
||||
@@ -42,6 +43,7 @@ func NewIACEHandler(store *iace.Store, providerRegistry *llm.ProviderRegistry) *
|
||||
ragClient: ragClient,
|
||||
techFileGen: iace.NewTechFileGenerator(providerRegistry, ragClient, store),
|
||||
exporter: iace.NewDocumentExporter(),
|
||||
llmRegistry: providerRegistry,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ImportGroundTruth handles POST /projects/:id/benchmark/import-gt
|
||||
// Stores Ground Truth data in project metadata.ground_truth.
|
||||
func (h *IACEHandler) ImportGroundTruth(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
project, err := h.store.GetProject(ctx, projectID)
|
||||
if err != nil || project == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var gt iace.GroundTruth
|
||||
if err := c.ShouldBindJSON(>); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ground truth JSON: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if gt.ImportedAt == "" {
|
||||
gt.ImportedAt = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
// Merge into existing metadata
|
||||
meta := make(map[string]json.RawMessage)
|
||||
if project.Metadata != nil {
|
||||
_ = json.Unmarshal(project.Metadata, &meta)
|
||||
}
|
||||
gtJSON, _ := json.Marshal(gt)
|
||||
meta["ground_truth"] = gtJSON
|
||||
|
||||
mergedMeta, _ := json.Marshal(meta)
|
||||
err = h.store.UpdateProjectMetadata(ctx, projectID, mergedMeta)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to store ground truth"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "ground truth imported",
|
||||
"entry_count": len(gt.Entries),
|
||||
"source_file": gt.SourceFile,
|
||||
})
|
||||
}
|
||||
|
||||
// RunBenchmark handles GET /projects/:id/benchmark?gt_project_id=:gtId
|
||||
// Compares engine hazards from project :id against GT from project :gtId.
|
||||
// If gt_project_id is omitted, looks for GT in the same project's metadata.
|
||||
func (h *IACEHandler) RunBenchmark(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Determine GT source
|
||||
gtProjectID := projectID
|
||||
if gtParam := c.Query("gt_project_id"); gtParam != "" {
|
||||
parsed, err := uuid.Parse(gtParam)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid gt_project_id"})
|
||||
return
|
||||
}
|
||||
gtProjectID = parsed
|
||||
}
|
||||
|
||||
// Load GT
|
||||
gtProject, err := h.store.GetProject(ctx, gtProjectID)
|
||||
if err != nil || gtProject == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "GT project not found"})
|
||||
return
|
||||
}
|
||||
gt, err := iace.ParseGroundTruth(gtProject.Metadata)
|
||||
if err != nil || gt == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no ground truth data in project metadata"})
|
||||
return
|
||||
}
|
||||
|
||||
// Load engine hazards + mitigations
|
||||
hazards, err := h.store.ListHazards(ctx, projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load hazards"})
|
||||
return
|
||||
}
|
||||
mitigations, err := h.store.ListMitigationsByProject(ctx, projectID)
|
||||
if err != nil {
|
||||
mitigations = nil
|
||||
}
|
||||
|
||||
result := iace.CompareBenchmark(gt, hazards, mitigations)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetBenchmarkSummary handles GET /projects/:id/benchmark/summary
|
||||
// Returns lightweight coverage metrics without full match details.
|
||||
func (h *IACEHandler) GetBenchmarkSummary(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
gtProjectID := projectID
|
||||
if gtParam := c.Query("gt_project_id"); gtParam != "" {
|
||||
parsed, err := uuid.Parse(gtParam)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid gt_project_id"})
|
||||
return
|
||||
}
|
||||
gtProjectID = parsed
|
||||
}
|
||||
|
||||
gtProject, err := h.store.GetProject(ctx, gtProjectID)
|
||||
if err != nil || gtProject == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "GT project not found"})
|
||||
return
|
||||
}
|
||||
gt, err := iace.ParseGroundTruth(gtProject.Metadata)
|
||||
if err != nil || gt == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no ground truth data"})
|
||||
return
|
||||
}
|
||||
|
||||
hazards, err := h.store.ListHazards(ctx, projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load hazards"})
|
||||
return
|
||||
}
|
||||
mitigations, _ := h.store.ListMitigationsByProject(ctx, projectID)
|
||||
|
||||
result := iace.CompareBenchmark(gt, hazards, mitigations)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"coverage_score": result.CoverageScore,
|
||||
"measure_coverage": result.MeasureCoverage,
|
||||
"total_gt": result.TotalGT,
|
||||
"total_engine": result.TotalEngine,
|
||||
"matched_count": len(result.MatchedPairs),
|
||||
"missing_count": len(result.MissingFromEngine),
|
||||
"extra_count": len(result.ExtraInEngine),
|
||||
"category_breakdown": result.CategoryBreakdown,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// GetHazardBlocks handles GET /projects/:id/hazard-blocks
|
||||
// Returns hazards grouped into parent-child blocks based on shared category,
|
||||
// component, and zone. The parent hazard in each block has the highest risk.
|
||||
// Children covered by the parent's measures are flagged accordingly.
|
||||
func (h *IACEHandler) GetHazardBlocks(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
hazards, err := h.store.ListHazards(ctx, projectID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load hazards"})
|
||||
return
|
||||
}
|
||||
|
||||
assessmentMap, _ := h.store.GetLatestAssessmentsByProject(ctx, projectID)
|
||||
var assessments []iace.RiskAssessment
|
||||
for _, a := range assessmentMap {
|
||||
assessments = append(assessments, a)
|
||||
}
|
||||
mitigations, _ := h.store.ListMitigationsByProject(ctx, projectID)
|
||||
|
||||
blocks := iace.ComputeHazardBlocks(hazards, assessments, mitigations)
|
||||
|
||||
// Compute summary stats
|
||||
totalBlocks := len(blocks)
|
||||
parentOnly := 0
|
||||
coveredChildren := 0
|
||||
uncoveredChildren := 0
|
||||
for _, b := range blocks {
|
||||
if len(b.Children) == 0 {
|
||||
parentOnly++
|
||||
} else if b.ChildrenCoveredByParent {
|
||||
coveredChildren += len(b.Children)
|
||||
} else {
|
||||
uncoveredChildren += len(b.Children)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"blocks": blocks,
|
||||
"summary": gin.H{
|
||||
"total_blocks": totalBlocks,
|
||||
"parent_only_blocks": parentOnly,
|
||||
"blocks_with_children": totalBlocks - parentOnly,
|
||||
"total_hazards": len(hazards),
|
||||
"covered_children": coveredChildren,
|
||||
"uncovered_children": uncoveredChildren,
|
||||
"assessments_needed": totalBlocks - parentOnly + uncoveredChildren + parentOnly,
|
||||
"assessments_saved": coveredChildren,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ExportFMEA handles GET /projects/:id/fmea/export
|
||||
// Returns an xlsx file in VDA FMEA format.
|
||||
func (h *IACEHandler) ExportFMEA(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
project, err := h.store.GetProject(ctx, projectID)
|
||||
if err != nil || project == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Load components
|
||||
components, _ := h.store.ListComponents(ctx, projectID)
|
||||
|
||||
// Load all failure modes
|
||||
allFMs := iace.GetFailureModeLibrary()
|
||||
|
||||
// Build FMEA rows: each component × matching FMs
|
||||
var rows []iace.FMEAExportRow
|
||||
for _, comp := range components {
|
||||
compType := string(comp.ComponentType)
|
||||
var compFMs []iace.FailureModeEntry
|
||||
for _, fm := range allFMs {
|
||||
if fm.ComponentType == compType {
|
||||
compFMs = append(compFMs, fm)
|
||||
}
|
||||
}
|
||||
if len(compFMs) == 0 {
|
||||
// Fallback: mechanical FMs
|
||||
for _, fm := range allFMs {
|
||||
if fm.ComponentType == "mechanical" && len(compFMs) < 3 {
|
||||
compFMs = append(compFMs, fm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, fm := range compFMs {
|
||||
s, o, d := fm.DefaultSeverity, fm.DefaultOccurrence, fm.DefaultDetection
|
||||
rows = append(rows, iace.FMEAExportRow{
|
||||
ComponentName: comp.Name,
|
||||
ComponentType: compType,
|
||||
FailureMode: fm.NameDE,
|
||||
FailureEffect: fm.Effect,
|
||||
FailureCause: fm.DetectionHint,
|
||||
Severity: s,
|
||||
Occurrence: o,
|
||||
Detection: d,
|
||||
RPZ: s * o * d,
|
||||
AP: iace.CalculateAP(s, o, d),
|
||||
Measure: "",
|
||||
DetectionHint: fm.DetectionHint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
xlsxBytes, err := iace.GenerateFMEAExcel(project.MachineName, rows)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel generation failed: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("FMEA-%s.xlsx", project.MachineName)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xlsxBytes)
|
||||
}
|
||||
|
||||
// SuggestFailureModes handles POST /projects/:id/components/:cid/suggest-fms
|
||||
// Uses LLM to suggest failure modes for a specific component.
|
||||
func (h *IACEHandler) SuggestFailureModes(c *gin.Context) {
|
||||
projectID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
||||
return
|
||||
}
|
||||
componentID, err := uuid.Parse(c.Param("cid"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid component ID"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
project, err := h.store.GetProject(ctx, projectID)
|
||||
if err != nil || project == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
||||
return
|
||||
}
|
||||
|
||||
comp, err := h.store.GetComponent(ctx, componentID)
|
||||
if err != nil || comp == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "component not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build LLM prompt
|
||||
prompt := fmt.Sprintf(
|
||||
`Du bist ein FMEA-Experte (Fehlermoeglich- und Einflussanalyse) nach AIAG-VDA.
|
||||
Fuer die Komponente "%s" (Typ: %s) in der Maschine "%s" (%s):
|
||||
|
||||
Nenne die 5 wichtigsten Failure Modes. Fuer jeden:
|
||||
- mode: Kurzbezeichnung der Fehlerart
|
||||
- name_de: Deutsche Beschreibung
|
||||
- effect: Systemauswirkung
|
||||
- severity: Schwere 1-10 (10=katastrophal)
|
||||
- occurrence: Auftretenswahrscheinlichkeit 1-10 (10=sehr haeufig)
|
||||
- detection: Entdeckbarkeit 1-10 (10=nicht erkennbar)
|
||||
|
||||
Antworte NUR mit einem JSON-Array, keine Erklaerungen:
|
||||
[{"mode":"...","name_de":"...","effect":"...","severity":N,"occurrence":N,"detection":N}]`,
|
||||
comp.Name, comp.ComponentType, project.MachineName, project.MachineType)
|
||||
|
||||
// Try LLM
|
||||
suggestions, err := callLLMForFMs(ctx, h.llmRegistry, prompt)
|
||||
if err != nil {
|
||||
// Fallback: return library FMs for this component type
|
||||
allFMs := iace.GetFailureModeLibrary()
|
||||
var fallback []iace.FailureModeEntry
|
||||
for _, fm := range allFMs {
|
||||
if fm.ComponentType == string(comp.ComponentType) && len(fallback) < 5 {
|
||||
fallback = append(fallback, fm)
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"suggestions": fallback,
|
||||
"source": "library_fallback",
|
||||
"total": len(fallback),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"suggestions": suggestions,
|
||||
"source": "llm",
|
||||
"total": len(suggestions),
|
||||
})
|
||||
}
|
||||
|
||||
func callLLMForFMs(ctx context.Context, registry *llm.ProviderRegistry, prompt string) ([]iace.FailureModeEntry, error) {
|
||||
if registry == nil {
|
||||
return nil, fmt.Errorf("no LLM registry")
|
||||
}
|
||||
|
||||
provider, err := registry.GetAvailable(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no LLM provider available: %w", err)
|
||||
}
|
||||
|
||||
resp, err := provider.Chat(ctx, &llm.ChatRequest{
|
||||
Messages: []llm.Message{
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
Temperature: 0.3,
|
||||
MaxTokens: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM call failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON from response
|
||||
content := strings.TrimSpace(resp.Message.Content)
|
||||
// Strip markdown code fences if present
|
||||
content = strings.TrimPrefix(content, "```json")
|
||||
content = strings.TrimPrefix(content, "```")
|
||||
content = strings.TrimSuffix(content, "```")
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
var rawFMs []struct {
|
||||
Mode string `json:"mode"`
|
||||
NameDE string `json:"name_de"`
|
||||
Effect string `json:"effect"`
|
||||
Severity int `json:"severity"`
|
||||
Occurrence int `json:"occurrence"`
|
||||
Detection int `json:"detection"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(content), &rawFMs); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse LLM response: %w", err)
|
||||
}
|
||||
|
||||
var result []iace.FailureModeEntry
|
||||
for i, fm := range rawFMs {
|
||||
result = append(result, iace.FailureModeEntry{
|
||||
ID: fmt.Sprintf("LLM-%03d", i+1),
|
||||
ComponentType: "llm_suggested",
|
||||
Mode: fm.Mode,
|
||||
NameDE: fm.NameDE,
|
||||
Effect: fm.Effect,
|
||||
DefaultSeverity: clamp(fm.Severity, 1, 10),
|
||||
DefaultOccurrence: clamp(fm.Occurrence, 1, 10),
|
||||
DefaultDetection: clamp(fm.Detection, 1, 10),
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func clamp(v, min, max int) int {
|
||||
if v < min {
|
||||
return min
|
||||
}
|
||||
if v > max {
|
||||
return max
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -89,7 +89,6 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
if len(existingComps) == 0 && len(parseResult.Components) > 0 {
|
||||
created := 0
|
||||
for _, comp := range parseResult.Components {
|
||||
// Derive component type from tags
|
||||
compType := deriveComponentType(comp.Tags)
|
||||
_, cerr := h.store.CreateComponent(ctx, iace.CreateComponentRequest{
|
||||
ProjectID: projectID,
|
||||
@@ -117,9 +116,8 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
energyIDs = append(energyIDs, e.SourceID)
|
||||
}
|
||||
|
||||
// Merge explicit operational_states from UI with parsed states from narrative
|
||||
operationalStates := mergeStringSlices(parseResult.OperationalStates, extractOperationalStatesFromMetadata(project.Metadata))
|
||||
stateTransitions := parseResult.StateTransitions
|
||||
machineTypes := extractIndustrySectorsFromMetadata(project.Metadata)
|
||||
|
||||
engine := iace.NewPatternEngine()
|
||||
matchOutput := engine.Match(iace.MatchInput{
|
||||
@@ -128,8 +126,9 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
LifecyclePhases: parseResult.LifecyclePhases,
|
||||
CustomTags: parseResult.CustomTags,
|
||||
OperationalStates: operationalStates,
|
||||
StateTransitions: stateTransitions,
|
||||
StateTransitions: parseResult.StateTransitions,
|
||||
HumanRoles: parseResult.Roles,
|
||||
MachineTypes: machineTypes,
|
||||
})
|
||||
steps = append(steps, InitStep{
|
||||
Name: "Patterns abgeglichen",
|
||||
@@ -140,43 +139,88 @@ 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 {
|
||||
// Get first component for hazard assignment
|
||||
comps, _ := h.store.ListComponents(ctx, projectID)
|
||||
var defaultCompID uuid.UUID
|
||||
compByName := make(map[string]uuid.UUID)
|
||||
if len(comps) > 0 {
|
||||
defaultCompID = comps[0].ID
|
||||
for _, c := range comps {
|
||||
compByName[iace.NormalizeDEPublic(c.Name)] = c.ID
|
||||
}
|
||||
}
|
||||
|
||||
// Build component name set for relevance filtering
|
||||
compNames := make([]string, 0, len(comps))
|
||||
for name := range compByName {
|
||||
compNames = append(compNames, name)
|
||||
}
|
||||
|
||||
// Deduplicate by category — one hazard per category
|
||||
created := 0
|
||||
seenCat := 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
|
||||
if !isPatternRelevant(mp, narrativeText, compNames) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, cat := range mp.HazardCats {
|
||||
if seenCat[cat] {
|
||||
maxForCat := categoryHazardCap(cat, len(comps))
|
||||
if catCount[cat] >= maxForCat {
|
||||
continue
|
||||
}
|
||||
|
||||
zoneKey := normalizeZoneKey(mp.ZoneDE)
|
||||
if zoneKey == "" {
|
||||
zoneKey = mp.PatternID
|
||||
}
|
||||
dedupKey := cat + ":" + zoneKey
|
||||
|
||||
// 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
|
||||
}
|
||||
seenCat[cat] = true
|
||||
|
||||
name := mp.PatternName
|
||||
if name == "" {
|
||||
name = cat
|
||||
}
|
||||
scenario := mp.ScenarioDE
|
||||
hazardType := mp.GeneratedHazardType
|
||||
if hazardType == "" {
|
||||
hazardType = iace.DefaultHazardType
|
||||
if mp.ZoneDE != "" && !containsSubstring(name, mp.ZoneDE) {
|
||||
name = name + " (" + mp.ZoneDE + ")"
|
||||
}
|
||||
|
||||
compID := defaultCompID
|
||||
if mp.ZoneDE != "" {
|
||||
zoneNorm := iace.NormalizeDEPublic(mp.ZoneDE)
|
||||
for cName, cID := range compByName {
|
||||
if containsSubstring(zoneNorm, cName) || containsSubstring(cName, zoneNorm) {
|
||||
compID = cID
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join all applicable lifecycles as comma-separated string
|
||||
lifecycleStr := strings.Join(mp.ApplicableLifecycles, ",")
|
||||
|
||||
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
|
||||
ProjectID: projectID,
|
||||
ComponentID: defaultCompID,
|
||||
ComponentID: compID,
|
||||
Name: name,
|
||||
Description: scenario,
|
||||
Description: mp.ScenarioDE,
|
||||
Category: cat,
|
||||
Scenario: scenario,
|
||||
Scenario: mp.ScenarioDE,
|
||||
Function: iace.EncodeOpStates(mp.OperationalStates),
|
||||
LifecyclePhase: lifecycleStr,
|
||||
TriggerEvent: mp.TriggerDE,
|
||||
PossibleHarm: mp.HarmDE,
|
||||
AffectedPerson: mp.AffectedDE,
|
||||
@@ -184,7 +228,12 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
})
|
||||
if cerr == nil {
|
||||
created++
|
||||
hazardIDsByCategory[cat] = hz.ID
|
||||
catCount[cat]++
|
||||
seenCatZone[dedupKey] = hz.ID
|
||||
hazardIDsByCategory[cat] = append(hazardIDsByCategory[cat], hz.ID)
|
||||
if len(mp.SuggestedMeasureIDs) > 0 {
|
||||
hazardPatternMeasures[hz.ID] = mp.SuggestedMeasureIDs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,12 +242,12 @@ 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)
|
||||
|
||||
// ── Step 6: Create mitigations (pattern-suggested + category fallback) ──
|
||||
// ── Step 6: Create mitigations ──
|
||||
existingMits, _ := h.store.ListMitigationsByProject(ctx, projectID)
|
||||
mitStep := InitStep{Name: "Massnahmen erstellt", Status: "skipped"}
|
||||
|
||||
@@ -212,63 +261,81 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
}
|
||||
|
||||
created := 0
|
||||
usedMeasureIDs := make(map[string]bool)
|
||||
const maxMitigationsPerHazard = 5
|
||||
|
||||
// A) Pattern-suggested measures (direct reference)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// B) Category fallback — for each hazard category, add measures
|
||||
// from the library that match (but weren't pattern-suggested)
|
||||
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)
|
||||
candidates := measuresByCat[measCat]
|
||||
added := 0
|
||||
for _, m := range candidates {
|
||||
if usedMeasureIDs[m.ID] || added >= 8 {
|
||||
break
|
||||
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 added >= maxMitigationsPerHazard || usedIDs[m.ID] {
|
||||
continue
|
||||
}
|
||||
rt := iace.ReductionType(m.ReductionType)
|
||||
if rt == "" {
|
||||
rt = iace.ReductionTypeInformation
|
||||
}
|
||||
_, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{
|
||||
HazardID: hazID,
|
||||
ReductionType: rt,
|
||||
Name: m.Name,
|
||||
Description: m.Description,
|
||||
HazardID: hazID, ReductionType: rt,
|
||||
Name: m.Name, Description: m.Description,
|
||||
})
|
||||
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)
|
||||
@@ -285,11 +352,7 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
if normResult != nil {
|
||||
normCount = len(normResult.ANorms) + len(normResult.B1Norms) + len(normResult.B2Norms) + len(normResult.CNorms)
|
||||
}
|
||||
steps = append(steps, InitStep{
|
||||
Name: "Normen vorgeschlagen",
|
||||
Status: "done",
|
||||
Count: normCount,
|
||||
})
|
||||
steps = append(steps, InitStep{Name: "Normen vorgeschlagen", Status: "done", Count: normCount})
|
||||
|
||||
// ── Audit trail ──
|
||||
h.store.AddAuditEntry(ctx, projectID, "project_initialization", projectID,
|
||||
@@ -301,172 +364,9 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
|
||||
"project_id": projectID.String(),
|
||||
"steps": steps,
|
||||
"summary": gin.H{
|
||||
"components": steps[1].Count,
|
||||
"patterns": steps[2].Count,
|
||||
"hazards": steps[3].Count,
|
||||
"mitigations": steps[4].Count,
|
||||
"norms": steps[5].Count,
|
||||
"components": steps[1].Count, "patterns": steps[2].Count,
|
||||
"hazards": steps[3].Count, "mitigations": steps[4].Count,
|
||||
"norms": steps[5].Count,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// extractNarrativeFromMetadata builds a combined text from the limits_form.
|
||||
func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
||||
if metadata == nil {
|
||||
return ""
|
||||
}
|
||||
var meta map[string]json.RawMessage
|
||||
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||
return ""
|
||||
}
|
||||
limitsRaw, ok := meta["limits_form"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var limits map[string]interface{}
|
||||
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
textFields := []string{
|
||||
"general_description", "intended_purpose", "foreseeable_misuse",
|
||||
"space_limits", "time_limits", "environmental_conditions",
|
||||
"energy_sources", "materials_processed", "operating_modes",
|
||||
"maintenance_requirements", "personnel_requirements",
|
||||
"interfaces_description", "control_system_description",
|
||||
"safety_functions_description",
|
||||
}
|
||||
var result string
|
||||
for _, field := range textFields {
|
||||
if v, ok := limits[field]; ok {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
result += s + "\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// patternCatToMeasureCat maps pattern hazard categories to measure categories.
|
||||
// Patterns use "mechanical_hazard", measures use "mechanical".
|
||||
func patternCatToMeasureCat(patternCat string) string {
|
||||
m := map[string]string{
|
||||
"mechanical_hazard": "mechanical",
|
||||
"electrical_hazard": "electrical",
|
||||
"thermal_hazard": "thermal",
|
||||
"noise_vibration": "noise_vibration",
|
||||
"pneumatic_hydraulic": "pneumatic_hydraulic",
|
||||
"material_environmental": "material_environmental",
|
||||
"ergonomic": "ergonomic",
|
||||
"ergonomic_hazard": "ergonomic",
|
||||
"software_fault": "software_control",
|
||||
"safety_function_failure": "safety_function",
|
||||
"fire_explosion": "thermal",
|
||||
"radiation_hazard": "material_environmental",
|
||||
"unauthorized_access": "cyber_network",
|
||||
"communication_failure": "cyber_network",
|
||||
"firmware_corruption": "cyber_network",
|
||||
"logging_audit_failure": "cyber_network",
|
||||
"ai_misclassification": "ai_specific",
|
||||
"false_classification": "ai_specific",
|
||||
"model_drift": "ai_specific",
|
||||
"data_poisoning": "ai_specific",
|
||||
"sensor_spoofing": "ai_specific",
|
||||
"unintended_bias": "ai_specific",
|
||||
"sensor_fault": "software_control",
|
||||
"configuration_error": "software_control",
|
||||
"update_failure": "software_control",
|
||||
"hmi_error": "software_control",
|
||||
"emc_hazard": "electrical",
|
||||
"maintenance_hazard": "mechanical",
|
||||
"mode_confusion": "software_control",
|
||||
}
|
||||
if cat, ok := m[patternCat]; ok {
|
||||
return cat
|
||||
}
|
||||
return "general"
|
||||
}
|
||||
|
||||
// deriveComponentType guesses the component type from its tags.
|
||||
func deriveComponentType(tags []string) iace.ComponentType {
|
||||
for _, t := range tags {
|
||||
switch {
|
||||
case t == "software" || t == "has_software":
|
||||
return iace.ComponentTypeSoftware
|
||||
case t == "firmware" || t == "has_firmware":
|
||||
return iace.ComponentTypeFirmware
|
||||
case t == "has_ai" || t == "ai_model":
|
||||
return iace.ComponentTypeAIModel
|
||||
case t == "hmi" || t == "display" || t == "touchscreen":
|
||||
return iace.ComponentTypeHMI
|
||||
case t == "sensor" || t == "camera":
|
||||
return iace.ComponentTypeSensor
|
||||
case t == "electric_motor" || t == "electric_drive":
|
||||
return iace.ComponentTypeElectrical
|
||||
case t == "networked" || t == "ethernet" || t == "wifi":
|
||||
return iace.ComponentTypeNetwork
|
||||
case t == "hydraulic" || t == "pneumatic":
|
||||
return iace.ComponentTypeActuator
|
||||
}
|
||||
}
|
||||
return iace.ComponentTypeMechanical
|
||||
}
|
||||
|
||||
// extractOperationalStatesFromMetadata reads the explicit operational_states
|
||||
// selection that the user set via the Betriebszustand-UI.
|
||||
func extractOperationalStatesFromMetadata(metadata json.RawMessage) []string {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
var meta map[string]json.RawMessage
|
||||
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||
return nil
|
||||
}
|
||||
raw, ok := meta["operational_states"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var states []string
|
||||
if err := json.Unmarshal(raw, &states); err != nil {
|
||||
return nil
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// mergeStringSlices merges two string slices, deduplicating entries.
|
||||
func mergeStringSlices(a, b []string) []string {
|
||||
seen := make(map[string]bool, len(a)+len(b))
|
||||
var result []string
|
||||
for _, s := range a {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
for _, s := range b {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// findHazardForMeasureByCategory finds a matching hazard for a measure.
|
||||
func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID {
|
||||
// Direct match
|
||||
if id, ok := hazardsByCategory[measureCat]; ok {
|
||||
return id
|
||||
}
|
||||
// Fuzzy match — "mechanical" matches "mechanical_hazard"
|
||||
for cat, id := range hazardsByCategory {
|
||||
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
|
||||
return id
|
||||
}
|
||||
}
|
||||
// Fallback: first hazard
|
||||
for _, id := range hazardsByCategory {
|
||||
return id
|
||||
}
|
||||
return uuid.Nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// extractNarrativeFromMetadata builds a combined text from the limits_form.
|
||||
func extractNarrativeFromMetadata(metadata json.RawMessage) string {
|
||||
if metadata == nil {
|
||||
return ""
|
||||
}
|
||||
var meta map[string]json.RawMessage
|
||||
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||
return ""
|
||||
}
|
||||
limitsRaw, ok := meta["limits_form"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
var limits map[string]interface{}
|
||||
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
textFields := []string{
|
||||
"general_description", "intended_purpose", "foreseeable_misuse",
|
||||
"space_limits", "time_limits", "environmental_conditions",
|
||||
"energy_sources", "materials_processed", "operating_modes",
|
||||
"maintenance_requirements", "personnel_requirements",
|
||||
"interfaces_description", "control_system_description",
|
||||
"safety_functions_description",
|
||||
}
|
||||
var result string
|
||||
for _, field := range textFields {
|
||||
if v, ok := limits[field]; ok {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
result += s + "\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// patternCatToMeasureCat maps pattern hazard categories to measure categories.
|
||||
func patternCatToMeasureCat(patternCat string) string {
|
||||
m := map[string]string{
|
||||
"mechanical_hazard": "mechanical", "electrical_hazard": "electrical",
|
||||
"thermal_hazard": "thermal", "noise_vibration": "noise_vibration",
|
||||
"pneumatic_hydraulic": "pneumatic_hydraulic", "material_environmental": "material_environmental",
|
||||
"ergonomic": "ergonomic", "ergonomic_hazard": "ergonomic",
|
||||
"software_fault": "software_control", "safety_function_failure": "safety_function",
|
||||
"fire_explosion": "thermal", "radiation_hazard": "material_environmental",
|
||||
"unauthorized_access": "cyber_network", "communication_failure": "cyber_network",
|
||||
"firmware_corruption": "cyber_network", "logging_audit_failure": "cyber_network",
|
||||
"ai_misclassification": "ai_specific", "false_classification": "ai_specific",
|
||||
"model_drift": "ai_specific", "data_poisoning": "ai_specific",
|
||||
"sensor_spoofing": "ai_specific", "unintended_bias": "ai_specific",
|
||||
"sensor_fault": "software_control", "configuration_error": "software_control",
|
||||
"update_failure": "software_control", "hmi_error": "software_control",
|
||||
"emc_hazard": "electrical", "maintenance_hazard": "mechanical",
|
||||
"mode_confusion": "software_control", "chemical_risk": "material_environmental",
|
||||
}
|
||||
if cat, ok := m[patternCat]; ok {
|
||||
return cat
|
||||
}
|
||||
return "general"
|
||||
}
|
||||
|
||||
// deriveComponentType guesses the component type from its tags.
|
||||
func deriveComponentType(tags []string) iace.ComponentType {
|
||||
for _, t := range tags {
|
||||
switch {
|
||||
case t == "software" || t == "has_software":
|
||||
return iace.ComponentTypeSoftware
|
||||
case t == "firmware" || t == "has_firmware":
|
||||
return iace.ComponentTypeFirmware
|
||||
case t == "has_ai" || t == "ai_model":
|
||||
return iace.ComponentTypeAIModel
|
||||
case t == "hmi" || t == "display" || t == "touchscreen":
|
||||
return iace.ComponentTypeHMI
|
||||
case t == "sensor" || t == "camera":
|
||||
return iace.ComponentTypeSensor
|
||||
case t == "electric_motor" || t == "electric_drive":
|
||||
return iace.ComponentTypeElectrical
|
||||
case t == "networked" || t == "ethernet" || t == "wifi":
|
||||
return iace.ComponentTypeNetwork
|
||||
case t == "hydraulic" || t == "pneumatic":
|
||||
return iace.ComponentTypeActuator
|
||||
}
|
||||
}
|
||||
return iace.ComponentTypeMechanical
|
||||
}
|
||||
|
||||
// extractOperationalStatesFromMetadata reads the explicit operational_states
|
||||
// selection that the user set via the Betriebszustand-UI.
|
||||
func extractOperationalStatesFromMetadata(metadata json.RawMessage) []string {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
var meta map[string]json.RawMessage
|
||||
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||
return nil
|
||||
}
|
||||
raw, ok := meta["operational_states"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var states []string
|
||||
if err := json.Unmarshal(raw, &states); err != nil {
|
||||
return nil
|
||||
}
|
||||
return states
|
||||
}
|
||||
|
||||
// mergeStringSlices merges two string slices, deduplicating entries.
|
||||
func mergeStringSlices(a, b []string) []string {
|
||||
seen := make(map[string]bool, len(a)+len(b))
|
||||
var result []string
|
||||
for _, s := range a {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
for _, s := range b {
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// extractIndustrySectorsFromMetadata reads the industry_sectors selection
|
||||
// from project metadata and maps them to MachineTypes for pattern filtering.
|
||||
func extractIndustrySectorsFromMetadata(metadata json.RawMessage) []string {
|
||||
if metadata == nil {
|
||||
return nil
|
||||
}
|
||||
var meta map[string]json.RawMessage
|
||||
if err := json.Unmarshal(metadata, &meta); err != nil {
|
||||
return nil
|
||||
}
|
||||
limitsRaw, ok := meta["limits_form"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var limits map[string]json.RawMessage
|
||||
if err := json.Unmarshal(limitsRaw, &limits); err != nil {
|
||||
return nil
|
||||
}
|
||||
sectorsRaw, ok := limits["industry_sectors"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var sectors []string
|
||||
if err := json.Unmarshal(sectorsRaw, §ors); err != nil {
|
||||
return nil
|
||||
}
|
||||
labelMap := map[string][]string{
|
||||
"Allgemeiner Maschinenbau": {"general_industry"},
|
||||
"Automobil / Zulieferer": {"automotive"},
|
||||
"Robotik / Cobot": {"robotics_cobot", "cobot"},
|
||||
"Medizintechnik": {"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
"Lebensmittel / Getraenke": {"food_processing"},
|
||||
"Verpackung": {"packaging"},
|
||||
"Pharma / Chemie": {"chemical", "pharmaceutical"},
|
||||
"Bau / Baumaschinen": {"construction", "crane", "excavator"},
|
||||
"Forst / Holzbearbeitung": {"forestry", "woodworking", "circular_saw"},
|
||||
"Aufzuege / Foerdertechnik": {"elevator", "lift", "escalator", "conveyor"},
|
||||
"Textil": {"textile", "spinning", "weaving", "finishing"},
|
||||
"Landmaschinen": {"agricultural", "tractor", "harvester"},
|
||||
"Druck / Papier": {"printing"},
|
||||
"Metall / CNC": {"cnc", "metalworking", "lathe", "milling"},
|
||||
"Schweissen / Oberflaechentechnik": {"welding", "surface_treatment"},
|
||||
}
|
||||
var result []string
|
||||
seen := make(map[string]bool)
|
||||
for _, sector := range sectors {
|
||||
for _, mt := range labelMap[sector] {
|
||||
if !seen[mt] {
|
||||
seen[mt] = true
|
||||
result = append(result, mt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// containsSubstring checks if haystack contains needle (case-insensitive, normalized).
|
||||
func containsSubstring(haystack, needle string) bool {
|
||||
return strings.Contains(
|
||||
strings.ToLower(haystack),
|
||||
strings.ToLower(needle),
|
||||
)
|
||||
}
|
||||
|
||||
// genericSafetyTerms are words that appear in almost all risk assessments
|
||||
// and should NOT be used to determine machine-specificity.
|
||||
var genericSafetyTerms = map[string]bool{
|
||||
"maschine": true, "anlage": true, "bereich": true, "gesamte": true,
|
||||
"arbeitsplatz": true, "gefahrbereich": true, "gefahrstelle": true,
|
||||
"gefahrenstelle": true, "person": true, "werker": true, "bediener": true,
|
||||
"steuerung": true, "schutzeinrichtung": true, "sicherheit": true,
|
||||
"betrieb": true, "wartung": true, "instandhaltung": true, "reinigung": true,
|
||||
"bewegung": true, "beweglich": true, "feststehend": true, "teil": true,
|
||||
"teile": true, "oeffnung": true, "zugang": true, "gefahr": true,
|
||||
"verletzung": true, "quetsch": true, "scher": true, "schneid": true,
|
||||
"stoss": true, "schlag": true, "einzug": true, "brand": true,
|
||||
"motor": true, "antrieb": true, "achse": true, "achsen": true,
|
||||
"kabel": true, "leitung": true, "schaltschrank": true, "spannung": true,
|
||||
"schutz": true, "gehaeuse": true, "oberflaeche": true, "boden": true,
|
||||
"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,
|
||||
"front": true, "rueck": true, "ober": true, "unter": true,
|
||||
"fuehrung": true, "lager": true, "verschleiss": true, "welle": true,
|
||||
"getriebe": true, "kette": true, "riemen": true, "feder": true,
|
||||
"spindel": true, "werkzeug": true, "werkstueck": true, "flucht": true,
|
||||
}
|
||||
|
||||
// isPatternRelevant checks whether a pattern match is relevant to the actual
|
||||
// machine described in the narrative. Uses narrative vocabulary overlap:
|
||||
// if the pattern's zone/scenario contains machine-specific words (not generic
|
||||
// safety terms) and NONE of them appear in the narrative → irrelevant.
|
||||
func isPatternRelevant(mp iace.PatternMatch, narrative string, compNames []string) bool {
|
||||
patternText := iace.NormalizeDEPublic(mp.ZoneDE + " " + mp.ScenarioDE + " " + mp.PatternName)
|
||||
narrativeNorm := iace.NormalizeDEPublic(narrative)
|
||||
|
||||
// Extract machine-specific words from pattern (not generic safety terms)
|
||||
patternWords := strings.Fields(patternText)
|
||||
var specificWords []string
|
||||
for _, w := range patternWords {
|
||||
// Clean punctuation
|
||||
w = strings.Trim(w, ".,;:!?()/-")
|
||||
if len(w) < 5 || genericSafetyTerms[w] {
|
||||
continue
|
||||
}
|
||||
specificWords = append(specificWords, w)
|
||||
}
|
||||
|
||||
// If pattern has no specific words, it's generic → always relevant
|
||||
if len(specificWords) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if at least one specific word appears in the narrative or components
|
||||
for _, sw := range specificWords {
|
||||
if strings.Contains(narrativeNorm, sw) {
|
||||
return true
|
||||
}
|
||||
for _, cn := range compNames {
|
||||
if strings.Contains(cn, sw) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No specific word found in narrative → pattern is for a different machine
|
||||
return false
|
||||
}
|
||||
|
||||
// categoryHazardCap returns the maximum number of hazards to generate per category.
|
||||
// Caps are based on typical ISO 12100 risk assessment proportions:
|
||||
// - Core physical categories (mechanical, electrical): scale with component count
|
||||
// - Secondary categories (thermal, noise, material): smaller fixed caps
|
||||
// - Software/IT/organizational categories: minimal (these are usually covered by
|
||||
// other standards like IEC 62443, not ISO 12100 machinery risk assessment)
|
||||
func categoryHazardCap(cat string, componentCount int) int {
|
||||
// Core machinery hazard categories — scale with complexity
|
||||
switch cat {
|
||||
case "mechanical_hazard":
|
||||
// Typically 1-3 hazards per component (quetschen, scheren, stoss...)
|
||||
cap := componentCount * 3
|
||||
if cap < 15 {
|
||||
cap = 15
|
||||
}
|
||||
if cap > 60 {
|
||||
cap = 60
|
||||
}
|
||||
return cap
|
||||
case "electrical_hazard":
|
||||
// Typically 8-15 for a standard machine
|
||||
cap := componentCount
|
||||
if cap < 8 {
|
||||
cap = 8
|
||||
}
|
||||
if cap > 20 {
|
||||
cap = 20
|
||||
}
|
||||
return cap
|
||||
case "pneumatic_hydraulic":
|
||||
return 8
|
||||
case "thermal_hazard":
|
||||
return 6
|
||||
case "noise_vibration":
|
||||
return 4
|
||||
case "material_environmental":
|
||||
return 6
|
||||
case "ergonomic", "ergonomic_hazard":
|
||||
return 4
|
||||
case "fire_explosion":
|
||||
return 4
|
||||
case "radiation_hazard", "emc_hazard":
|
||||
return 3
|
||||
// Software/IT/organizational — minimal for machinery assessment
|
||||
case "safety_function_failure":
|
||||
return 5
|
||||
case "software_fault":
|
||||
return 3
|
||||
case "configuration_error":
|
||||
return 3
|
||||
case "hmi_error":
|
||||
return 3
|
||||
case "maintenance_hazard":
|
||||
return 4
|
||||
case "mode_confusion":
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeZoneKey reduces a zone string to its core components for better dedup.
|
||||
// E.g. "Schaltschrank, Sammelschiene" and "Schaltschrank-Innenraum, Sammelschienen"
|
||||
// should dedup to the same key.
|
||||
func normalizeZoneKey(zone string) string {
|
||||
if zone == "" {
|
||||
return ""
|
||||
}
|
||||
norm := iace.NormalizeDEPublic(zone)
|
||||
// Remove filler words and punctuation
|
||||
for _, r := range []string{",", "/", "(", ")", "-", ".", ":", ";"} {
|
||||
norm = strings.ReplaceAll(norm, r, " ")
|
||||
}
|
||||
// Extract significant words (>3 chars), sort for stable key
|
||||
words := strings.Fields(norm)
|
||||
var sig []string
|
||||
seen := make(map[string]bool)
|
||||
stopWords := map[string]bool{
|
||||
"der": true, "die": true, "das": true, "und": true, "oder": true,
|
||||
"von": true, "des": true, "den": true, "dem": true, "ein": true,
|
||||
"eine": true, "fuer": true, "bei": true, "mit": true, "nach": true,
|
||||
"alle": true, "aller": true, "allem": true, "sowie": true,
|
||||
"insbesondere": true, "bereich": true, "gesamte": true, "gesamter": true,
|
||||
"innerhalb": true, "ausserhalb": true, "umgebung": true,
|
||||
}
|
||||
for _, w := range words {
|
||||
if len(w) < 4 || stopWords[w] || seen[w] {
|
||||
continue
|
||||
}
|
||||
seen[w] = true
|
||||
sig = append(sig, w)
|
||||
}
|
||||
if len(sig) == 0 {
|
||||
return norm
|
||||
}
|
||||
// Take first 3 significant words as key (enough for dedup)
|
||||
if len(sig) > 3 {
|
||||
sig = sig[:3]
|
||||
}
|
||||
return strings.Join(sig, "_")
|
||||
}
|
||||
|
||||
// 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, ids := range hazardsByCategory {
|
||||
if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] {
|
||||
return ids
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/dsms"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -411,6 +412,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("PDF export failed: %v", err)})
|
||||
return
|
||||
}
|
||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.pdf", safeName), projectID.String())
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.pdf"`, safeName))
|
||||
c.Data(http.StatusOK, "application/pdf", data)
|
||||
|
||||
@@ -420,6 +422,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel export failed: %v", err)})
|
||||
return
|
||||
}
|
||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.xlsx", safeName), projectID.String())
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.xlsx"`, safeName))
|
||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
|
||||
|
||||
@@ -429,6 +432,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("DOCX export failed: %v", err)})
|
||||
return
|
||||
}
|
||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.docx", safeName), projectID.String())
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.docx"`, safeName))
|
||||
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", data)
|
||||
|
||||
@@ -438,6 +442,7 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Markdown export failed: %v", err)})
|
||||
return
|
||||
}
|
||||
archiveTechFile(data, fmt.Sprintf("CE-Akte-%s.md", safeName), projectID.String())
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.md"`, safeName))
|
||||
c.Data(http.StatusOK, "text/markdown", data)
|
||||
|
||||
@@ -462,3 +467,8 @@ func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// archiveTechFile stores a tech-file export to DSMS (best-effort, non-blocking).
|
||||
func archiveTechFile(data []byte, filename, projectID string) {
|
||||
dsms.Archive(data, filename, "ce_techfile", projectID, "1")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/usecase"
|
||||
)
|
||||
|
||||
// UseCaseHandler handles use-case compiler endpoints.
|
||||
type UseCaseHandler struct {
|
||||
store *usecase.Store
|
||||
compiler *usecase.Compiler
|
||||
gapDetector *usecase.GapDetector
|
||||
}
|
||||
|
||||
// NewUseCaseHandler creates a new UseCaseHandler.
|
||||
func NewUseCaseHandler(pool *pgxpool.Pool, registry *llm.ProviderRegistry) *UseCaseHandler {
|
||||
store := usecase.NewStore(pool)
|
||||
llmGen := usecase.NewLLMQuestionGenerator(registry)
|
||||
return &UseCaseHandler{
|
||||
store: store,
|
||||
compiler: usecase.NewCompiler(store, llmGen),
|
||||
gapDetector: usecase.NewGapDetector(store),
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplates returns all available use-case templates.
|
||||
// GET /sdk/v1/use-case/templates
|
||||
func (h *UseCaseHandler) GetTemplates(c *gin.Context) {
|
||||
templates := usecase.TemplateList()
|
||||
c.JSON(http.StatusOK, gin.H{"templates": templates, "total": len(templates)})
|
||||
}
|
||||
|
||||
// GetTemplate returns a specific template with compiled questions.
|
||||
// GET /sdk/v1/use-case/templates/:id
|
||||
func (h *UseCaseHandler) GetTemplate(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
tmpl, ok := usecase.Templates[id]
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "template not found"})
|
||||
return
|
||||
}
|
||||
|
||||
questions, err := h.compiler.Compile(&tmpl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
tmpl.Questions = questions
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"template": tmpl})
|
||||
}
|
||||
|
||||
// Compile generates questions from MC filters ad-hoc.
|
||||
// POST /sdk/v1/use-case/compile
|
||||
// Uses the full pipeline: doc_check → LLM → deterministic fallback
|
||||
func (h *UseCaseHandler) Compile(c *gin.Context) {
|
||||
var req struct {
|
||||
MCFilters []string `json:"mc_filters" binding:"required"`
|
||||
Regulations []string `json:"regulations"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := &usecase.Template{
|
||||
ID: "custom",
|
||||
MCFilters: req.MCFilters,
|
||||
Regulations: req.Regulations,
|
||||
}
|
||||
|
||||
questions, err := h.compiler.Compile(tmpl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"questions": questions, "total": len(questions)})
|
||||
}
|
||||
|
||||
// CreateAudit starts a new audit from a template.
|
||||
// POST /sdk/v1/use-case/audits
|
||||
func (h *UseCaseHandler) CreateAudit(c *gin.Context) {
|
||||
var input usecase.CreateAuditInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tenantID, err := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "X-Tenant-ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, ok := usecase.Templates[input.TemplateID]
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unknown template_id"})
|
||||
return
|
||||
}
|
||||
|
||||
questions, err := h.compiler.Compile(&tmpl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
audit := &usecase.Audit{
|
||||
TenantID: tenantID,
|
||||
TemplateID: input.TemplateID,
|
||||
Name: input.Name,
|
||||
TargetName: input.TargetName,
|
||||
TotalQuestions: len(questions),
|
||||
Questions: questions,
|
||||
}
|
||||
|
||||
if err := h.store.CreateAudit(audit); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create audit"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"audit": audit})
|
||||
}
|
||||
|
||||
// ListAudits returns all audits for a tenant.
|
||||
// GET /sdk/v1/use-case/audits
|
||||
func (h *UseCaseHandler) ListAudits(c *gin.Context) {
|
||||
tenantID, err := uuid.Parse(c.GetHeader("X-Tenant-ID"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "X-Tenant-ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
audits, err := h.store.ListAudits(tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list audits"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"audits": audits, "total": len(audits)})
|
||||
}
|
||||
|
||||
// GetAudit returns an audit with questions and answers.
|
||||
// GET /sdk/v1/use-case/audits/:id
|
||||
func (h *UseCaseHandler) GetAudit(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"})
|
||||
return
|
||||
}
|
||||
|
||||
audit, err := h.store.GetAudit(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"})
|
||||
return
|
||||
}
|
||||
|
||||
answers, err := h.store.ListAnswers(id)
|
||||
if err != nil {
|
||||
answers = nil
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"audit": audit, "answers": answers})
|
||||
}
|
||||
|
||||
// AnswerQuestion saves an answer for a question in an audit.
|
||||
// POST /sdk/v1/use-case/audits/:id/answer
|
||||
func (h *UseCaseHandler) AnswerQuestion(c *gin.Context) {
|
||||
auditID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var input usecase.AnswerInput
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Find MC ID from the question
|
||||
audit, err := h.store.GetAudit(auditID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var mcID string
|
||||
for _, q := range audit.Questions {
|
||||
if q.ID == input.QuestionID {
|
||||
mcID = q.MCID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
status := usecase.AnswerStatusAnswered
|
||||
if input.Status == "skipped" {
|
||||
status = usecase.AnswerStatusSkipped
|
||||
} else if input.Status == "escalated" {
|
||||
status = usecase.AnswerStatusEscalated
|
||||
}
|
||||
|
||||
answer := &usecase.Answer{
|
||||
AuditID: auditID,
|
||||
QuestionID: input.QuestionID,
|
||||
MCID: mcID,
|
||||
Value: input.Value,
|
||||
Comment: input.Comment,
|
||||
EvidenceIDs: input.EvidenceIDs,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if err := h.store.SaveAnswer(answer); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save answer"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update audit counters
|
||||
answers, _ := h.store.ListAnswers(auditID)
|
||||
score := usecase.Score(audit, answers)
|
||||
|
||||
auditStatus := usecase.StatusInProgress
|
||||
if score.Answered >= audit.TotalQuestions {
|
||||
auditStatus = usecase.StatusCompleted
|
||||
}
|
||||
h.store.UpdateAuditScore(auditID, score.Answered, score.ComplianceScore, auditStatus)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"answer": answer, "progress": score})
|
||||
}
|
||||
|
||||
// GetScore calculates and returns the compliance score.
|
||||
// GET /sdk/v1/use-case/audits/:id/score
|
||||
func (h *UseCaseHandler) GetScore(c *gin.Context) {
|
||||
auditID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"})
|
||||
return
|
||||
}
|
||||
|
||||
audit, err := h.store.GetAudit(auditID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"})
|
||||
return
|
||||
}
|
||||
|
||||
answers, err := h.store.ListAnswers(auditID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load answers"})
|
||||
return
|
||||
}
|
||||
|
||||
score := usecase.Score(audit, answers)
|
||||
c.JSON(http.StatusOK, score)
|
||||
}
|
||||
|
||||
// GetGaps returns missing regulation sources for an audit.
|
||||
// GET /sdk/v1/use-case/audits/:id/gaps
|
||||
func (h *UseCaseHandler) GetGaps(c *gin.Context) {
|
||||
auditID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid audit ID"})
|
||||
return
|
||||
}
|
||||
|
||||
audit, err := h.store.GetAudit(auditID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "audit not found"})
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, ok := usecase.Templates[audit.TemplateID]
|
||||
if !ok {
|
||||
c.JSON(http.StatusOK, gin.H{"gaps": []interface{}{}, "audit_gaps": []interface{}{}})
|
||||
return
|
||||
}
|
||||
|
||||
// Missing regulation sources (from MC analysis)
|
||||
missingRegs, err := h.gapDetector.DetectMissingRegulations(&tmpl)
|
||||
if err != nil {
|
||||
missingRegs = nil
|
||||
}
|
||||
|
||||
// Audit-specific gaps (from answer analysis)
|
||||
answers, _ := h.store.ListAnswers(auditID)
|
||||
auditGaps := h.gapDetector.DetectAuditGaps(audit, answers)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"missing_sources": missingRegs,
|
||||
"audit_gaps": auditGaps,
|
||||
})
|
||||
}
|
||||
@@ -155,6 +155,9 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine {
|
||||
// Gap Analysis
|
||||
gapHandler := handlers.NewGapHandler(pool)
|
||||
|
||||
// Use-Case Compiler
|
||||
useCaseHandler := handlers.NewUseCaseHandler(pool, providerRegistry)
|
||||
|
||||
rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine)
|
||||
|
||||
// Router
|
||||
@@ -179,7 +182,7 @@ func buildRouter(cfg *config.Config, pool *pgxpool.Pool) *gin.Engine {
|
||||
uccaHandlers, escalationHandlers, obligationsHandlers, ragHandlers,
|
||||
roadmapHandlers, workshopHandlers, portfolioHandlers,
|
||||
academyHandlers, trainingHandlers, whistleblowerHandlers, iaceHandler,
|
||||
gapHandler, maximizerHandlers, regulatoryNewsHandlers)
|
||||
gapHandler, maximizerHandlers, regulatoryNewsHandlers, useCaseHandler)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func registerRoutes(
|
||||
gapHandler *handlers.GapHandler,
|
||||
maximizerHandlers *handlers.MaximizerHandlers,
|
||||
regulatoryNewsHandlers *handlers.RegulatoryNewsHandlers,
|
||||
useCaseHandler *handlers.UseCaseHandler,
|
||||
) {
|
||||
v1 := router.Group("/sdk/v1")
|
||||
{
|
||||
@@ -51,6 +52,7 @@ func registerRoutes(
|
||||
registerIACERoutes(v1, iaceHandler)
|
||||
registerGapRoutes(v1, gapHandler)
|
||||
registerMaximizerRoutes(v1, maximizerHandlers)
|
||||
registerUseCaseRoutes(v1, useCaseHandler)
|
||||
v1.GET("/regulatory-news", regulatoryNewsHandlers.GetNews)
|
||||
}
|
||||
}
|
||||
@@ -393,6 +395,8 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) {
|
||||
iaceRoutes.POST("/projects/:id/match-patterns", h.MatchPatterns)
|
||||
iaceRoutes.POST("/projects/:id/parse-narrative", h.ParseNarrative)
|
||||
iaceRoutes.POST("/projects/:id/delta-analysis", h.DeltaAnalysis)
|
||||
iaceRoutes.GET("/projects/:id/fmea/export", h.ExportFMEA)
|
||||
iaceRoutes.POST("/projects/:id/components/:cid/suggest-fms", h.SuggestFailureModes)
|
||||
iaceRoutes.POST("/projects/:id/apply-patterns", h.ApplyPatternResults)
|
||||
iaceRoutes.POST("/projects/:id/hazards/:hid/suggest-measures", h.SuggestMeasuresForHazard)
|
||||
iaceRoutes.POST("/projects/:id/mitigations/:mid/suggest-evidence", h.SuggestEvidenceForMitigation)
|
||||
@@ -428,6 +432,10 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) {
|
||||
iaceRoutes.POST("/library-search", h.SearchLibrary)
|
||||
iaceRoutes.GET("/ce-corpus-documents", h.ListCECorpusDocuments)
|
||||
iaceRoutes.POST("/projects/:id/initialize", h.InitializeProject)
|
||||
iaceRoutes.GET("/projects/:id/hazard-blocks", h.GetHazardBlocks)
|
||||
iaceRoutes.POST("/projects/:id/benchmark/import-gt", h.ImportGroundTruth)
|
||||
iaceRoutes.GET("/projects/:id/benchmark", h.RunBenchmark)
|
||||
iaceRoutes.GET("/projects/:id/benchmark/summary", h.GetBenchmarkSummary)
|
||||
iaceRoutes.GET("/projects/:id/hazards/:hid/regulatory-hints", h.EnrichHazardWithRegulations)
|
||||
iaceRoutes.GET("/projects/:id/mitigations/:mid/regulatory-hints", h.EnrichMitigationWithRegulations)
|
||||
iaceRoutes.GET("/projects/:id/regulatory-hints", h.EnrichProjectHazardsBatch)
|
||||
@@ -461,6 +469,21 @@ func registerMaximizerRoutes(v1 *gin.RouterGroup, h *handlers.MaximizerHandlers)
|
||||
}
|
||||
}
|
||||
|
||||
func registerUseCaseRoutes(v1 *gin.RouterGroup, h *handlers.UseCaseHandler) {
|
||||
uc := v1.Group("/use-case")
|
||||
{
|
||||
uc.GET("/templates", h.GetTemplates)
|
||||
uc.GET("/templates/:id", h.GetTemplate)
|
||||
uc.POST("/compile", h.Compile)
|
||||
uc.POST("/audits", h.CreateAudit)
|
||||
uc.GET("/audits", h.ListAudits)
|
||||
uc.GET("/audits/:id", h.GetAudit)
|
||||
uc.POST("/audits/:id/answer", h.AnswerQuestion)
|
||||
uc.GET("/audits/:id/score", h.GetScore)
|
||||
uc.GET("/audits/:id/gaps", h.GetGaps)
|
||||
}
|
||||
}
|
||||
|
||||
func registerGapRoutes(v1 *gin.RouterGroup, h *handlers.GapHandler) {
|
||||
g := v1.Group("/gap")
|
||||
{
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
package iace
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Fuzzy matching: Ground Truth entries ↔ Engine hazards
|
||||
// ============================================================================
|
||||
|
||||
const matchThreshold = 0.20
|
||||
|
||||
// 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 {
|
||||
if gt == nil || len(gt.Entries) == 0 {
|
||||
return &BenchmarkResult{}
|
||||
}
|
||||
|
||||
// Build mitigation names per hazard
|
||||
mitNamesByHazard := make(map[string][]string)
|
||||
for _, m := range mitigations {
|
||||
mitNamesByHazard[m.HazardID.String()] = append(mitNamesByHazard[m.HazardID.String()], m.Name)
|
||||
}
|
||||
|
||||
engineSummaries := make([]HazardSummary, len(hazards))
|
||||
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,
|
||||
AffectedPerson: h.AffectedPerson,
|
||||
LifecyclePhase: h.LifecyclePhase,
|
||||
Mitigations: mitNamesByHazard[h.ID.String()],
|
||||
}
|
||||
}
|
||||
|
||||
// Build score matrix: gt[i] × engine[j]
|
||||
type scoredPair struct {
|
||||
gtIdx, engIdx int
|
||||
score float64
|
||||
reason string
|
||||
}
|
||||
var pairs []scoredPair
|
||||
for i := range gt.Entries {
|
||||
for j := range hazards {
|
||||
score, reason := fuzzyMatchScore(>.Entries[i], &hazards[j])
|
||||
if score >= matchThreshold {
|
||||
pairs = append(pairs, scoredPair{i, j, score, reason})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
for _, p := range pairs {
|
||||
if usedGT[p.gtIdx] || usedEng[p.engIdx] {
|
||||
continue
|
||||
}
|
||||
usedGT[p.gtIdx] = true
|
||||
usedEng[p.engIdx] = true
|
||||
matched = append(matched, HazardMatchPair{
|
||||
GTEntry: gt.Entries[p.gtIdx],
|
||||
EngineHazard: engineSummaries[p.engIdx],
|
||||
MatchScore: p.score,
|
||||
MatchReason: p.reason,
|
||||
})
|
||||
}
|
||||
|
||||
// Collect unmatched
|
||||
var missing []GroundTruthEntry
|
||||
for i, e := range gt.Entries {
|
||||
if !usedGT[i] {
|
||||
missing = append(missing, e)
|
||||
}
|
||||
}
|
||||
var extra []HazardSummary
|
||||
for i, s := range engineSummaries {
|
||||
if !usedEng[i] {
|
||||
extra = append(extra, s)
|
||||
}
|
||||
}
|
||||
|
||||
// Category breakdown
|
||||
catGT := map[string]int{}
|
||||
catMatch := map[string]int{}
|
||||
for _, e := range gt.Entries {
|
||||
cat := normalizeCategoryDE(e.HazardGroup)
|
||||
catGT[cat]++
|
||||
}
|
||||
for _, m := range matched {
|
||||
cat := normalizeCategoryDE(m.GTEntry.HazardGroup)
|
||||
catMatch[cat]++
|
||||
}
|
||||
var breakdown []CategoryScore
|
||||
for cat, total := range catGT {
|
||||
cov := 0.0
|
||||
if total > 0 {
|
||||
cov = float64(catMatch[cat]) / float64(total)
|
||||
}
|
||||
breakdown = append(breakdown, CategoryScore{
|
||||
Category: cat, GTCount: total, MatchCount: catMatch[cat], Coverage: cov,
|
||||
})
|
||||
}
|
||||
sort.Slice(breakdown, func(i, j int) bool { return breakdown[i].GTCount > breakdown[j].GTCount })
|
||||
|
||||
// Measure coverage (simplified: count GT entries where at least 1 measure keyword matches)
|
||||
measMatched := 0
|
||||
for _, m := range matched {
|
||||
if measureOverlap(m.GTEntry.Measures, mitigations) {
|
||||
measMatched++
|
||||
}
|
||||
}
|
||||
measCov := 0.0
|
||||
if len(matched) > 0 {
|
||||
measCov = float64(measMatched) / float64(len(matched))
|
||||
}
|
||||
|
||||
// Risk rank comparison
|
||||
rankPairs := buildRiskRankPairs(matched)
|
||||
|
||||
coverage := 0.0
|
||||
if len(gt.Entries) > 0 {
|
||||
coverage = float64(len(matched)) / float64(len(gt.Entries))
|
||||
}
|
||||
|
||||
return &BenchmarkResult{
|
||||
CoverageScore: coverage,
|
||||
MeasureCoverage: measCov,
|
||||
TotalGT: len(gt.Entries),
|
||||
TotalEngine: len(hazards),
|
||||
MatchedPairs: matched,
|
||||
MissingFromEngine: missing,
|
||||
ExtraInEngine: extra,
|
||||
CategoryBreakdown: breakdown,
|
||||
RiskRankPairs: rankPairs,
|
||||
}
|
||||
}
|
||||
|
||||
// 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.2)
|
||||
catScore := categoryMatchScore(gt.HazardGroup, h.Category)
|
||||
score += 0.2 * catScore
|
||||
if catScore > 0 {
|
||||
reasons = append(reasons, "Kategorie")
|
||||
}
|
||||
|
||||
// 2. Keyword/synonym match on hazard TYPE (weight 0.2)
|
||||
kwScore := keywordMatchScore(gt.HazardType, gt.HazardCause, h.Name, h.Description, h.Scenario)
|
||||
score += 0.2 * kwScore
|
||||
if kwScore > 0 {
|
||||
reasons = append(reasons, "Keywords")
|
||||
}
|
||||
|
||||
// 3. Component/zone match (weight 0.3)
|
||||
zoneScore := zoneMatchScore(gt.ComponentZone, gt.HazardSubgroup, h.HazardousZone, h.MachineModule)
|
||||
score += 0.3 * zoneScore
|
||||
if zoneScore > 0 {
|
||||
reasons = append(reasons, "Zone")
|
||||
}
|
||||
|
||||
// 4. Scenario similarity (weight 0.3) — compares the actual event description
|
||||
scenScore := scenarioSimilarity(gt.HazardCause, h.Scenario, h.Name)
|
||||
score += 0.3 * scenScore
|
||||
if scenScore > 0 {
|
||||
reasons = append(reasons, "Szenario")
|
||||
}
|
||||
|
||||
// Penalty: wrong machine term
|
||||
if hasWrongMachineTerm(h.Name, h.Scenario, gt.HazardCause, gt.ComponentZone) {
|
||||
score *= 0.3
|
||||
reasons = append(reasons, "Strafabzug:FremdMaschine")
|
||||
}
|
||||
|
||||
// Penalty: no keyword AND no scenario overlap → unreliable
|
||||
if kwScore == 0 && scenScore == 0 && zoneScore < 0.5 {
|
||||
score *= 0.4
|
||||
reasons = append(reasons, "Strafabzug:KeinInhalt")
|
||||
}
|
||||
|
||||
return score, strings.Join(reasons, "+")
|
||||
}
|
||||
|
||||
// scenarioSimilarity compares the GT cause description with the engine scenario.
|
||||
// Uses action words + synonym-set cross-matching for robust comparison.
|
||||
func scenarioSimilarity(gtCause, engScenario, engName string) float64 {
|
||||
gtText := normalizeDE(gtCause)
|
||||
engText := normalizeDE(engScenario + " " + engName)
|
||||
|
||||
gtActions := extractActionWords(gtText)
|
||||
engActions := extractActionWords(engText)
|
||||
|
||||
if len(gtActions) == 0 {
|
||||
// Fallback: use significant word overlap
|
||||
return significantWordOverlap(gtText, engText)
|
||||
}
|
||||
|
||||
matched := 0
|
||||
for _, ga := range gtActions {
|
||||
// Direct match
|
||||
directFound := false
|
||||
for _, ea := range engActions {
|
||||
if ga == ea || strings.HasPrefix(ea, ga) || strings.HasPrefix(ga, ea) {
|
||||
directFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if directFound {
|
||||
matched++
|
||||
continue
|
||||
}
|
||||
// Synonym-set match: if GT action and any engine action are in the same synonym set
|
||||
for _, synSet := range synonymSets {
|
||||
gaInSet := false
|
||||
for _, syn := range synSet {
|
||||
if strings.Contains(ga, syn) || strings.Contains(syn, ga) {
|
||||
gaInSet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !gaInSet {
|
||||
continue
|
||||
}
|
||||
// Check if any engine action is in this same set
|
||||
for _, ea := range engActions {
|
||||
for _, syn := range synSet {
|
||||
if strings.Contains(ea, syn) || strings.Contains(syn, ea) {
|
||||
matched++
|
||||
goto nextAction
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check full engine text for synonym hit
|
||||
for _, syn := range synSet {
|
||||
if strings.Contains(engText, syn) {
|
||||
matched++
|
||||
goto nextAction
|
||||
}
|
||||
}
|
||||
}
|
||||
nextAction:
|
||||
}
|
||||
return float64(matched) / float64(len(gtActions))
|
||||
}
|
||||
|
||||
// significantWordOverlap is a fallback when no action words are found.
|
||||
func significantWordOverlap(gtText, engText string) float64 {
|
||||
gtWords := extractSignificantWords(gtText)
|
||||
if len(gtWords) == 0 {
|
||||
return 0
|
||||
}
|
||||
matched := 0
|
||||
for _, w := range gtWords {
|
||||
if strings.Contains(engText, w) {
|
||||
matched++
|
||||
}
|
||||
}
|
||||
return float64(matched) / float64(len(gtWords))
|
||||
}
|
||||
|
||||
func hasWrongMachineTerm(engName, engScenario, gtCause, gtZone string) bool {
|
||||
engText := normalizeDE(engName + " " + engScenario)
|
||||
gtText := normalizeDE(gtCause + " " + gtZone)
|
||||
for _, term := range wrongMachineTerms {
|
||||
if strings.Contains(engText, term) && !strings.Contains(gtText, term) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func categoryMatchScore(gtGroup, engCategory string) float64 {
|
||||
normalized := normalizeDE(gtGroup)
|
||||
prefixes, ok := categoryMap[normalized]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
engLower := strings.ToLower(engCategory)
|
||||
for _, p := range prefixes {
|
||||
if strings.Contains(engLower, p) {
|
||||
return 1.0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func keywordMatchScore(gtType, gtCause, engName, engDesc, engScenario string) float64 {
|
||||
gtText := normalizeDE(gtType + " " + gtCause)
|
||||
engText := normalizeDE(engName + " " + engDesc + " " + engScenario)
|
||||
|
||||
matchedSets := 0
|
||||
totalRelevant := 0
|
||||
|
||||
for _, synSet := range synonymSets {
|
||||
gtHas := false
|
||||
engHas := false
|
||||
for _, syn := range synSet {
|
||||
if strings.Contains(gtText, syn) {
|
||||
gtHas = true
|
||||
}
|
||||
if strings.Contains(engText, syn) {
|
||||
engHas = true
|
||||
}
|
||||
}
|
||||
if gtHas {
|
||||
totalRelevant++
|
||||
if engHas {
|
||||
matchedSets++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalRelevant == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(matchedSets) / float64(totalRelevant)
|
||||
}
|
||||
|
||||
func zoneMatchScore(gtZone, gtSubgroup, engZone, engModule string) float64 {
|
||||
gtText := normalizeDE(gtZone + " " + gtSubgroup)
|
||||
engText := normalizeDE(engZone + " " + engModule)
|
||||
|
||||
if gtText == "" || engText == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Check for significant word overlap
|
||||
gtWords := extractSignificantWords(gtText)
|
||||
engWords := extractSignificantWords(engText)
|
||||
|
||||
if len(gtWords) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
matched := 0
|
||||
for _, gw := range gtWords {
|
||||
for _, ew := range engWords {
|
||||
if strings.Contains(ew, gw) || strings.Contains(gw, ew) {
|
||||
matched++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return float64(matched) / float64(len(gtWords))
|
||||
}
|
||||
|
||||
func extractSignificantWords(text string) []string {
|
||||
stopWords := map[string]bool{
|
||||
"der": true, "die": true, "das": true, "und": true, "oder": true,
|
||||
"von": true, "in": true, "an": true, "am": true, "im": true,
|
||||
"zu": true, "bei": true, "mit": true, "des": true, "den": true,
|
||||
"dem": true, "ein": true, "eine": true, "einer": true, "einem": true,
|
||||
"fuer": true, "auf": true, "aus": true, "um": true, "nach": true,
|
||||
"ueber": true, "unter": true, "vor": true, "durch": true,
|
||||
}
|
||||
words := strings.Fields(text)
|
||||
var sig []string
|
||||
for _, w := range words {
|
||||
if len(w) < 3 || stopWords[w] {
|
||||
continue
|
||||
}
|
||||
sig = append(sig, w)
|
||||
}
|
||||
return sig
|
||||
}
|
||||
|
||||
// NormalizeDEPublic is the exported version of normalizeDE for use outside this package.
|
||||
func NormalizeDEPublic(s string) string { return normalizeDE(s) }
|
||||
|
||||
// normalizeDE lowercases and replaces umlauts (same as narrative_parser).
|
||||
func normalizeDE(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
s = strings.ReplaceAll(s, "ä", "ae")
|
||||
s = strings.ReplaceAll(s, "ö", "oe")
|
||||
s = strings.ReplaceAll(s, "ü", "ue")
|
||||
s = strings.ReplaceAll(s, "ß", "ss")
|
||||
return s
|
||||
}
|
||||
|
||||
func normalizeCategoryDE(group string) string {
|
||||
n := normalizeDE(group)
|
||||
// Shorten for display
|
||||
n = strings.TrimPrefix(n, "gefaehrdungen durch ")
|
||||
n = strings.TrimPrefix(n, "gefaehrdungen im zusammenhang mit ")
|
||||
return n
|
||||
}
|
||||
|
||||
func measureOverlap(gtMeasures []string, mitigations []Mitigation) bool {
|
||||
for _, gm := range gtMeasures {
|
||||
gmNorm := normalizeDE(gm)
|
||||
for _, m := range mitigations {
|
||||
mNorm := normalizeDE(m.Name + " " + m.Description)
|
||||
// Check if any significant word from GT measure appears in engine mitigation
|
||||
words := extractSignificantWords(gmNorm)
|
||||
for _, w := range words {
|
||||
if strings.Contains(mNorm, w) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildRiskRankPairs(matched []HazardMatchPair) []RiskRankPair {
|
||||
if len(matched) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort by GT risk descending to get GT rank
|
||||
type ranked struct {
|
||||
idx int
|
||||
gtRisk int
|
||||
name string
|
||||
}
|
||||
items := make([]ranked, len(matched))
|
||||
for i, m := range matched {
|
||||
items[i] = ranked{i, m.GTEntry.RiskIn.R, m.GTEntry.HazardType}
|
||||
}
|
||||
sort.Slice(items, func(a, b int) bool { return items[a].gtRisk > items[b].gtRisk })
|
||||
|
||||
pairs := make([]RiskRankPair, len(items))
|
||||
for rank, item := range items {
|
||||
pairs[rank] = RiskRankPair{
|
||||
GTRank: rank + 1,
|
||||
EngineRank: 0, // Engine has no assessment yet for auto-generated hazards
|
||||
HazardName: item.name,
|
||||
GTRiskScore: item.gtRisk,
|
||||
EngineRisk: 0,
|
||||
}
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package iace
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// ============================================================================
|
||||
// Ground Truth types — stores a professional risk assessment for benchmarking
|
||||
// ============================================================================
|
||||
|
||||
// GroundTruth is the top-level container stored in project metadata.ground_truth.
|
||||
type GroundTruth struct {
|
||||
Entries []GroundTruthEntry `json:"entries"`
|
||||
SourceFile string `json:"source_file,omitempty"`
|
||||
ImportedAt string `json:"imported_at"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// GroundTruthEntry represents a single hazard from a professional risk assessment.
|
||||
type GroundTruthEntry struct {
|
||||
Nr string `json:"nr"`
|
||||
HazardGroup string `json:"hazard_group"`
|
||||
HazardGroupApplicable bool `json:"hazard_group_applicable"`
|
||||
HazardSubgroup string `json:"hazard_subgroup"`
|
||||
HazardType string `json:"hazard_type"`
|
||||
HazardCause string `json:"hazard_cause"`
|
||||
LifecyclePhases []string `json:"lifecycle_phases"`
|
||||
ComponentZone string `json:"component_zone"`
|
||||
RiskIn GTRisk `json:"risk_in"`
|
||||
PLr *GTPLr `json:"plr,omitempty"`
|
||||
Measures []string `json:"measures"`
|
||||
MeasureType string `json:"measure_type"`
|
||||
RiskOut GTRisk `json:"risk_out"`
|
||||
NormReferences []string `json:"norm_references"`
|
||||
Sufficient bool `json:"sufficient"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
ReductionSteps []GTReductionStep `json:"reduction_steps,omitempty"`
|
||||
}
|
||||
|
||||
// GTRisk represents the EN 62061 additive risk: R = (F + W + P) * S.
|
||||
type GTRisk struct {
|
||||
F int `json:"f"`
|
||||
W int `json:"w"`
|
||||
P int `json:"p"`
|
||||
S int `json:"s"`
|
||||
R int `json:"r"`
|
||||
}
|
||||
|
||||
// GTPLr represents Performance Level required (EN ISO 13849-1).
|
||||
type GTPLr struct {
|
||||
S string `json:"s"`
|
||||
F string `json:"f"`
|
||||
P string `json:"p"`
|
||||
EW string `json:"ew,omitempty"`
|
||||
PLr string `json:"plr"`
|
||||
}
|
||||
|
||||
// GTReductionStep represents an iterative risk reduction row.
|
||||
type GTReductionStep struct {
|
||||
RiskIn GTRisk `json:"risk_in"`
|
||||
PLr *GTPLr `json:"plr,omitempty"`
|
||||
Measures []string `json:"measures"`
|
||||
MeasureType string `json:"measure_type"`
|
||||
RiskOut GTRisk `json:"risk_out"`
|
||||
NormReferences []string `json:"norm_references"`
|
||||
Sufficient bool `json:"sufficient"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Benchmark result types — comparison output
|
||||
// ============================================================================
|
||||
|
||||
// BenchmarkResult is the API response for the comparison endpoint.
|
||||
type BenchmarkResult struct {
|
||||
CoverageScore float64 `json:"coverage_score"`
|
||||
MeasureCoverage float64 `json:"measure_coverage"`
|
||||
TotalGT int `json:"total_gt"`
|
||||
TotalEngine int `json:"total_engine"`
|
||||
MatchedPairs []HazardMatchPair `json:"matched_pairs"`
|
||||
MissingFromEngine []GroundTruthEntry `json:"missing_from_engine"`
|
||||
ExtraInEngine []HazardSummary `json:"extra_in_engine"`
|
||||
CategoryBreakdown []CategoryScore `json:"category_breakdown"`
|
||||
RiskRankPairs []RiskRankPair `json:"risk_rank_pairs"`
|
||||
}
|
||||
|
||||
// HazardMatchPair links a GT entry to an engine hazard.
|
||||
type HazardMatchPair struct {
|
||||
GTEntry GroundTruthEntry `json:"gt_entry"`
|
||||
EngineHazard HazardSummary `json:"engine_hazard"`
|
||||
MatchScore float64 `json:"match_score"`
|
||||
MatchReason string `json:"match_reason"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
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.
|
||||
type CategoryScore struct {
|
||||
Category string `json:"category"`
|
||||
GTCount int `json:"gt_count"`
|
||||
MatchCount int `json:"match_count"`
|
||||
Coverage float64 `json:"coverage"`
|
||||
}
|
||||
|
||||
// RiskRankPair compares risk ordering between GT and engine.
|
||||
type RiskRankPair struct {
|
||||
GTRank int `json:"gt_rank"`
|
||||
EngineRank int `json:"engine_rank"`
|
||||
HazardName string `json:"hazard_name"`
|
||||
GTRiskScore int `json:"gt_risk_score"`
|
||||
EngineRisk float64 `json:"engine_risk"`
|
||||
}
|
||||
|
||||
// ParseGroundTruth extracts GroundTruth from project metadata JSON.
|
||||
func ParseGroundTruth(metadata json.RawMessage) (*GroundTruth, error) {
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(metadata, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, ok := m["ground_truth"]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
var gt GroundTruth
|
||||
if err := json.Unmarshal(raw, >); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return >, nil
|
||||
}
|
||||
@@ -50,6 +50,10 @@ func (e *DocumentExporter) ExportPDF(
|
||||
pdf.AddPage()
|
||||
e.pdfCoverPage(pdf, project)
|
||||
|
||||
// --- Methodology ("Erklaerteil") ---
|
||||
pdf.AddPage()
|
||||
e.pdfMethodologySection(pdf)
|
||||
|
||||
// --- Table of Contents ---
|
||||
pdf.AddPage()
|
||||
e.pdfTableOfContents(pdf, sections)
|
||||
@@ -127,6 +131,11 @@ func (e *DocumentExporter) ExportMarkdown(
|
||||
buf.WriteString(fmt.Sprintf("> %s\n\n", project.Description))
|
||||
}
|
||||
|
||||
buf.WriteString("---\n\n")
|
||||
buf.WriteString(fmt.Sprintf("## %s\n\n", RiskAssessmentMethodologySectionTitle))
|
||||
buf.WriteString(RiskAssessmentMethodologyDE)
|
||||
buf.WriteString("\n\n---\n\n")
|
||||
|
||||
for _, section := range sections {
|
||||
buf.WriteString(fmt.Sprintf("## %s\n\n", section.Title))
|
||||
buf.WriteString(fmt.Sprintf("*Typ: %s | Status: %s | Version: %d*\n\n",
|
||||
|
||||
@@ -2,6 +2,7 @@ package iace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jung-kurt/gofpdf"
|
||||
@@ -49,6 +50,31 @@ func (e *DocumentExporter) pdfCoverPage(pdf *gofpdf.Fpdf, project *Project) {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DocumentExporter) pdfMethodologySection(pdf *gofpdf.Fpdf) {
|
||||
paragraphs := strings.Split(RiskAssessmentMethodologyDE, "\n\n")
|
||||
for _, para := range paragraphs {
|
||||
para = strings.TrimSpace(para)
|
||||
if para == "" {
|
||||
continue
|
||||
}
|
||||
// Headings: lines that are short and don't end with punctuation
|
||||
if len(para) < 60 && !strings.HasSuffix(para, ".") && !strings.HasSuffix(para, ")") {
|
||||
pdf.Ln(4)
|
||||
pdf.SetFont("Helvetica", "B", 12)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
pdf.CellFormat(0, 8, para, "", 1, "L", false, 0, "")
|
||||
pdf.SetTextColor(0, 0, 0)
|
||||
pdf.SetDrawColor(200, 200, 200)
|
||||
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
|
||||
pdf.Ln(3)
|
||||
continue
|
||||
}
|
||||
pdf.SetFont("Helvetica", "", 10)
|
||||
pdf.MultiCell(0, 5, para, "", "L", false)
|
||||
pdf.Ln(2)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DocumentExporter) pdfTableOfContents(pdf *gofpdf.Fpdf, sections []TechFileSection) {
|
||||
pdf.SetFont("Helvetica", "B", 16)
|
||||
pdf.SetTextColor(50, 50, 50)
|
||||
@@ -61,6 +87,7 @@ func (e *DocumentExporter) pdfTableOfContents(pdf *gofpdf.Fpdf, sections []TechF
|
||||
pdf.SetFont("Helvetica", "", 11)
|
||||
|
||||
fixedEntries := []string{
|
||||
RiskAssessmentMethodologySectionTitle,
|
||||
"Gefaehrdungsprotokoll",
|
||||
"Risikomatrix-Zusammenfassung",
|
||||
"Massnahmen-Uebersicht",
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package iace
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// FMEAExportRow represents one row in the VDA FMEA worksheet.
|
||||
type FMEAExportRow struct {
|
||||
ComponentName string `json:"component_name"`
|
||||
ComponentType string `json:"component_type"`
|
||||
FailureMode string `json:"failure_mode"`
|
||||
FailureEffect string `json:"failure_effect"`
|
||||
FailureCause string `json:"failure_cause"`
|
||||
Severity int `json:"severity"`
|
||||
Occurrence int `json:"occurrence"`
|
||||
Detection int `json:"detection"`
|
||||
RPZ int `json:"rpz"`
|
||||
AP string `json:"ap"`
|
||||
Measure string `json:"measure"`
|
||||
DetectionHint string `json:"detection_hint"`
|
||||
}
|
||||
|
||||
// GenerateFMEAExcel creates a VDA-format FMEA worksheet as xlsx bytes.
|
||||
func GenerateFMEAExcel(projectName string, rows []FMEAExportRow) ([]byte, error) {
|
||||
f := excelize.NewFile()
|
||||
sheet := "FMEA-Worksheet"
|
||||
f.SetSheetName("Sheet1", sheet)
|
||||
|
||||
// Column widths
|
||||
widths := map[string]float64{
|
||||
"A": 6, "B": 25, "C": 14, "D": 28, "E": 30, "F": 28,
|
||||
"G": 6, "H": 6, "I": 6, "J": 8, "K": 6, "L": 30, "M": 25,
|
||||
}
|
||||
for col, w := range widths {
|
||||
_ = f.SetColWidth(sheet, col, col, w)
|
||||
}
|
||||
|
||||
// Header style
|
||||
headerStyle, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"},
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true},
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "000000", Style: 1}, {Type: "right", Color: "000000", Style: 1},
|
||||
{Type: "top", Color: "000000", Style: 1}, {Type: "bottom", Color: "000000", Style: 1},
|
||||
},
|
||||
})
|
||||
|
||||
// Title row
|
||||
f.SetCellValue(sheet, "A1", fmt.Sprintf("FMEA-Worksheet — %s", projectName))
|
||||
titleStyle, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Size: 14},
|
||||
})
|
||||
f.SetCellStyle(sheet, "A1", "A1", titleStyle)
|
||||
f.MergeCell(sheet, "A1", "M1")
|
||||
|
||||
// Sub-header
|
||||
f.SetCellValue(sheet, "A2", "AIAG-VDA FMEA Format | AP = Action Priority (H/M/L)")
|
||||
f.MergeCell(sheet, "A2", "M2")
|
||||
|
||||
// Column headers (row 4)
|
||||
headers := []string{"Nr.", "Komponente", "Typ", "Fehlerart", "Fehlerfolge", "Fehlerursache",
|
||||
"S", "O", "D", "RPZ", "AP", "Empfohlene Massnahme", "Erkennung"}
|
||||
for i, h := range headers {
|
||||
cell := fmt.Sprintf("%s4", string(rune('A'+i)))
|
||||
f.SetCellValue(sheet, cell, h)
|
||||
f.SetCellStyle(sheet, cell, cell, headerStyle)
|
||||
}
|
||||
|
||||
// Data rows
|
||||
dataStyle, _ := f.NewStyle(&excelize.Style{
|
||||
Alignment: &excelize.Alignment{WrapText: true, Vertical: "top"},
|
||||
Border: []excelize.Border{
|
||||
{Type: "left", Color: "D0D0D0", Style: 1}, {Type: "right", Color: "D0D0D0", Style: 1},
|
||||
{Type: "bottom", Color: "D0D0D0", Style: 1},
|
||||
},
|
||||
})
|
||||
apHigh, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Color: "FFFFFF"},
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FF0000"}},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center"},
|
||||
})
|
||||
apMed, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{Bold: true},
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FFD700"}},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center"},
|
||||
})
|
||||
apLow, _ := f.NewStyle(&excelize.Style{
|
||||
Font: &excelize.Font{Bold: true, Color: "FFFFFF"},
|
||||
Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"00B050"}},
|
||||
Alignment: &excelize.Alignment{Horizontal: "center"},
|
||||
})
|
||||
|
||||
for i, row := range rows {
|
||||
r := i + 5 // data starts at row 5
|
||||
f.SetCellValue(sheet, fmt.Sprintf("A%d", r), i+1)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("B%d", r), row.ComponentName)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("C%d", r), row.ComponentType)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("D%d", r), row.FailureMode)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("E%d", r), row.FailureEffect)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("F%d", r), row.FailureCause)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("G%d", r), row.Severity)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("H%d", r), row.Occurrence)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("I%d", r), row.Detection)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("J%d", r), row.RPZ)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("K%d", r), row.AP)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("L%d", r), row.Measure)
|
||||
f.SetCellValue(sheet, fmt.Sprintf("M%d", r), row.DetectionHint)
|
||||
|
||||
// Style data cells
|
||||
for c := 0; c < 13; c++ {
|
||||
cell := fmt.Sprintf("%s%d", string(rune('A'+c)), r)
|
||||
f.SetCellStyle(sheet, cell, cell, dataStyle)
|
||||
}
|
||||
// AP color
|
||||
apCell := fmt.Sprintf("K%d", r)
|
||||
switch row.AP {
|
||||
case "H":
|
||||
f.SetCellStyle(sheet, apCell, apCell, apHigh)
|
||||
case "M":
|
||||
f.SetCellStyle(sheet, apCell, apCell, apMed)
|
||||
case "L":
|
||||
f.SetCellStyle(sheet, apCell, apCell, apLow)
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("excel write: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package iace
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// HazardBlock groups related hazards under a parent hazard.
|
||||
// The parent is the hazard with the highest inherent risk in the group.
|
||||
// Child hazards are covered by the same or similar protective measures.
|
||||
type HazardBlock struct {
|
||||
ParentHazard HazardBlockEntry `json:"parent_hazard"`
|
||||
Children []HazardBlockEntry `json:"children"`
|
||||
BlockKey string `json:"block_key"`
|
||||
SharedMeasureCount int `json:"shared_measure_count"`
|
||||
// If true, the parent's measures cover all children → children
|
||||
// don't need individual risk assessment.
|
||||
ChildrenCoveredByParent bool `json:"children_covered_by_parent"`
|
||||
}
|
||||
|
||||
// HazardBlockEntry is a hazard with its assessment and linked measures.
|
||||
type HazardBlockEntry struct {
|
||||
Hazard Hazard `json:"hazard"`
|
||||
Assessment *RiskAssessment `json:"assessment,omitempty"`
|
||||
MitigationIDs []uuid.UUID `json:"mitigation_ids"`
|
||||
}
|
||||
|
||||
// ComputeHazardBlocks groups hazards into blocks based on category + component.
|
||||
// Within each block, the hazard with the highest risk becomes the parent.
|
||||
// Children whose measures are a subset of the parent's measures are marked as covered.
|
||||
func ComputeHazardBlocks(
|
||||
hazards []Hazard,
|
||||
assessments []RiskAssessment,
|
||||
mitigations []Mitigation,
|
||||
) []HazardBlock {
|
||||
if len(hazards) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build assessment lookup: hazard_id → latest assessment
|
||||
assessMap := make(map[uuid.UUID]*RiskAssessment)
|
||||
for i := range assessments {
|
||||
a := &assessments[i]
|
||||
if existing, ok := assessMap[a.HazardID]; !ok || a.Version > existing.Version {
|
||||
assessMap[a.HazardID] = a
|
||||
}
|
||||
}
|
||||
|
||||
// Build mitigation lookup: hazard_id → []mitigation_ids
|
||||
mitsByHazard := make(map[uuid.UUID][]uuid.UUID)
|
||||
for _, m := range mitigations {
|
||||
mitsByHazard[m.HazardID] = append(mitsByHazard[m.HazardID], m.ID)
|
||||
}
|
||||
|
||||
// Group by blockKey = category + ":" + componentID
|
||||
groups := make(map[string][]HazardBlockEntry)
|
||||
for _, h := range hazards {
|
||||
key := buildBlockKey(h)
|
||||
entry := HazardBlockEntry{
|
||||
Hazard: h,
|
||||
Assessment: assessMap[h.ID],
|
||||
MitigationIDs: mitsByHazard[h.ID],
|
||||
}
|
||||
groups[key] = append(groups[key], entry)
|
||||
}
|
||||
|
||||
// Build blocks: sort each group by risk, first is parent
|
||||
var blocks []HazardBlock
|
||||
for key, entries := range groups {
|
||||
sortByRiskDesc(entries, assessMap)
|
||||
|
||||
parent := entries[0]
|
||||
children := entries[1:]
|
||||
|
||||
// Check if parent's measures cover children
|
||||
parentMitSet := toUUIDSet(parent.MitigationIDs)
|
||||
allCovered := true
|
||||
for _, child := range children {
|
||||
if !mitigationsCoveredBy(child, parent, mitigations) {
|
||||
allCovered = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
block := HazardBlock{
|
||||
ParentHazard: parent,
|
||||
Children: children,
|
||||
BlockKey: key,
|
||||
SharedMeasureCount: len(parentMitSet),
|
||||
ChildrenCoveredByParent: allCovered && len(children) > 0,
|
||||
}
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
|
||||
// Sort blocks: largest (most children) first, then by parent risk
|
||||
sort.Slice(blocks, func(i, j int) bool {
|
||||
ri := inherentRisk(blocks[i].ParentHazard, assessMap)
|
||||
rj := inherentRisk(blocks[j].ParentHazard, assessMap)
|
||||
if len(blocks[i].Children) != len(blocks[j].Children) {
|
||||
return len(blocks[i].Children) > len(blocks[j].Children)
|
||||
}
|
||||
return ri > rj
|
||||
})
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
func buildBlockKey(h Hazard) string {
|
||||
// Group by category + component. Hazards at the same component in the
|
||||
// same category form one block — the zone is typically different but the
|
||||
// protective measures (e.g. Schutzzaun, Sicherheitszuhaltung) are shared.
|
||||
return h.Category + ":" + h.ComponentID.String()
|
||||
}
|
||||
|
||||
func sortByRiskDesc(entries []HazardBlockEntry, assessMap map[uuid.UUID]*RiskAssessment) {
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
ri := inherentRisk(entries[i], assessMap)
|
||||
rj := inherentRisk(entries[j], assessMap)
|
||||
return ri > rj
|
||||
})
|
||||
}
|
||||
|
||||
func inherentRisk(entry HazardBlockEntry, assessMap map[uuid.UUID]*RiskAssessment) float64 {
|
||||
if entry.Assessment != nil {
|
||||
return entry.Assessment.InherentRisk
|
||||
}
|
||||
if a, ok := assessMap[entry.Hazard.ID]; ok {
|
||||
return a.InherentRisk
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// mitigationsCoveredBy checks if child's measures are functionally covered
|
||||
// by parent's measures (same reduction type and hazard category).
|
||||
func mitigationsCoveredBy(child, parent HazardBlockEntry, allMits []Mitigation) bool {
|
||||
if len(child.MitigationIDs) == 0 {
|
||||
return true // No measures needed → covered by default
|
||||
}
|
||||
|
||||
mitMap := make(map[uuid.UUID]Mitigation)
|
||||
for _, m := range allMits {
|
||||
mitMap[m.ID] = m
|
||||
}
|
||||
|
||||
// Check: for each child mitigation type, parent has same type
|
||||
parentTypes := make(map[ReductionType]bool)
|
||||
for _, mid := range parent.MitigationIDs {
|
||||
if m, ok := mitMap[mid]; ok {
|
||||
parentTypes[m.ReductionType] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, mid := range child.MitigationIDs {
|
||||
if m, ok := mitMap[mid]; ok {
|
||||
if !parentTypes[m.ReductionType] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func toUUIDSet(ids []uuid.UUID) map[uuid.UUID]bool {
|
||||
s := make(map[uuid.UUID]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
s[id] = true
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -54,6 +54,10 @@ type HazardPattern struct {
|
||||
// of the listed failure modes is relevant (by ComponentType match against project components).
|
||||
// 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",
|
||||
|
||||
@@ -10,6 +10,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP400", NameDE: "Umkippen Bagger bei Grabungsarbeiten", NameEN: "Excavator tipping during digging",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -25,6 +26,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP401", NameDE: "Verschuettung — Grabenrand bricht ein", NameEN: "Burial — trench wall collapses",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"structural_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -40,6 +42,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP402", NameDE: "Quetschen zwischen Baggerarm und Fahrerkabine", NameEN: "Crushing between excavator arm and cab",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -55,6 +58,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP403", NameDE: "Ueberfahren von Personen durch Radlader", NameEN: "Running over persons by wheel loader",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"moving_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -74,6 +78,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP404", NameDE: "Absturz von Betonpumpenausleger", NameEN: "Collapse of concrete pump boom",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -91,6 +96,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP405", NameDE: "Druckversagen Betonpumpe", NameEN: "Pressure failure of concrete pump",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "high_pressure"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"pneumatic_hydraulic"},
|
||||
@@ -106,6 +112,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP406", NameDE: "Heisser Asphalt — Verbrennungsgefahr", NameEN: "Hot asphalt — burn hazard",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"high_temperature", "chemical_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
@@ -125,6 +132,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP407", NameDE: "Herabfallen von Tunneldecke (Vortrieb)", NameEN: "Tunnel roof collapse during boring",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"structural_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -140,6 +148,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP408", NameDE: "Quetschen bei Ramme/Bohrgeraet", NameEN: "Crushing at pile driver/drilling rig",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -155,6 +164,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP409", NameDE: "Steinschlag bei Tunnelvortrieb", NameEN: "Rockfall during tunnel excavation",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"gravity_risk"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -174,6 +184,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP410", NameDE: "Walze ueberrollt Person (Strassenbau)", NameEN: "Road roller runs over person",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"moving_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -189,6 +200,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP411", NameDE: "Laerm und Vibration bei Rammarbeiten", NameEN: "Noise and vibration during pile driving",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"vibration_source"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"ergonomic"},
|
||||
@@ -208,6 +220,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP412", NameDE: "Kippen von Mobilkran bei Ueberreichweite", NameEN: "Mobile crane tipping at over-reach",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -225,6 +238,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP413", NameDE: "Seilbruch am Kran", NameEN: "Crane wire rope failure",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"gravity_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -242,6 +256,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP414", NameDE: "Pendelnde Last trifft Person", NameEN: "Swinging load strikes person",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"gravity_risk", "moving_part"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -257,6 +272,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP415", NameDE: "Kollision zweier Krane", NameEN: "Collision of two cranes in overlapping work areas",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"moving_part", "structural_part"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -276,6 +292,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP416", NameDE: "Absturz Hubarbeitsbuehne bei Wind", NameEN: "Aerial work platform overturning in wind",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -291,6 +308,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP417", NameDE: "Quetschen beim Containerumschlag", NameEN: "Crushing during container handling",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"moving_part", "crush_point", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -306,6 +324,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP418", NameDE: "Herabfallen Fassadengeruest", NameEN: "Facade scaffolding collapse",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"structural_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -321,6 +340,7 @@ func GetConstructionPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP419", NameDE: "Absturz von Fahrtreppen bei Stufe-Ketten-Bruch", NameEN: "Escalator fall due to step chain failure",
|
||||
MachineTypes: []string{"construction", "crane", "excavator"},
|
||||
RequiredComponentTags: []string{"moving_part", "structural_part"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,6 +10,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP174", NameDE: "Einschluss in Schachtgrube", NameEN: "Entrapment in pit",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"gravity_risk", "structural_part", "elevator_shaft"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -25,6 +26,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP175", NameDE: "Quetschen im Tuerspalt", NameEN: "Crushing in door gap",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_door", "pinch_point", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -40,6 +42,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP176", NameDE: "Absturzgefahr bei offenem Schacht", NameEN: "Fall hazard at open shaft",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_shaft", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
ExcludedComponentTags: []string{"safety_device"},
|
||||
@@ -56,6 +59,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP177", NameDE: "Pufferversagen — Aufprall in Endlage", NameEN: "Buffer failure — impact at terminal",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_shaft", "gravity_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{"stored_energy"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -73,6 +77,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP178", NameDE: "Seilriss / Treibscheibenversagen", NameEN: "Rope break / traction sheave failure",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_traction", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -90,6 +95,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP179", NameDE: "Uebergeschwindigkeit des Fahrkorbs", NameEN: "Car overspeed",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_traction", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||
@@ -107,6 +113,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP180", NameDE: "Fehlende Notrufeinrichtung", NameEN: "Missing emergency call device",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_car", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
ExcludedComponentTags: []string{"emergency_comm"},
|
||||
@@ -123,6 +130,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP181", NameDE: "Brandfalle im Aufzugsschacht", NameEN: "Fire trap in elevator shaft",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_shaft", "structural_part"},
|
||||
RequiredEnergyTags: []string{"thermal"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "material_environmental"},
|
||||
@@ -138,6 +146,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP182", NameDE: "Quetschen zwischen Fahrkorb und Gegengewicht", NameEN: "Crushing between car and counterweight",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_traction", "counterweight", "pinch_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -153,6 +162,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP183", NameDE: "Elektrischer Schlag im Triebwerksraum", NameEN: "Electric shock in machine room",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_traction", "electrical_part"},
|
||||
RequiredEnergyTags: []string{"electrical"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
@@ -168,6 +178,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP184", NameDE: "Hydraulikversagen bei hydraulischem Aufzug", NameEN: "Hydraulic failure in hydraulic lift",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "elevator_car", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "pneumatic_hydraulic"},
|
||||
@@ -183,6 +194,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP185", NameDE: "Scherenstelle Fahrkorb / Schachtwand", NameEN: "Shearing between car and shaft wall",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_car", "shear_risk", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -198,6 +210,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP186", NameDE: "NOT-Befreiung durch Laien", NameEN: "Emergency rescue by untrained persons",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_car", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||
@@ -213,6 +226,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP187", NameDE: "Einklemmung im Tuermechanismus", NameEN: "Trapping in door mechanism",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_door", "pinch_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -228,6 +242,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP188", NameDE: "Ueberlast — Seilspannungsgrenze ueberschritten", NameEN: "Overload — rope tension limit exceeded",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_traction", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||
@@ -245,6 +260,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP189", NameDE: "Fahrkorb-Niveauversatz an Haltestelle", NameEN: "Car leveling offset at landing",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_car", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -260,6 +276,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP190", NameDE: "Quetschgefahr auf Fahrkorbdach", NameEN: "Crushing hazard on car roof",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_car", "pinch_point", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -275,6 +292,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP191", NameDE: "Unbeabsichtigte Fahrkorbewegung bei offener Tuer", NameEN: "Unintended car movement with door open",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_door", "elevator_traction", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "safety_function_failure"},
|
||||
@@ -292,6 +310,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP192", NameDE: "Vandalismus an Aufzugssteuerung", NameEN: "Vandalism on elevator controls",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_car", "programmable"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"safety_function_failure"},
|
||||
@@ -307,6 +326,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP193", NameDE: "Seismische Belastung Aufzugsanlage", NameEN: "Seismic load on elevator system",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_shaft", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -322,6 +342,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP194", NameDE: "Fangvorrichtung klemmt nicht", NameEN: "Safety gear does not engage",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_traction", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"safety_function_failure", "mechanical_hazard"},
|
||||
@@ -339,6 +360,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP195", NameDE: "Laermexposition Triebwerksraum", NameEN: "Noise exposure in machine room",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_traction", "noise_source"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"noise_vibration"},
|
||||
@@ -354,6 +376,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP196", NameDE: "Absturz in Schacht bei Fahrkorbdach-Arbeiten", NameEN: "Fall into shaft during car-top work",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_car", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -369,6 +392,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP197", NameDE: "Vergiftung durch Oel/Schmierstoffe im Schacht", NameEN: "Intoxication from oil/lubricants in shaft",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_shaft", "chemical_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -384,6 +408,7 @@ func GetElevatorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP198", NameDE: "Tuerschliessdruck zu hoch", NameEN: "Door closing force too high",
|
||||
MachineTypes: []string{"elevator", "lift", "escalator"},
|
||||
RequiredComponentTags: []string{"elevator_door", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,6 +6,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
return []HazardPattern{
|
||||
{
|
||||
ID: "HP300", NameDE: "Einzug in Fleischwolf", NameEN: "Draw-in at meat grinder",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"rotating_part", "cutting_part"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -21,6 +22,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP301", NameDE: "Verbrennungsgefahr am Backofen/Kochkessel", NameEN: "Burn hazard at oven/cooking kettle",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"high_temperature", "structural_part"},
|
||||
RequiredEnergyTags: []string{"thermal"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
@@ -36,6 +38,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP302", NameDE: "Kontamination durch mangelnde Hygiene", NameEN: "Contamination due to insufficient hygiene",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -51,6 +54,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP303", NameDE: "Schneidverletzung an Aufschnittmaschine", NameEN: "Cut injury at slicing machine",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"cutting_part", "rotating_part"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -66,6 +70,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP304", NameDE: "Dampfverbrennung beim Oeffnen des Druckkessels", NameEN: "Steam burn when opening pressure vessel",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"high_pressure", "high_temperature"},
|
||||
RequiredEnergyTags: []string{"thermal", "stored_energy"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "pneumatic_hydraulic"},
|
||||
@@ -84,6 +89,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP305", NameDE: "Quetschen in Teigknetmaschine", NameEN: "Crushing in dough kneading machine",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"rotating_part", "crush_point", "high_force"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -99,6 +105,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP306", NameDE: "Einzug in Walzenmuehle", NameEN: "Draw-in at roller mill",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"rotating_part", "entanglement_risk", "high_force"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -114,6 +121,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP307", NameDE: "Elektrischer Schlag in Nassumgebung (IP-Schutz)", NameEN: "Electric shock in wet environment (IP rating)",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"electrical_part"},
|
||||
RequiredEnergyTags: []string{"electrical_energy"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
@@ -123,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.",
|
||||
@@ -132,6 +140,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP308", NameDE: "Allergene Kreuzkontamination", NameEN: "Allergen cross-contamination",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"structural_part", "chemical_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -147,6 +156,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP309", NameDE: "Biologische Gefaehrdung (Bakterien, Schimmel)", NameEN: "Biological hazard (bacteria, mold)",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -162,6 +172,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP310", NameDE: "Quetschen durch Abfuellstempel", NameEN: "Crushing by filling piston",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"crush_point", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -177,6 +188,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP311", NameDE: "Sturz auf nassem/fettigem Boden", NameEN: "Slip on wet/greasy floor",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "ergonomic"},
|
||||
@@ -192,6 +204,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP312", NameDE: "Erstickungsgefahr in Gaerbehaelter/Silo", NameEN: "Asphyxiation in fermentation vessel/silo",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -207,6 +220,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP313", NameDE: "Veraetzung durch Reinigungsmittel (CIP)", NameEN: "Chemical burn from CIP cleaning agents",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"chemical_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -222,6 +236,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP314", NameDE: "Laerm durch Hochdruckreinigung", NameEN: "Noise from high-pressure cleaning",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"noise_source", "high_pressure"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"noise_vibration"},
|
||||
@@ -237,6 +252,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP315", NameDE: "Verbrennungsgefahr an Fritteuse/Heissoelbad", NameEN: "Burn hazard at deep fryer/hot oil bath",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"high_temperature"},
|
||||
RequiredEnergyTags: []string{"thermal"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
@@ -252,6 +268,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP316", NameDE: "Schnitt beim manuellen Messerwechsel", NameEN: "Cut during manual blade change",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"cutting_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
ExcludedComponentTags: []string{"safety_device"},
|
||||
@@ -268,6 +285,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP317", NameDE: "Explosion in Mehlstaubatmosphaere", NameEN: "Explosion in flour dust atmosphere",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -286,6 +304,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP318", NameDE: "Ergonomische Belastung bei manueller Portionierung", NameEN: "Ergonomic strain during manual portioning",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"ergonomic"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"ergonomic"},
|
||||
@@ -301,6 +320,7 @@ func GetFoodProcessingPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP319", NameDE: "Kaelteverletzung im Tiefkuehlbereich", NameEN: "Cold injury in deep-freeze area",
|
||||
MachineTypes: []string{"food_processing", "packaging"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "ergonomic"},
|
||||
|
||||
@@ -10,6 +10,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP420", NameDE: "Rueckschlag Kettensaege", NameEN: "Chainsaw kickback",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"cutting_part", "vibration_source"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -25,6 +26,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP421", NameDE: "Herabfallender Ast/Baum trifft Person", NameEN: "Falling branch/tree strikes person",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"gravity_risk"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -40,6 +42,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP422", NameDE: "Einzug in Hacker/Haecksler", NameEN: "Entanglement in wood chipper/shredder",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"rotating_part", "cutting_part"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -55,13 +58,14 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP423", NameDE: "Schnitt durch rotierendes Maehwerk", NameEN: "Cut by rotating mower blade",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"rotating_part", "cutting_part"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
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",
|
||||
@@ -70,6 +74,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP424", NameDE: "Hand-Arm-Vibration bei Kettensaege (Dauerschaden)", NameEN: "Hand-arm vibration from chainsaw (chronic damage)",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"vibration_source"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"ergonomic"},
|
||||
@@ -85,6 +90,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP425", NameDE: "Gehoerschaden bei Motorsaege", NameEN: "Hearing damage from chainsaw operation",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"vibration_source"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"ergonomic"},
|
||||
@@ -100,6 +106,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP426", NameDE: "Umkippen des Forstschleppers", NameEN: "Forestry skidder overturning",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"moving_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -115,6 +122,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP427", NameDE: "Quetschen durch Holzgreifer", NameEN: "Crushing by log grapple",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -130,6 +138,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP428", NameDE: "Splitterflug bei Holzbearbeitung", NameEN: "Flying splinters during wood processing",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"cutting_part", "high_speed"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -145,6 +154,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP429", NameDE: "Vergiftung durch Abgase Zweitaktmotor", NameEN: "Poisoning by two-stroke engine exhaust",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"chemical_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -160,6 +170,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP430", NameDE: "Allergische Reaktion auf Pflanzenschutz", NameEN: "Allergic reaction to pesticide",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"chemical_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -179,6 +190,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP431", NameDE: "Einzug an Bandumlenkung", NameEN: "Entanglement at belt conveyor deflection point",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"rotating_part", "entanglement_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -194,6 +206,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP432", NameDE: "Herabfallen von Stueckgut vom Foerderband", NameEN: "Falling unit load from conveyor belt",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"gravity_risk", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -209,6 +222,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP433", NameDE: "Quetschen zwischen Foerderband und Rahmen", NameEN: "Crushing between conveyor belt and frame",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"moving_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -224,6 +238,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP434", NameDE: "Materialstau mit ploetzlicher Freisetzung", NameEN: "Material blockage with sudden release",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"moving_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -239,6 +254,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP435", NameDE: "Staubexplosion in Schuettgutfoerderung (Silo)", NameEN: "Dust explosion in bulk material handling (silo)",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"chemical_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"fire_explosion"},
|
||||
@@ -256,6 +272,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP436", NameDE: "Vergiftung in Silozelle (Gaerungsgase)", NameEN: "Poisoning in silo cell (fermentation gases)",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"chemical_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -271,6 +288,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP437", NameDE: "Einzug in Schneckenfoerderer", NameEN: "Entanglement in screw conveyor",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"rotating_part", "entanglement_risk"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -286,13 +304,14 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP438", NameDE: "Absturz von Rollenfoerderer (Erhoehung)", NameEN: "Fall from elevated roller conveyor",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"structural_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
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",
|
||||
@@ -301,6 +320,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP439", NameDE: "Quetschen durch Hubwerk/Scherenhubtisch", NameEN: "Crushing by lift table/scissor lift mechanism",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -316,6 +336,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP440", NameDE: "Einklemmen am Drehteller", NameEN: "Trapping at rotary turntable",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"rotating_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{"rotational"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -331,6 +352,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP441", NameDE: "Kollision FTS mit Foerderanlage", NameEN: "AGV collision with conveyor system",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"moving_part"},
|
||||
RequiredEnergyTags: []string{"kinetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -346,6 +368,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP442", NameDE: "Herabfallen von Paletten-Stapel", NameEN: "Pallet stack collapse",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"gravity_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -361,6 +384,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP443", NameDE: "Quetschen an Verladebruecke", NameEN: "Crushing at loading dock leveler",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -376,6 +400,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP444", NameDE: "Ladebuehne senkt sich unkontrolliert", NameEN: "Loading dock platform drops uncontrolled",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"hydraulic_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -391,6 +416,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP445", NameDE: "Quetschen durch Industrietor", NameEN: "Crushing by industrial door",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"moving_part", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -406,6 +432,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP446", NameDE: "Einzug in Rolltor", NameEN: "Entanglement in roller shutter door",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"moving_part", "entanglement_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -421,6 +448,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP447", NameDE: "Absturz durch offene Schachtgrube", NameEN: "Fall through open shaft pit",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"structural_part", "gravity_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -436,6 +464,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP448", NameDE: "Stolpern ueber Schienen von Verschiebeanlage", NameEN: "Tripping over rails of transfer system",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -451,6 +480,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP449", NameDE: "Brand in Absauganlage (Holzstaub)", NameEN: "Fire in extraction system (wood dust)",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"chemical_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"fire_explosion"},
|
||||
@@ -468,6 +498,7 @@ func GetForestryConveyorPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP450", NameDE: "Explosion in Mehlsilo", NameEN: "Explosion in flour silo",
|
||||
MachineTypes: []string{"forestry", "conveyor"},
|
||||
RequiredComponentTags: []string{"chemical_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"fire_explosion"},
|
||||
|
||||
@@ -5,6 +5,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
return []HazardPattern{
|
||||
{
|
||||
ID: "HP335", NameDE: "Augenverletzung durch Laserstrahlung (Klasse 3B/4)", NameEN: "Eye injury from laser radiation (Class 3B/4)",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -23,6 +24,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP336", NameDE: "Hautverbrennung durch Laserstrahl", NameEN: "Skin burn from laser beam",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
@@ -38,6 +40,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP337", NameDE: "Brand durch Laserstrahl auf brennbarem Material", NameEN: "Fire from laser beam on combustible material",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk", "high_temperature"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "material_environmental"},
|
||||
@@ -53,6 +56,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP338", NameDE: "Reflexion — Strahl trifft unkontrolliert Person", NameEN: "Reflection — beam hits person uncontrolled",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -68,6 +72,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP339", NameDE: "Rauchgas bei Laserschneiden (Metalldaempfe)", NameEN: "Fumes during laser cutting (metal vapors)",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk", "chemical_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -83,6 +88,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP340", NameDE: "Explosionsgefahr bei Laser + brennbare Atmosphaere", NameEN: "Explosion hazard laser + combustible atmosphere",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -101,6 +107,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP341", NameDE: "Quetschen durch CNC-Achsen der Laseranlage", NameEN: "Crushing by CNC axes of laser system",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"moving_part", "high_force", "crush_point"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -116,6 +123,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP342", NameDE: "Blendung durch Streulicht", NameEN: "Glare from stray light",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -131,6 +139,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP343", NameDE: "Elektroschock an Laserquelle (Hochspannung)", NameEN: "Electric shock at laser source (high voltage)",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"electrical_part", "high_voltage"},
|
||||
RequiredEnergyTags: []string{"electrical_energy"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
@@ -149,6 +158,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP344", NameDE: "UV-Strahlung bei bestimmten Lasertypen", NameEN: "UV radiation from certain laser types",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -164,6 +174,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP345", NameDE: "Schutzgaserstickung in Laserkabine", NameEN: "Inert gas asphyxiation in laser cabin",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"structural_part", "chemical_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -179,6 +190,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP346", NameDE: "Fokussierlinsenverschmutzung verursacht Rueckreflex", NameEN: "Focusing lens contamination causes back-reflection",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "material_environmental"},
|
||||
@@ -194,6 +206,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP347", NameDE: "Laserstrahl-Austritt bei defekter Einhausung", NameEN: "Laser beam escape from defective enclosure",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk", "interlocked"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental", "safety_function_failure"},
|
||||
@@ -209,6 +222,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP348", NameDE: "Laermbelastung durch Laserschneidprozess", NameEN: "Noise exposure from laser cutting process",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"noise_source", "radiation_risk"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"noise_vibration"},
|
||||
@@ -224,6 +238,7 @@ func GetLaserPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP349", NameDE: "Restenergie in Lasermedium nach Abschaltung", NameEN: "Residual energy in laser medium after shutdown",
|
||||
MachineTypes: []string{"medical_device", "laser_device"},
|
||||
RequiredComponentTags: []string{"radiation_risk", "stored_energy"},
|
||||
RequiredEnergyTags: []string{"stored_energy"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard", "material_environmental"},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
return []HazardPattern{
|
||||
{
|
||||
ID: "HP350", NameDE: "Elektrischer Schlag am Patienten (Ableitstrom)", NameEN: "Electric shock to patient (leakage current)",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"electrical_part"},
|
||||
RequiredEnergyTags: []string{"electrical_energy"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
@@ -24,6 +25,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP351", NameDE: "Fehlfunktion des Defibrillators", NameEN: "Defibrillator malfunction",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"electrical_part", "has_software"},
|
||||
RequiredEnergyTags: []string{"electrical_energy", "stored_energy"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard", "safety_function_failure"},
|
||||
@@ -42,6 +44,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP352", NameDE: "Ueberhitzung durch HF-Chirurgiegeraet", NameEN: "Overheating by HF surgical device",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"electrical_part", "high_temperature"},
|
||||
RequiredEnergyTags: []string{"electrical_energy", "thermal"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "electrical_hazard"},
|
||||
@@ -57,6 +60,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP353", NameDE: "Strahlenexposition am CT-Scanner", NameEN: "Radiation exposure at CT scanner",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"radiation_risk"},
|
||||
RequiredEnergyTags: []string{"radiation"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -75,6 +79,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP354", NameDE: "Fehlalarm fuehrt zu falscher Behandlung", NameEN: "False alarm leads to wrong treatment",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"sensor_part", "has_software"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"sensor_fault", "software_fault"},
|
||||
@@ -90,6 +95,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP355", NameDE: "Infektionsgefahr durch mangelhafte Sterilisation", NameEN: "Infection risk from insufficient sterilization",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -105,6 +111,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP356", NameDE: "Mechanisches Versagen des OP-Tischs", NameEN: "Mechanical failure of operating table",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"moving_part", "hydraulic_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -120,6 +127,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP357", NameDE: "EMV-Stoerung anderer Geraete", NameEN: "EMC interference with other devices",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"electrical_part", "networked"},
|
||||
RequiredEnergyTags: []string{"electromagnetic"},
|
||||
GeneratedHazardCats: []string{"emc_hazard", "safety_function_failure"},
|
||||
@@ -135,6 +143,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP358", NameDE: "Softwarefehler in Dosierungssystem", NameEN: "Software error in dosing system",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"has_software", "programmable"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"software_fault", "safety_function_failure"},
|
||||
@@ -153,6 +162,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP359", NameDE: "Patientenfall vom Krankenbett", NameEN: "Patient fall from hospital bed",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"gravity_risk", "moving_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
@@ -168,6 +178,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP360", NameDE: "Ueberhitzung tragbarer Geraetebatterie", NameEN: "Overheating of portable device battery",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"electrical_part"},
|
||||
RequiredEnergyTags: []string{"stored_energy", "thermal"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "electrical_hazard"},
|
||||
@@ -183,6 +194,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP361", NameDE: "Fehlerhafte Anzeige am Patientenmonitor", NameEN: "Erroneous display on patient monitor",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"user_interface", "has_software"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"hmi_error", "software_fault"},
|
||||
@@ -198,6 +210,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP362", NameDE: "Bewegungseinschraenkung in MRT-Roehre", NameEN: "Movement restriction in MRI bore",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{"electromagnetic"},
|
||||
GeneratedHazardCats: []string{"ergonomic", "material_environmental"},
|
||||
@@ -213,6 +226,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP363", NameDE: "Ferromagnetischer Gegenstand als MRT-Projektil", NameEN: "Ferromagnetic object as MRI projectile",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{"electromagnetic"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard", "material_environmental"},
|
||||
@@ -228,6 +242,7 @@ func GetMedicalDevicePatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP364", NameDE: "Quenchgefahr bei supraleitendem MRT-Magnet", NameEN: "Quench hazard at superconducting MRI magnet",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"structural_part"},
|
||||
RequiredEnergyTags: []string{"electromagnetic", "stored_energy"},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -250,6 +265,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
return []HazardPattern{
|
||||
{
|
||||
ID: "HP365", NameDE: "Bersten eines Druckbehaelters", NameEN: "Bursting of a pressure vessel",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_pressure", "structural_part"},
|
||||
RequiredEnergyTags: []string{"stored_energy"},
|
||||
GeneratedHazardCats: []string{"pneumatic_hydraulic"},
|
||||
@@ -268,6 +284,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP366", NameDE: "Dampfaustritt an undichter Flanschverbindung", NameEN: "Steam leak at flanged joint",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_pressure", "high_temperature"},
|
||||
RequiredEnergyTags: []string{"thermal", "stored_energy"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard", "pneumatic_hydraulic"},
|
||||
@@ -283,6 +300,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP367", NameDE: "Sicherheitsventil oeffnet nicht", NameEN: "Safety valve fails to open",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_pressure"},
|
||||
RequiredEnergyTags: []string{"stored_energy"},
|
||||
GeneratedHazardCats: []string{"safety_function_failure", "pneumatic_hydraulic"},
|
||||
@@ -301,6 +319,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP368", NameDE: "Druckstoss (Wasserschlag) in Rohrleitung", NameEN: "Pressure surge (water hammer) in pipeline",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_pressure"},
|
||||
RequiredEnergyTags: []string{"stored_energy"},
|
||||
GeneratedHazardCats: []string{"pneumatic_hydraulic", "mechanical_hazard"},
|
||||
@@ -316,6 +335,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP369", NameDE: "Korrosionsversagen unter Isolierung", NameEN: "Corrosion under insulation failure",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_pressure", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental", "pneumatic_hydraulic"},
|
||||
@@ -331,6 +351,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP370", NameDE: "Verbrennungsgefahr an heisser Dampfleitung", NameEN: "Burn hazard at hot steam pipe",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_temperature", "structural_part"},
|
||||
RequiredEnergyTags: []string{"thermal"},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
@@ -346,6 +367,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP371", NameDE: "Erstickungsgefahr durch Inertgas-Austritt", NameEN: "Asphyxiation from inert gas release",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"chemical_risk", "structural_part"},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
@@ -361,6 +383,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP372", NameDE: "Ueberdruckversagen Waermetauscher", NameEN: "Overpressure failure of heat exchanger",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_pressure", "high_temperature"},
|
||||
RequiredEnergyTags: []string{"thermal", "stored_energy"},
|
||||
GeneratedHazardCats: []string{"pneumatic_hydraulic", "thermal_hazard"},
|
||||
@@ -379,6 +402,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP373", NameDE: "Druckluft-Hautinjektion", NameEN: "Compressed air skin injection",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"pneumatic_part", "high_pressure"},
|
||||
RequiredEnergyTags: []string{"pneumatic_pressure"},
|
||||
GeneratedHazardCats: []string{"pneumatic_hydraulic"},
|
||||
@@ -394,6 +418,7 @@ func GetPressureEquipmentPatterns() []HazardPattern {
|
||||
},
|
||||
{
|
||||
ID: "HP374", NameDE: "Schnellschluss-Ventilversagen bei Druckentlastung", NameEN: "Fast-closing valve failure during pressure relief",
|
||||
MachineTypes: []string{"medical_device", "infusion_pump", "ventilator", "patient_monitor"},
|
||||
RequiredComponentTags: []string{"high_pressure", "actuator_part"},
|
||||
RequiredEnergyTags: []string{"stored_energy"},
|
||||
GeneratedHazardCats: []string{"safety_function_failure", "pneumatic_hydraulic"},
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
package iace
|
||||
|
||||
// GetRobotCellPatterns returns hazard patterns for industrial robot cells
|
||||
// (non-collaborative) with safety fence, conveyors, and CNC machine tools.
|
||||
// Based on typical ISO 10218-2 risk assessment scope for integrated robot systems.
|
||||
//
|
||||
// FORMULIERUNGSREGEL: Gefährdung und Szenario NEUTRAL formulieren — keine
|
||||
// Lebensphasen im Text. Lebensphasen stehen in ApplicableLifecycles.
|
||||
// HP1600-HP1649
|
||||
func GetRobotCellPatterns() []HazardPattern {
|
||||
return []HazardPattern{
|
||||
// ================================================================
|
||||
// Roboterarm — Quetschen/Einklemmen von Personen
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1600", NameDE: "Einklemmen zwischen Roboterarm und Anlage", NameEN: "Crushing between robot arm and fixed structure",
|
||||
RequiredComponentTags: []string{"moving_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M062", "M054"},
|
||||
Priority: 99, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "teach_mode", "cleaning", "maintenance", "fault_clearing", "changeover"},
|
||||
ScenarioDE: "Person befindet sich im Bewegungsbereich des Roboterarms und wird zwischen Roboterarm und feststehenden Anlagenteilen eingeklemmt.",
|
||||
TriggerDE: "Roboterarm bewegt sich waehrend Person im Gefahrenbereich steht.",
|
||||
HarmDE: "Quetschungen, Knochenbrueche, innere Verletzungen durch Einklemmen von Koerperteilen.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal, Reinigungspersonal",
|
||||
ZoneDE: "Roboterarm, feststehende Anlagenteile innerhalb der Roboterzelle",
|
||||
DefaultSeverity: 4, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1602", NameDE: "Durchgreifen durch Schutzzaun zum Roboter", NameEN: "Reaching through safety fence to robot",
|
||||
RequiredComponentTags: []string{"moving_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person greift ueber oder durch den Schutzzaun und erreicht den Bewegungsbereich des Roboterarms.",
|
||||
TriggerDE: "Unzureichender Sicherheitsabstand zwischen Schutzzaun-Oberkante und Roboter-Schwenkbereich.",
|
||||
HarmDE: "Quetschung von Hand oder Arm zwischen Roboterarm und feststehenden Teilen.",
|
||||
AffectedDE: "Bedienpersonal, Reinigungspersonal",
|
||||
ZoneDE: "Schutzzaun-Oberkante, Roboterarm",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1603", NameDE: "Eingeschlossen in Roboterzelle", NameEN: "Trapped inside robot cell",
|
||||
RequiredComponentTags: []string{"moving_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M054", "M141"},
|
||||
Priority: 99,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing", "changeover"},
|
||||
ScenarioDE: "Person befindet sich in der Roboterzelle, Schutztuer wird geschlossen und Roboter startet. Person kann den Gefahrenbereich nicht rechtzeitig verlassen.",
|
||||
TriggerDE: "Schutztuer schliesst waehrend Person im Innenraum. Wiederanlauf des Roboters ohne Quittierung.",
|
||||
HarmDE: "Quetschungen, Stoss durch anlaufenden Roboter.",
|
||||
AffectedDE: "Wartungspersonal, Einrichter, Reinigungspersonal",
|
||||
ZoneDE: "Inneres der Roboterzelle",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1604", NameDE: "Roboterarm durchschlaegt Schutzzaun", NameEN: "Robot arm penetrates safety fence",
|
||||
RequiredComponentTags: []string{"moving_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M002"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
|
||||
ScenarioDE: "Roboterarm ueberschreitet Bewegungsbereich und trifft Schutzzaun. Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
|
||||
TriggerDE: "Fehler in der Bahnplanung oder Ausfall der Achsbegrenzung.",
|
||||
HarmDE: "Teile des Schutzzauns werden herausgeschleudert, Person ausserhalb wird getroffen.",
|
||||
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
|
||||
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1605", NameDE: "Stoss durch Werkzeug/Greifer im Einrichtbetrieb", NameEN: "Impact by tool/gripper during setup",
|
||||
RequiredComponentTags: []string{"moving_part", "clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054"},
|
||||
Priority: 98, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
|
||||
ApplicableLifecycles: []string{"teach_mode", "setup", "changeover", "fault_clearing"},
|
||||
ScenarioDE: "Person steht im Bewegungsbereich des Roboterarms und wird von bewegtem Werkzeug oder Greifer getroffen. Geschwindigkeitsreduzierung im Einrichtbetrieb reicht nicht aus.",
|
||||
TriggerDE: "Roboter bewegt Werkzeug/Greifer mit unerwartet hoher Geschwindigkeit oder in unerwartete Richtung.",
|
||||
HarmDE: "Prellungen, Quetschungen durch Kontakt mit Werkzeug/Greifer am Roboterarm.",
|
||||
AffectedDE: "Einrichter, Programmierer, Wartungspersonal",
|
||||
ZoneDE: "Inneres der Roboterzelle, Schwenkbereich Werkzeug/Greifer",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// Greifer / Werkstueck
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1610", NameDE: "Quetschen im Greiferbereich", NameEN: "Crushing in gripper area",
|
||||
RequiredComponentTags: []string{"clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 99, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
|
||||
ScenarioDE: "Person greift in den Bereich des Greifers. Hand wird zwischen Greifbacken und Werkstueck eingeklemmt.",
|
||||
TriggerDE: "Greiferbacken schliessen waehrend Koerperteil im Greifbereich ist.",
|
||||
HarmDE: "Quetschung oder Amputation von Fingern durch Greifkraft.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter",
|
||||
ZoneDE: "Greifer des Roboterarms, Werkstueckaufnahme",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1611", NameDE: "Werkstueck faellt aus Greifer herab", NameEN: "Workpiece falls from gripper",
|
||||
RequiredComponentTags: []string{"clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M007", "M141"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
|
||||
ScenarioDE: "Greifer verliert das Werkstueck waehrend des Transports. Werkstueck faellt herab und trifft Person unterhalb des Roboterarms.",
|
||||
TriggerDE: "Werkstueck faellt aus Greifer und trifft Person unterhalb des Roboterarms.",
|
||||
HarmDE: "Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht und Fallhoehe.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Bereich unterhalb des Greifer/Roboterarms",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1612", NameDE: "Werkstueck durchschlaegt Einhausung", NameEN: "Workpiece penetrates enclosure",
|
||||
RequiredComponentTags: []string{"clamping_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M141"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation"},
|
||||
ScenarioDE: "Greifer versagt und Werkstueck wird in Richtung Schutzzaun geschleudert. Person ausserhalb der Zelle wird von durchschlagendem Werkstueck getroffen.",
|
||||
TriggerDE: "Werkstueck wird durch Roboterbewegung weggeschleudert und durchschlaegt die Schutzeinrichtung.",
|
||||
HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Werkstueck getroffen.",
|
||||
AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle",
|
||||
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
// ================================================================
|
||||
// Foerderbaender / Werkstueckzu-/-auslauf
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1620", NameDE: "Quetschen an Foerderband-Einlauf", NameEN: "Crushing at conveyor infeed",
|
||||
RequiredComponentTags: []string{"entanglement_risk"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M061", "M003"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person greift an Foerderband und wird zwischen beweglichen und feststehenden Teilen eingeklemmt.",
|
||||
TriggerDE: "Hand oder Finger geraten zwischen Band und Umlenkrolle oder zwischen Werkstueck und Tunnelrahmen.",
|
||||
HarmDE: "Quetschung von Fingern, Einzug von Kleidung oder Haaren.",
|
||||
AffectedDE: "Bedienpersonal, Reinigungspersonal",
|
||||
ZoneDE: "Foerderbaender, Bandein- und -auslauf",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1621", NameDE: "Durchgreifen durch Foerderband-Oeffnung in Schutzzaun", NameEN: "Reaching through conveyor opening in fence",
|
||||
RequiredComponentTags: []string{"entanglement_risk", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"},
|
||||
ScenarioDE: "Person greift durch die Oeffnung im Schutzzaun fuer die Foerderbaender in den Gefahrenbereich des Roboters.",
|
||||
TriggerDE: "Oeffnung ist zu gross oder Sicherheitsabstand zum Roboter-Schwenkbereich ist zu gering.",
|
||||
HarmDE: "Quetschung von Hand oder Arm durch Roboterarm oder bewegte Maschinenteile.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Oeffnung der Foerderbaender im Schutzzaun, Roboterbereich dahinter",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1622", NameDE: "Herunterfallen von Werkstueck am Bandende", NameEN: "Workpiece falling off conveyor end",
|
||||
RequiredComponentTags: []string{"entanglement_risk"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M008"},
|
||||
Priority: 97,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup"},
|
||||
ScenarioDE: "Werkstueck faehrt ueber das Ende des Transportbandes hinaus, faellt herab und trifft Person am Be-/Entladeplatz.",
|
||||
TriggerDE: "Mechanischer Anschlag fehlt oder ist beschaedigt.",
|
||||
HarmDE: "Prellungen, Quetschung von Fuessen durch herabfallendes Werkstueck.",
|
||||
AffectedDE: "Bedienpersonal am Be-/Entladeplatz",
|
||||
ZoneDE: "Ende der Transportbaender, Be-/Entladeplatz",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// Scharfe Kanten / Allgemein
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1625", NameDE: "Schneiden an scharfen Kanten der Einhausung", NameEN: "Cutting on sharp enclosure edges",
|
||||
RequiredComponentTags: []string{"guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M003"},
|
||||
Priority: 97,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person schneidet sich an nicht entgrateten oder scharfkantigen Blechen der Einhausung oder Verkleidung.",
|
||||
TriggerDE: "Zugaengliche Kanten sind nicht gerundet oder gebrochen.",
|
||||
HarmDE: "Schnittwunden an Haenden und Armen.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal, Reinigungspersonal",
|
||||
ZoneDE: "Zugaengliche Kanten der Maschine und Einhausung",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// Pneumatik / Druckluft
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1630", NameDE: "Pneumatikschlauch springt unter Druck ab", NameEN: "Pressurized hose comes loose",
|
||||
RequiredComponentTags: []string{"pinch_point"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M480"},
|
||||
Priority: 97,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Pneumatikschlauch der Automation springt unter Druck ab und trifft eine Person (Peitscheneffekt).",
|
||||
TriggerDE: "Befestigung loest sich, Verschraubung wird undicht, Materialermuedung des Schlauchs.",
|
||||
HarmDE: "Prellungen, Augenverletzungen durch abspringenden Schlauch.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Pneumatikschlaeuche der Automation",
|
||||
DefaultSeverity: 2, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1631", NameDE: "Restdruck in Pneumatik nach Abschaltung", NameEN: "Residual pressure in pneumatics after shutdown",
|
||||
RequiredComponentTags: []string{"pinch_point"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M480", "M141"},
|
||||
Priority: 97,
|
||||
ApplicableLifecycles: []string{"maintenance", "fault_clearing", "changeover"},
|
||||
ScenarioDE: "Person loest druckbeaufschlagte Pneumatik-Komponenten die nach Abschaltung noch unter Druck stehen. Teile fliegen unkontrolliert weg und treffen die Person.",
|
||||
TriggerDE: "Fehlende Druckentlastung. Gesperrte Rueckschlagventile halten Druck.",
|
||||
HarmDE: "Person wird von wegfliegenden Teilen oder unkontrolliert loesenden Verbindungen getroffen. Prellungen, Schnittverletzungen.",
|
||||
AffectedDE: "Wartungspersonal, Einrichter",
|
||||
ZoneDE: "Pneumatikschlaeuche und -komponenten",
|
||||
DefaultSeverity: 2, DefaultExposure: 2,
|
||||
},
|
||||
// ================================================================
|
||||
// Kuehlschmierstoff (KSS)
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1606", NameDE: "Quetschen/Scheren durch Greifer im Einrichtbetrieb", NameEN: "Crushing/shearing by gripper during setup",
|
||||
RequiredComponentTags: []string{"clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054"},
|
||||
Priority: 98, MachineTypes: []string{"robotics_cobot", "automotive", "metalworking", "general_industry"},
|
||||
ApplicableLifecycles: []string{"teach_mode", "setup", "changeover", "fault_clearing"},
|
||||
ScenarioDE: "Einrichter steht im Schwenkbereich des Roboterarms und wird von bewegtem Greifer oder daran befestigtem Werkzeug verletzt.",
|
||||
TriggerDE: "Reduzierte Geschwindigkeit im Einrichtbetrieb reicht nicht aus oder wird nicht aktiviert.",
|
||||
HarmDE: "Quetschung, Schnittverletzung durch Greiferkanten oder Werkzeug am Roboter.",
|
||||
AffectedDE: "Einrichter, Programmierer",
|
||||
ZoneDE: "Inneres der Roboterzelle, Greifer/Werkzeug am Roboterarm",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1634", NameDE: "KSS-Pumpe spritzt bei geoeffneter Schutztuer", NameEN: "Coolant pump sprays with open guard door",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061"},
|
||||
Priority: 96, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Niederdruck-Pumpe fuer Bettspuelung laeuft an waehrend Schutztuer geoeffnet ist. Person bekommt KSS-Spritzer ins Auge oder Gesicht.",
|
||||
TriggerDE: "Pumpe startet automatisch, kein Verriegelungssignal von Schutztuer zur KSS-Pumpe.",
|
||||
HarmDE: "Augenverletzung durch KSS-Spritzer, Hautreizung.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Bearbeitungszelle, Austrittsduesen der Bettspuelung",
|
||||
DefaultSeverity: 1, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1633", NameDE: "KSS-Versorgungsschlauch platzt oder reisst ab", NameEN: "Coolant supply hose bursts or tears off",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M480"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "KSS-Versorgungsschlauch reisst ab oder platzt. Person in der Naehe wird von abspringendem Schlauch oder KSS-Strahl unter Druck getroffen.",
|
||||
TriggerDE: "Materialermuedung, mechanische Beschaedigung, fehlerhafte Befestigung des Schlauchs.",
|
||||
HarmDE: "Person wird von KSS-Strahl getroffen. Einstichverletzung, Hautreizung, Rutschgefahr.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems, Verbindungsstellen",
|
||||
DefaultSeverity: 2, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1635", NameDE: "Ausrutschen durch KSS-Leckage", NameEN: "Slipping due to coolant leakage",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M420"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Kuehlschmierstoff tritt aus und bildet rutschigen Belag auf dem Boden. Person rutscht aus und stuerzt.",
|
||||
TriggerDE: "Leckage an Schlauchverbindung, Dichtungsversagen.",
|
||||
HarmDE: "Ausrutschen und Sturz, Prellungen, Knochenbrueche.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Boden um Bearbeitungszentrum und Kuehlschmierstoffanlage",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1636", NameDE: "Hautkontakt mit Kuehlschmierstoff", NameEN: "Skin contact with coolant",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person kommt bei Arbeiten am Bearbeitungszentrum oder der Roboterzelle mit Kuehlschmierstoff in Beruehrung.",
|
||||
TriggerDE: "Hautkontakt beim Reinigen, Werkzeugwechsel oder Beseitigung von Stoerungen.",
|
||||
HarmDE: "Hautirritationen, allergische Reaktionen, bei laengerer Exposition Ekzeme.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Bearbeitungszentrum, Roboterzelle im Bereich der Beladetuer",
|
||||
DefaultSeverity: 1, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1637", NameDE: "Einatmen von KSS-Aerosolen", NameEN: "Inhalation of coolant aerosols",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"material_environmental"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
|
||||
ScenarioDE: "Person oeffnet Schutztuer der Bearbeitungszelle und atmet freigesetzte KSS-Aerosole ein.",
|
||||
TriggerDE: "Oeffnen der Schutztuer nach Bearbeitungsvorgang, unzureichende Absaugung.",
|
||||
HarmDE: "Person atmet KSS-Aerosole ein. Atembeschwerden, Reizung der Atemwege, bei chronischer Exposition Atemwegserkrankungen.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer",
|
||||
DefaultSeverity: 1, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// Elektrisch (Roboterzelle-spezifisch)
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1640", NameDE: "Direktes Beruehren spannungsfuehrender Teile", NameEN: "Direct contact with live parts",
|
||||
RequiredComponentTags: []string{},
|
||||
RequiredEnergyTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M265", "M089", "M088", "M139", "M475"},
|
||||
Priority: 99,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person beruehrt spannungsfuehrende Teile der Anlage die nicht ausreichend isoliert oder abgedeckt sind.",
|
||||
TriggerDE: "Beschaedigte Isolation, fehlende Abdeckung, ungesicherter Schaltschrank.",
|
||||
HarmDE: "Person erleidet elektrischen Schlag. Herzkammerflimmern, Verbrennungen, bei Hochspannung Todesfolge.",
|
||||
AffectedDE: "Wartungspersonal, Einrichter",
|
||||
ZoneDE: "Zugaengliche Kabel, Klemmen, Schaltschrank",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1641", NameDE: "Gefaehrliche Beruehrungsspannung durch Schutzleiterfehler", NameEN: "Dangerous touch voltage due to PE failure",
|
||||
RequiredComponentTags: []string{},
|
||||
RequiredEnergyTags: []string{"electrical"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M475", "M476"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Schutzleiter ist unterbrochen. Person beruehrt das Maschinengehaeuse und erleidet elektrischen Schlag durch gefaehrliche Beruehrungsspannung.",
|
||||
TriggerDE: "Schutzleiterunterbrechung durch mechanische Beschaedigung oder fehlerhafte Installation.",
|
||||
HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder leitfaehiger Oberflaechen.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Beruehrbare leitfaehige Oberflaechen der Anlage",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1642", NameDE: "Kabelbrand durch Ueberlast oder Kurzschluss", NameEN: "Cable fire from overload or short circuit",
|
||||
RequiredComponentTags: []string{},
|
||||
RequiredEnergyTags: []string{"electrical"},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M009"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance"},
|
||||
ScenarioDE: "Kabel ueberhitzt und entzuendet sich durch Ueberlast oder fehlenden Ueberstromschutz. Person wird durch Brand oder toxische Gase verletzt.",
|
||||
TriggerDE: "Dauerhafter Betrieb nahe der Belastungsgrenze, falsch dimensionierte Sicherung.",
|
||||
HarmDE: "Brand, Rauchentwicklung, Verletzung durch Feuer oder toxische Gase.",
|
||||
AffectedDE: "Alle Personen im Bereich der Anlage",
|
||||
ZoneDE: "Kabel und Leitungen der Anlage",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
package iace
|
||||
|
||||
// GetRobotCellPatternsExt returns additional hazard patterns for robot cells.
|
||||
// These cover specific scenarios identified through GT benchmark gaps.
|
||||
// HP1650-HP1699
|
||||
func GetRobotCellPatternsExt() []HazardPattern {
|
||||
return []HazardPattern{
|
||||
// ================================================================
|
||||
// Roboterarm — Spezifische Szenarien (GT-Gaps)
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1650", NameDE: "Roboterarm durchschlaegt Bewegungsbegrenzung", NameEN: "Robot arm exceeds motion limit",
|
||||
RequiredComponentTags: []string{"moving_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061", "M054"},
|
||||
Priority: 99,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "changeover", "fault_clearing"},
|
||||
ScenarioDE: "Roboterarm ueberschreitet Bewegungsbegrenzung und trifft Schutzzaun. Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
|
||||
TriggerDE: "Softwareendschalter versagt, Achsbegrenzung (DCS) fehlerhaft konfiguriert.",
|
||||
HarmDE: "Person ausserhalb wird von Zaunteilen oder dem Roboterarm getroffen.",
|
||||
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
|
||||
ZoneDE: "Schutzzaun, Bereich um die Roboterzelle",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1651", NameDE: "Wiederanlauf Roboter waehrend Person in Zelle", NameEN: "Robot restart while person inside cell",
|
||||
RequiredComponentTags: []string{"moving_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061", "M141"},
|
||||
Priority: 99,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing", "changeover"},
|
||||
ScenarioDE: "Person befindet sich in der Roboterzelle. Schutztuer wird geschlossen und Roboter startet ohne dass sichergestellt ist, dass niemand im Gefahrenbereich ist.",
|
||||
TriggerDE: "Fehlende Quittierungspflicht, kein Personenscanner, Schutztuer ohne Sicherheitszuhaltung.",
|
||||
HarmDE: "Schwere Quetschungen, Knochenbrueche durch anlaufenden Roboter.",
|
||||
AffectedDE: "Wartungspersonal, Einrichter, Reinigungspersonal",
|
||||
ZoneDE: "Inneres der Roboterzelle, Roboterarm",
|
||||
DefaultSeverity: 4, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1652", NameDE: "Quetschen durch Werkzeug/Greifer am Roboter im Betrieb", NameEN: "Crushing by tool/gripper during operation",
|
||||
RequiredComponentTags: []string{"moving_part", "clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 99,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning"},
|
||||
ScenarioDE: "Person wird von bewegtem Werkzeug oder Greifer am Roboterarm getroffen oder zwischen Werkzeug und feststehenden Teilen eingeklemmt.",
|
||||
TriggerDE: "Roboter bewegt Werkzeug/Greifer waehrend Person im Schwenkbereich.",
|
||||
HarmDE: "Quetschungen, Schnittverletzungen, Prellungen durch Werkzeug/Greifer.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter",
|
||||
ZoneDE: "Inneres der Roboterzelle, Greifer/Werkzeug des Roboterarms",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1653", NameDE: "Quetschen durch Werkstück am Robotergreifer", NameEN: "Crushing by workpiece on robot gripper",
|
||||
RequiredComponentTags: []string{"moving_part", "clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
|
||||
ScenarioDE: "Person wird von sich bewegendem Werkstueck am Robotergreifer getroffen oder zwischen Werkstueck und feststehenden Anlagenteilen eingeklemmt.",
|
||||
TriggerDE: "Roboter transportiert Werkstueck, Person steht im Schwenkbereich.",
|
||||
HarmDE: "Quetschungen, Prellungen, Knochenbrueche abhaengig von Werkstueckgewicht.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter",
|
||||
ZoneDE: "Inneres der Roboterzelle, Greifer des Roboterarms",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1654", NameDE: "Werkstück/Werkzeug durchschlaegt Schutzzaun", NameEN: "Workpiece/tool penetrates safety fence",
|
||||
RequiredComponentTags: []string{"clamping_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation"},
|
||||
ScenarioDE: "Greifer versagt und Werkstueck/Werkzeug wird Richtung Schutzzaun geschleudert. Person ausserhalb wird getroffen.",
|
||||
TriggerDE: "Greifkraftverlust, Druckausfall, oelige Oberflaeche des Werkstuecks.",
|
||||
HarmDE: "Person ausserhalb der Zelle wird von weggeschleudertem Teil getroffen.",
|
||||
AffectedDE: "Bedienpersonal in der Naehe der Roboterzelle",
|
||||
ZoneDE: "Schutzzaun, Bereich ausserhalb der Roboterzelle",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1655", NameDE: "Durchgreifen ueber Schutzzaun zum Greifer/Werkstueck", NameEN: "Reaching over fence to gripper/workpiece",
|
||||
RequiredComponentTags: []string{"clamping_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person greift ueber den Schutzzaun und erreicht den Greifer oder das Werkstueck am Roboterarm.",
|
||||
TriggerDE: "Sicherheitsabstand zwischen Zaun-Oberkante und Greifer/Werkstueck zu gering.",
|
||||
HarmDE: "Quetschung von Hand oder Arm zwischen Greifer/Werkstueck und feststehenden Teilen.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Schutzzaun-Oberkante, Greifer/Werkstueck am Roboterarm",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
// ================================================================
|
||||
// Zentriergreifer an Förderbändern
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1660", NameDE: "Quetschen am Zentriergreifer von aussen", NameEN: "Crushing at centering gripper from outside",
|
||||
RequiredComponentTags: []string{"clamping_part", "entanglement_risk"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person befindet sich ausserhalb der Roboterzelle und greift an die Zentriereinheit (fest montierter Greifer am Foerderband).",
|
||||
TriggerDE: "Zentriergreifer schliesst waehrend Hand im Greifbereich. Unzureichender Abstand zwischen Greifer und Schutzzaun-Oeffnung.",
|
||||
HarmDE: "Quetschung von Fingern oder Hand zwischen Greifbacken und Werkstueck.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Zentriereinheit an Foerderbaendern, Schutzzaun-Oeffnung",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1661", NameDE: "Quetschen am Zentriergreifer von innen", NameEN: "Crushing at centering gripper from inside cell",
|
||||
RequiredComponentTags: []string{"clamping_part", "entanglement_risk", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "cleaning", "fault_clearing"},
|
||||
ScenarioDE: "Person befindet sich innerhalb der Roboterzelle und greift an die Zentriereinheit am Foerderband.",
|
||||
TriggerDE: "Schutztuer geoeffnet, aber Zentriergreifer wird nicht automatisch stillgesetzt.",
|
||||
HarmDE: "Quetschung von Fingern oder Hand zwischen Greifbacken und Werkstueck.",
|
||||
AffectedDE: "Wartungspersonal, Reinigungspersonal",
|
||||
ZoneDE: "Zentriereinheit an Foerderbaendern innerhalb der Roboterzelle",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// Bearbeitungszentrum (Robodrill/WZM) innerhalb Roboterzelle
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1665", NameDE: "Quetschen an Beladetuer der Werkzeugmaschine", NameEN: "Crushing at machine tool loading door",
|
||||
RequiredComponentTags: []string{"moving_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive", "robotics_cobot"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person greift durch die Beladetuer der Werkzeugmaschine. Beladetuer schliesst sich oder bewegliche Teile im Innenraum starten.",
|
||||
TriggerDE: "Tuerpositionsschalter nicht in Robotersteuerung eingebunden, fehlende Verriegelung.",
|
||||
HarmDE: "Quetschung von Hand/Arm an Beladetuer oder durch bewegliche Teile im Bearbeitungsraum.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter, Wartungspersonal",
|
||||
ZoneDE: "Beladetuer der Werkzeugmaschine, Bearbeitungsraum",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1666", NameDE: "Quetschen/Scheren im Bearbeitungsraum der WZM", NameEN: "Crushing/shearing inside machine tool workspace",
|
||||
RequiredComponentTags: []string{"moving_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054"},
|
||||
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive", "robotics_cobot"},
|
||||
ApplicableLifecycles: []string{"setup", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person greift in den Bearbeitungsraum der Werkzeugmaschine und wird von beweglichen Achsen, Werkzeug oder Spannvorrichtung verletzt.",
|
||||
TriggerDE: "Bewegliche Teile starten waehrend Hand im Bearbeitungsraum (Einrichtbetrieb, Stoerungsbeseitigung).",
|
||||
HarmDE: "Quetschungen, Schnittverletzungen durch rotierende Werkzeuge, Scheren an Achsbewegungen.",
|
||||
AffectedDE: "Einrichter, Wartungspersonal",
|
||||
ZoneDE: "Bearbeitungsraum der Werkzeugmaschine, Achsen, Werkzeug, Spannvorrichtung",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// KSS-Spritzer / Druckluft in Bearbeitungszelle
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1670", NameDE: "KSS-Spritzer in Augen/Gesicht", NameEN: "Coolant splash to eyes/face",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person bekommt Kuehlschmierstoff-Spritzer ins Auge oder Gesicht beim Oeffnen der Bearbeitungszelle oder bei laufender Bettspuelung.",
|
||||
TriggerDE: "KSS-Pumpe laeuft waehrend Schutztuer geoeffnet ist, Austrittsduese nicht korrekt gerichtet.",
|
||||
HarmDE: "Augenverletzung, Reizung der Bindehaut, bei Hochdruck-KSS ernsthafte Augenschaeden.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Bearbeitungszelle, Bereich vor der Schutztuer, Austrittsduesen",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1671", NameDE: "Druckluft-Verletzung in Bearbeitungszelle", NameEN: "Compressed air injury in machining cell",
|
||||
RequiredComponentTags: []string{"pinch_point"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person wird von ausstroemender Druckluft oder aufgewirbelten Bearbeitungsrueckstaenden in der Bearbeitungszelle verletzt.",
|
||||
TriggerDE: "Druckluftreinigungsduese aktiv waehrend Schutztuer geoeffnet, Spaene oder Partikel werden aufgewirbelt.",
|
||||
HarmDE: "Augenverletzung durch Spaene, Hautverletzung durch Druckluftstoss.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Bearbeitungszelle, Druckluftreinigungsduesen",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// KSS-Schläuche unter Druck
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1675", NameDE: "KSS-Schlauch bersten oder abspringen", NameEN: "Coolant hose burst or detachment",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M480"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Schlauch der Kuehlschmierstoffversorgung zwischen Aufbereitungsanlage und Bearbeitungszentrum platzt oder springt unter Druck ab.",
|
||||
TriggerDE: "Materialermuedung, Ueberdruck, fehlerhafte Befestigung, mechanische Beschaedigung des Schlauchs.",
|
||||
HarmDE: "Person wird von abspringendem Schlauch getroffen (Peitscheneffekt). KSS-Spritzer unter Druck verletzen Haut und Augen. Rutschgefahr durch austretenden KSS.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems",
|
||||
DefaultSeverity: 2, DefaultExposure: 2,
|
||||
},
|
||||
// ================================================================
|
||||
// Quetschen am Förderband — Werkstück/Tunnel
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1680", NameDE: "Quetschen zwischen Werkstueck und Tunnel am Foerderband", NameEN: "Crushing between workpiece and conveyor tunnel",
|
||||
RequiredComponentTags: []string{"entanglement_risk"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M002", "M003"},
|
||||
Priority: 97,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "fault_clearing"},
|
||||
ScenarioDE: "Person greift an den Tunnel/Rahmen des Foerderbandes und wird von einem darauf bewegten Werkstueck eingequetscht.",
|
||||
TriggerDE: "Zu geringer Abstand zwischen Werkstueck und Tunnel/Rahmen, scharfe Kanten an Tunneleingang.",
|
||||
HarmDE: "Quetschung von Fingern zwischen Werkstueck und Rahmen.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Foerderband-Tunnel, Werkstück auf dem Band",
|
||||
DefaultSeverity: 2, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// Elektrisch — Spezifische Szenarien
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1685", NameDE: "Indirektes Beruehren durch Schutzleiterunterbrechung", NameEN: "Indirect contact due to PE interruption",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M475", "M476"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Schutzleiter ist unterbrochen. Person beruehrt leitfaehige Maschinenteile und erleidet elektrischen Schlag.",
|
||||
TriggerDE: "Mechanische Beschaedigung des Schutzleiters, korrodierte Verbindung, fehlerhafte Installation.",
|
||||
HarmDE: "Elektrischer Schlag bei Beruehren des Maschinengehaeuses oder anderer leitfaehiger Teile.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Beruehrbare leitfaehige Oberflaechen der Anlage",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1686", NameDE: "Direktes Beruehren im Schaltschrank", NameEN: "Direct contact inside control cabinet",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M009"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"maintenance", "fault_clearing", "commissioning"},
|
||||
ScenarioDE: "Person beruehrt spannungsfuehrende Teile bei geoeffnetem Schaltschrank. Leiter um Bedienelemente sind nicht fingersicher geschuetzt.",
|
||||
TriggerDE: "Schaltschranktuer geoeffnet fuer Wartung oder Fehlersuche, unzureichender Beruehrungsschutz.",
|
||||
HarmDE: "Person erleidet elektrischen Schlag. Herzkammerflimmern, Verbrennungen, bei Hochspannung Todesfolge.",
|
||||
AffectedDE: "Wartungspersonal, Elektrofachkraefte",
|
||||
ZoneDE: "Schaltschrank-Innenraum, Klemmen, Sammelschienen",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1687", NameDE: "Brand durch eindringende Fluessigkeit", NameEN: "Fire from liquid ingress causing short circuit",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M009"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "cleaning"},
|
||||
ScenarioDE: "Fluessigkeit dringt in elektrische Komponenten ein und verursacht Kurzschluss. Person wird durch Brand oder Rauchentwicklung gefaehrdet.",
|
||||
TriggerDE: "Reinigung mit Wasser, KSS-Leckage tropft auf Schaltschrank oder Steuerungskomponenten.",
|
||||
HarmDE: "Person wird durch Brand, Flammen oder toxische Rauchgase verletzt. Verbrennungen, Rauchvergiftung.",
|
||||
AffectedDE: "Bedienpersonal, Reinigungspersonal",
|
||||
ZoneDE: "Schaltgeraetekombinationen, elektrische Komponenten unterhalb von Rohrleitungen",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1688", NameDE: "Gefaehrliche Beruehrungsspannung durch Potentialunterschiede", NameEN: "Dangerous touch voltage from potential differences",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M475", "M477", "M138", "M329"},
|
||||
Priority: 96,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Person beruehrt gleichzeitig Anlagenteile mit unterschiedlichem Potential und erleidet elektrischen Schlag.",
|
||||
TriggerDE: "Fehlender Potentialausgleich zwischen Anlagenteilen verschiedener Hersteller.",
|
||||
HarmDE: "Elektrischer Schlag bei gleichzeitigem Beruehren von Teilen unterschiedlichen Potentials.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Elektrisch leitfaehige Oberflaechen verschiedener Anlagenteile",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1689", NameDE: "Fehlerstromschutz an Steckdosenstromkreisen", NameEN: "RCD protection at socket circuits",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M475"},
|
||||
Priority: 97,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Defektes Geraet wird an Steckdose der Maschine angeschlossen. Fehlerstrom fliesst ueber den Koerper der beruerenden Person.",
|
||||
TriggerDE: "Fehlende Fehlerstrom-Schutzeinrichtung (RCD) an Steckdosenstromkreisen der Maschine.",
|
||||
HarmDE: "Person erleidet elektrischen Schlag durch Fehlerstrom. Herzkammerflimmern, potentiell toedlich.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Steckdosen der Maschine, angeschlossene Betriebsmittel",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
// ================================================================
|
||||
// Ergonomie
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1690", NameDE: "Ergonomisch unguenstige Einlegeposition", NameEN: "Unfavorable ergonomic loading position",
|
||||
RequiredComponentTags: []string{"entanglement_risk"},
|
||||
GeneratedHazardCats: []string{"ergonomic_hazard"},
|
||||
SuggestedMeasureIDs: []string{},
|
||||
Priority: 85,
|
||||
ApplicableLifecycles: []string{"normal_operation"},
|
||||
ScenarioDE: "Person muss Werkstuecke in ergonomisch unguenstiger Hoehe oder Reichweite auf das Foerderband auflegen oder entnehmen.",
|
||||
TriggerDE: "Bandhoehe nicht auf ergonomische Handhabung ausgelegt, schwere Werkstuecke.",
|
||||
HarmDE: "Person erleidet Rueckenbeschwerden und Schulterbelastung durch wiederholte Fehlhaltung. Langfristig Muskel-Skelett-Erkrankungen.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Beladebereich der Foerderbaender",
|
||||
DefaultSeverity: 2, DefaultExposure: 4,
|
||||
},
|
||||
{
|
||||
ID: "HP1691", NameDE: "Unergonomische Position der Bedienelemente", NameEN: "Unfavorable ergonomic position of controls",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"ergonomic_hazard"},
|
||||
SuggestedMeasureIDs: []string{},
|
||||
Priority: 85,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup"},
|
||||
ScenarioDE: "Person bedient Anlage in ergonomisch unguenstiger Position ueber laengere Zeit.",
|
||||
TriggerDE: "Bedienfeld zu hoch, zu niedrig oder seitlich versetzt montiert.",
|
||||
HarmDE: "Person erleidet Nacken- und Schulterbelastung durch unguenstige Bedienposition. Langfristig Haltungsschaeden.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Bedienfeld, HMI, Betriebsartenwahlschalter",
|
||||
DefaultSeverity: 2, DefaultExposure: 4,
|
||||
},
|
||||
// ================================================================
|
||||
// Thermisch / Verbrennung
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1695", NameDE: "Verbrennung an heissen Werkstuecken", NameEN: "Burn from hot workpieces",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"thermal_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M141"},
|
||||
Priority: 88, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
|
||||
ScenarioDE: "Person beruehrt heisse Werkstuecke die durch die Bearbeitung erwaermt wurden.",
|
||||
TriggerDE: "Manuelle Entnahme von Werkstuecken ohne Wartezeit oder Schutzhandschuhe.",
|
||||
HarmDE: "Verbrennungen an Haenden und Fingern.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Werkstueckausgabe, Entnahmeplatz",
|
||||
DefaultSeverity: 1, DefaultExposure: 3,
|
||||
},
|
||||
// ================================================================
|
||||
// Tragfähigkeit / Aufstellung
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1697", NameDE: "Anlage bricht durch unzureichenden Untergrund ein", NameEN: "Machine collapses through insufficient floor",
|
||||
RequiredComponentTags: []string{"high_force"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{},
|
||||
Priority: 88,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "commissioning"},
|
||||
ScenarioDE: "Untergrund bricht unter dem Maschinengewicht ein. Personen im Umfeld werden von kippender oder absackender Anlage eingeklemmt.",
|
||||
TriggerDE: "Boden nicht auf maximale statische und dynamische Lasten der Maschine ausgelegt.",
|
||||
HarmDE: "Anlage bricht ein, Quetschung von Personen im Umfeld.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Bereich um die Maschine, Aufstellflaeche",
|
||||
DefaultSeverity: 4, DefaultExposure: 1,
|
||||
},
|
||||
// ================================================================
|
||||
// Elektrisch — Kriechstrecken + EMV
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1698", NameDE: "Kurzschluss durch unzureichende Luft-/Kriechstrecken", NameEN: "Short circuit from insufficient creepage/clearance",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M477"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "Unzureichende Luft-/Kriechstrecken fuehren bei Verschmutzung zu Kriechstroemen. Person beruehrt betroffene Teile und erleidet elektrischen Schlag.",
|
||||
TriggerDE: "Verschmutzungsgrad hoeher als bei der Dimensionierung angenommen, Feuchtigkeit, alterungsbedingte Veraenderung.",
|
||||
HarmDE: "Gefaehrliche Beruehrungsspannung an beruehrbaren Teilen, Kurzschluss, Brand.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Schaltgeraetekombinationen, elektrische Anschluesse",
|
||||
DefaultSeverity: 4, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1699", NameDE: "EMV-Stoereinfluss auf Sicherheitsfunktionen", NameEN: "EMC interference with safety functions",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"radiation_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M478", "M479"},
|
||||
Priority: 97,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup"},
|
||||
ScenarioDE: "EMV-Stoerungen verursachen unerwartete Maschinenbewegungen. Person im Gefahrenbereich wird von unkontrolliert bewegten Teilen getroffen.",
|
||||
TriggerDE: "Unzureichende EMV-Schirmung, nicht-fachgerechte Verkabelung, externe Stoerquellen.",
|
||||
HarmDE: "Unkontrollierte Bewegung von Achsen, Werkzeug oder Roboterarm durch Steuerungsfehler.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter",
|
||||
ZoneDE: "Bearbeitungsbereich, sicherheitsrelevante Steuerungen",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
// ================================================================
|
||||
// Differenzierte Patterns (GT-Benchmark: gleiche Zone, anderes Szenario)
|
||||
// ================================================================
|
||||
{
|
||||
ID: "HP1700", NameDE: "Getroffen von bewegtem Werkzeug/Greifer am Roboter", NameEN: "Struck by moving tool/gripper on robot",
|
||||
RequiredComponentTags: []string{"moving_part", "clamping_part"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M054", "M061"},
|
||||
Priority: 99,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "teach_mode", "cleaning"},
|
||||
ScenarioDE: "Person steht im Bewegungsbereich des Roboterarms und wird von bewegtem Werkzeug oder Greifer getroffen.",
|
||||
TriggerDE: "Roboter schwenkt mit Werkzeug/Greifer in Richtung Person.",
|
||||
HarmDE: "Prellungen, Schnittverletzungen durch Werkzeugkanten, Knochenbrueche.",
|
||||
AffectedDE: "Bedienpersonal, Einrichter",
|
||||
ZoneDE: "Inneres der Roboterzelle, Schwenkbereich Werkzeug/Greifer",
|
||||
DefaultSeverity: 3, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1701", NameDE: "Greifer/Werkzeug durchschlaegt Schutzzaun", NameEN: "Gripper/tool penetrates safety fence",
|
||||
RequiredComponentTags: []string{"clamping_part", "guard"},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061"},
|
||||
Priority: 98,
|
||||
ApplicableLifecycles: []string{"normal_operation", "setup", "changeover"},
|
||||
ScenarioDE: "Greifer oder Werkzeug am Roboterarm durchschlaegt den Schutzzaun und trifft Person ausserhalb der Zelle.",
|
||||
TriggerDE: "Bewegungsbegrenzung versagt, Schutzzaun nicht auf Aufprallenergie ausgelegt.",
|
||||
HarmDE: "Person ausserhalb wird von Greifer/Werkzeug oder Zaunteilen getroffen.",
|
||||
AffectedDE: "Bedienpersonal in der Naehe des Schutzzauns",
|
||||
ZoneDE: "Bereich um Roboterarm ausserhalb der Roboterzelle",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1702", NameDE: "KSS-Schlauch platzt unter Druck", NameEN: "Coolant hose bursts under pressure",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M480"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "KSS-Schlauch platzt und spritzt Kuehlschmierstoff unter Druck. Person in der Naehe wird von KSS-Strahl getroffen.",
|
||||
TriggerDE: "Alterung, Beschaedigung oder Ueberdruck fuehrt zum Versagen des Schlauchs.",
|
||||
HarmDE: "Einstichverletzung durch KSS-Strahl unter Druck, Augenverletzung, Rutschgefahr.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Druckschlaeuche des Kuehlschmierstoffsystems",
|
||||
DefaultSeverity: 2, DefaultExposure: 2,
|
||||
},
|
||||
{
|
||||
ID: "HP1703", NameDE: "KSS-Bettspuelung bei geoeffneter Schutztuer", NameEN: "Coolant bed wash with open guard door",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"mechanical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M061"},
|
||||
Priority: 97, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance", "fault_clearing"},
|
||||
ScenarioDE: "KSS-Pumpe laeuft bei geoeffneter Schutztuer. Person vor der Bearbeitungszelle bekommt KSS-Spritzer ins Auge oder Gesicht.",
|
||||
TriggerDE: "Kein automatisches Abschalten der KSS-Pumpe bei geoeffneter Tuer.",
|
||||
HarmDE: "KSS-Spritzer in Augen oder Gesicht, Rutschgefahr durch austretenden KSS.",
|
||||
AffectedDE: "Bedienpersonal, Wartungspersonal",
|
||||
ZoneDE: "Inneres des Bearbeitungszentrums, Bereich vor der Schutztuer",
|
||||
DefaultSeverity: 1, DefaultExposure: 3,
|
||||
},
|
||||
{
|
||||
ID: "HP1704", NameDE: "Brand durch KSS-Leckage auf elektrische Komponenten", NameEN: "Fire from coolant leakage on electrical components",
|
||||
RequiredComponentTags: []string{},
|
||||
GeneratedHazardCats: []string{"electrical_hazard"},
|
||||
SuggestedMeasureIDs: []string{"M480", "M009"},
|
||||
Priority: 98, MachineTypes: []string{"cnc", "metalworking", "automotive"},
|
||||
ApplicableLifecycles: []string{"normal_operation", "cleaning", "maintenance"},
|
||||
ScenarioDE: "KSS-Leckage tropft auf elektrische Komponenten und verursacht Kurzschluss. Person wird durch Brand oder Rauchentwicklung gefaehrdet.",
|
||||
TriggerDE: "KSS-Leitung undicht oberhalb elektrischer Komponenten, tropft auf Klemmen oder Leiterplatten.",
|
||||
HarmDE: "Person wird durch Brand, Flammen oder toxische Rauchgase verletzt. Verbrennungen, Rauchvergiftung.",
|
||||
AffectedDE: "Bedienpersonal",
|
||||
ZoneDE: "Spannungsfuehrende Teile unterhalb/angrenzend von KSS-Leitungen",
|
||||
DefaultSeverity: 3, DefaultExposure: 2,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ func builtinSoftwarePatterns() []HazardPattern {
|
||||
SuggestedMeasureIDs: []string{"M145", "M146", "M121"},
|
||||
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",
|
||||
|
||||
@@ -66,5 +66,26 @@ func getSupplementaryMeasures() []ProtectiveMeasureEntry {
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
{ID: "M402", ReductionType: "protection", SubType: "monitoring", Name: "Sicherheitsbeleuchtung an Fluchtwegen und Arbeitsplaetzen", Description: "Fluchtwege und sicherheitsrelevante Arbeitsplaetze erhalten eine Sicherheitsbeleuchtung die bei Ausfall der allgemeinen Beleuchtung automatisch aktiviert wird.", HazardCategory: "general", Examples: []string{"Notbeleuchtung mit Batteriepufferung", "Nachleuchtende Leitmarkierung am Boden"}, NormReferences: []string{"ASR A3.4 — Beleuchtung", "ASR A2.3 — Fluchtwege"}},
|
||||
{ID: "M403", ReductionType: "protection", SubType: "safety_control", Name: "Quetschschutz an kraftbetaetigten Tueren und Toren", Description: "Kraftbetaetigte Tueren und Tore erhalten Schutzeinrichtungen gegen Quetschen, Scheren und Einziehen an allen Schliesskanten.", HazardCategory: "mechanical", Examples: []string{"Schaltleiste an Schiebetorunterkante", "Lichtschranke an Rolltoroeffnung"}, NormReferences: []string{"ASR A1.7 — Tueren und Tore", "EN 12453"}},
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Elektrische Sicherheit — Potentialausgleich & Ableitstroeme
|
||||
// Gap: GT-Benchmark 2.12 (Potentialausgleich), 2.4 (Ableitstroeme)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
{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: "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: "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)
|
||||
}
|
||||
}
|
||||
@@ -4,104 +4,30 @@ package iace
|
||||
// IDs: M452-M474 (23 measures).
|
||||
func GetTextileAgriMeasures() []ProtectiveMeasureEntry {
|
||||
return []ProtectiveMeasureEntry{
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// Textilmaschinen (M452-M460)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
{ID: "M452", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Walzenspalt-Schutzeinrichtung (EN ISO 11111-1)", NameEN: "Nip guard per EN ISO 11111-1",
|
||||
DescriptionDE: "Formschluessige Schutzeinrichtung am Walzenspalt mit Verriegelung die den Antrieb bei Oeffnung stoppt. Alternativ: sensorbasierter Schutz mit Lichtgitter.",
|
||||
HazardCategory: "mechanical", NormRef: "EN ISO 11111-1:2016 Abschnitt 5.2", RiskReduction: 40},
|
||||
{ID: "M453", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Not-Ruecklauf an Walzenpaaren", NameEN: "Emergency reverse on roller pairs",
|
||||
DescriptionDE: "Sofortige Drehrichtungsumkehr bei Betaetigung des Not-Ruecklauf-Buegels am Walzenspalt.",
|
||||
HazardCategory: "mechanical", NormRef: "EN ISO 11111-1:2016 Abschnitt 5.2.4", RiskReduction: 30},
|
||||
{ID: "M454", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Geschlossene Absaugung am Kardierbereich", NameEN: "Enclosed extraction at carding area",
|
||||
DescriptionDE: "Vollstaendig eingehaustes Kardiersystem mit integrierter Faserstaub-Absaugung und Filterung. OEL unter 1 mg/m3 Faserstaub.",
|
||||
HazardCategory: "material_environmental", NormRef: "EN ISO 11111-3:2016, TRGS 900", RiskReduction: 35},
|
||||
{ID: "M455", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Laermkapselung fuer Webmaschinen", NameEN: "Noise enclosure for looms",
|
||||
DescriptionDE: "Schalldaemmende Einhausung der Webmaschine. Reduktion um mindestens 15 dB(A) am Bedienerplatz.",
|
||||
HazardCategory: "noise_vibration", NormRef: "EN ISO 11111-7:2016", RiskReduction: 25},
|
||||
{ID: "M456", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Beruehrungsschutz an Heissteilen der Fixiermaschine", NameEN: "Contact protection on stenter hot parts",
|
||||
DescriptionDE: "Isolierung und Verkleidung aller Oberflaechen mit Temperaturen > 65 Grad C. Warnmarkierung.",
|
||||
HazardCategory: "thermal", NormRef: "EN ISO 11111-6:2016 Abschnitt 5.3", RiskReduction: 30},
|
||||
{ID: "M457", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Geschlossenes Chemikalien-Dosiersystem", NameEN: "Closed chemical dosing system",
|
||||
DescriptionDE: "Automatische Dosierung und Zufuehrung von Faerbemitteln ueber geschlossene Leitungen. Vermeidung offener Wannen.",
|
||||
HazardCategory: "material_environmental", NormRef: "EN ISO 11111-6:2016, TRGS 401", RiskReduction: 35},
|
||||
{ID: "M458", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Antistatik-Ausstattung Textilmaschine", NameEN: "Anti-static equipment for textile machine",
|
||||
DescriptionDE: "Erdung aller leitfaehigen Teile, antistatische Transportbaender, Luftbefeuchtung > 50% rF.",
|
||||
HazardCategory: "electrical", NormRef: "EN ISO 11111-1:2016, EN 1127-1", RiskReduction: 20},
|
||||
{ID: "M459", ReductionType: "information", SubType: "instruction",
|
||||
NameDE: "Ergonomie-Unterweisung Textilarbeitsplaetze", NameEN: "Ergonomics training textile workstations",
|
||||
DescriptionDE: "Unterweisung zu Arbeitshaltung, Pausenregelung und ergonomischer Arbeitsplatzgestaltung an Webmaschinen.",
|
||||
HazardCategory: "ergonomic", NormRef: "EN ISO 11111-1:2016 Abschnitt 6", RiskReduction: 10},
|
||||
{ID: "M460", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Erdung und Potentialausgleich Gewebeauslauf", NameEN: "Grounding and bonding at fabric exit",
|
||||
DescriptionDE: "Leitfaehige Walzen und Ableitsysteme am Gewebeauslauf verhindern elektrostatische Aufladung.",
|
||||
HazardCategory: "electrical", NormRef: "EN 1127-1, TRBS 2153", RiskReduction: 20},
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
{ID: "M452", ReductionType: "design", SubType: "inherent_safety", Name: "Walzenspalt-Schutzeinrichtung (EN ISO 11111-1)", Description: "Formschluessige Schutzeinrichtung am Walzenspalt mit Verriegelung die den Antrieb bei Oeffnung stoppt.", HazardCategory: "mechanical", NormReferences: []string{"EN ISO 11111-1:2016 Abschnitt 5.2"}},
|
||||
{ID: "M453", ReductionType: "protection", SubType: "safeguard", Name: "Not-Ruecklauf an Walzenpaaren", Description: "Sofortige Drehrichtungsumkehr bei Betaetigung des Not-Ruecklauf-Buegels am Walzenspalt.", HazardCategory: "mechanical", NormReferences: []string{"EN ISO 11111-1:2016 Abschnitt 5.2.4"}},
|
||||
{ID: "M454", ReductionType: "design", SubType: "inherent_safety", Name: "Geschlossene Absaugung am Kardierbereich", Description: "Vollstaendig eingehaustes Kardiersystem mit integrierter Faserstaub-Absaugung und Filterung. OEL unter 1 mg/m3.", HazardCategory: "material_environmental", NormReferences: []string{"EN ISO 11111-3:2016", "TRGS 900"}},
|
||||
{ID: "M455", ReductionType: "protection", SubType: "safeguard", Name: "Laermkapselung fuer Webmaschinen", Description: "Schalldaemmende Einhausung der Webmaschine. Reduktion um mindestens 15 dB(A) am Bedienerplatz.", HazardCategory: "noise_vibration", NormReferences: []string{"EN ISO 11111-7:2016"}},
|
||||
{ID: "M456", ReductionType: "protection", SubType: "safeguard", Name: "Beruehrungsschutz an Heissteilen der Fixiermaschine", Description: "Isolierung und Verkleidung aller Oberflaechen mit Temperaturen > 65 Grad C. Warnmarkierung.", HazardCategory: "thermal", NormReferences: []string{"EN ISO 11111-6:2016 Abschnitt 5.3"}},
|
||||
{ID: "M457", ReductionType: "protection", SubType: "safeguard", Name: "Geschlossenes Chemikalien-Dosiersystem", Description: "Automatische Dosierung und Zufuehrung von Faerbemitteln ueber geschlossene Leitungen. Vermeidung offener Wannen.", HazardCategory: "material_environmental", NormReferences: []string{"EN ISO 11111-6:2016", "TRGS 401"}},
|
||||
{ID: "M458", ReductionType: "design", SubType: "inherent_safety", Name: "Antistatik-Ausstattung Textilmaschine", Description: "Erdung aller leitfaehigen Teile, antistatische Transportbaender, Luftbefeuchtung > 50% rF.", HazardCategory: "electrical", NormReferences: []string{"EN ISO 11111-1:2016", "EN 1127-1"}},
|
||||
{ID: "M459", ReductionType: "information", SubType: "instruction", Name: "Ergonomie-Unterweisung Textilarbeitsplaetze", Description: "Unterweisung zu Arbeitshaltung, Pausenregelung und ergonomischer Arbeitsplatzgestaltung an Webmaschinen.", HazardCategory: "ergonomic", NormReferences: []string{"EN ISO 11111-1:2016 Abschnitt 6"}},
|
||||
{ID: "M460", ReductionType: "design", SubType: "inherent_safety", Name: "Erdung und Potentialausgleich Gewebeauslauf", Description: "Leitfaehige Walzen und Ableitsysteme am Gewebeauslauf verhindern elektrostatische Aufladung.", HazardCategory: "electrical", NormReferences: []string{"EN 1127-1", "TRBS 2153"}},
|
||||
// Landmaschinen (M461-M474)
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
{ID: "M461", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Zapfwellenschutzhuelse nach ISO 5674", NameEN: "PTO guard per ISO 5674",
|
||||
DescriptionDE: "Formschluessige Schutzhuelse ueber Gelenkwelle und Zapfwellenstummel mit integrierten Sicherheitsketten.",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 5674:2004, ISO 4254-1:2013 Abschnitt 4.7", RiskReduction: 45},
|
||||
{ID: "M462", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Automatische Zapfwellenabschaltung", NameEN: "Automatic PTO shutdown",
|
||||
DescriptionDE: "Sensorbasierte Erkennung der Schutzhuelsen-Position. Automatische Abschaltung der Zapfwelle bei entfernter Huelse.",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 4254-1:2013", RiskReduction: 35},
|
||||
{ID: "M463", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "ROPS/FOPS Ueberrollschutzstruktur", NameEN: "ROPS/FOPS rollover protection",
|
||||
DescriptionDE: "Normgerechte Ueberrollschutzstruktur (ROPS) mit Sicherheitsgurt. Bei Forstarbeiten zusaetzlich FOPS.",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 3471:2008, ISO 4254-1:2013 Abschnitt 4.3", RiskReduction: 50},
|
||||
{ID: "M464", ReductionType: "information", SubType: "instruction",
|
||||
NameDE: "Hangfahrt-Schulung und Neigungsanzeige", NameEN: "Slope driving training and inclinometer",
|
||||
DescriptionDE: "Elektronische Neigungsanzeige in der Kabine mit Warnung ab 15 Grad. Pflichtschulung fuer Hangfahrten.",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 4254-1:2013 Abschnitt 6", RiskReduction: 15},
|
||||
{ID: "M465", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Schneidwerk-Verriegelung mit Nachlaufueberwachung", NameEN: "Cutting header interlock with rundown monitoring",
|
||||
DescriptionDE: "Verriegelter Zugang zum Schneidwerk. Oeffnung erst moeglich nach Stillstandskontrolle (Nachlauf < 5s).",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 4254-7:2017 Abschnitt 4.3", RiskReduction: 40},
|
||||
{ID: "M466", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Berstschutz-Hydraulikleitungen", NameEN: "Hydraulic hose burst protection",
|
||||
DescriptionDE: "Schlauchbruchventile an allen heb-/senk-relevanten Hydraulikzylindern. Schlauchleitungen mit Gewebeschlauch-Ueberziehern.",
|
||||
HazardCategory: "pneumatic_hydraulic", NormRef: "ISO 4254-1:2013 Abschnitt 4.10", RiskReduction: 35},
|
||||
{ID: "M467", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Geschlossenes Befuellsystem Feldspritze", NameEN: "Closed transfer system for sprayer",
|
||||
DescriptionDE: "Geschlossenes Chemikalien-Umfuellsystem (CTS) verhindert Hautkontakt beim Befuellen der Feldspritze.",
|
||||
HazardCategory: "material_environmental", NormRef: "ISO 4254-6:2020, Pflanzenschutz-Anwendungsverordnung", RiskReduction: 40},
|
||||
{ID: "M468", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Explosionsschutz Getreidesilo nach ATEX", NameEN: "Explosion protection grain silo per ATEX",
|
||||
DescriptionDE: "ATEX-konforme Ausfuehrung: Explosionsdruckentlastung, Funkenerkennung, Inertisierung, Erdung aller Metallteile.",
|
||||
HazardCategory: "fire_explosion", NormRef: "ATEX 2014/34/EU, EN 14491", RiskReduction: 45},
|
||||
{ID: "M469", ReductionType: "information", SubType: "instruction",
|
||||
NameDE: "Silo-Zugangsverfahren mit Rettungskonzept", NameEN: "Silo entry procedure with rescue plan",
|
||||
DescriptionDE: "Schriftliches Verfahren fuer Silobetreten: Freimessung, Anseilschutz, Sicherungsposten, Rettungsgeraet bereitstellen.",
|
||||
HazardCategory: "mechanical", NormRef: "DGUV Regel 113-004, ISO 4254-1:2013", RiskReduction: 20},
|
||||
{ID: "M470", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Personen-Erkennung autonomer Traktor", NameEN: "Person detection for autonomous tractor",
|
||||
DescriptionDE: "Redundantes Personenerkennungssystem (LiDAR + Kamera + Radar) mit automatischem Not-Stopp. PL d nach EN ISO 13849.",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 18497:2018, ISO 4254-1:2013", RiskReduction: 45},
|
||||
{ID: "M471", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Geo-Fencing und Geschwindigkeitsbegrenzung", NameEN: "Geo-fencing and speed limitation",
|
||||
DescriptionDE: "GPS-basierte Begrenzung des Einsatzgebiets. Automatische Geschwindigkeitsreduktion bei Annaeherung an Grenzbereiche.",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 18497:2018", RiskReduction: 25},
|
||||
{ID: "M472", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Schallisolierte Fahrerkabine", NameEN: "Sound-insulated cab",
|
||||
DescriptionDE: "Fahrerkabine mit Schalldaemmung auf < 80 dB(A) Innenpegel. Klimaanlage fuer geschlossenen Betrieb.",
|
||||
HazardCategory: "noise_vibration", NormRef: "ISO 4254-1:2013 Abschnitt 4.12", RiskReduction: 25},
|
||||
{ID: "M473", ReductionType: "design", SubType: "inherent_safety",
|
||||
NameDE: "Schwingungsgedaempfter Fahrersitz", NameEN: "Vibration-damped driver seat",
|
||||
DescriptionDE: "Aktiv oder passiv gefederter Fahrersitz mit Schwingungsdaempfung. Grenzwert A(8) < 0,5 m/s2 nach EU Vibrationsrichtlinie.",
|
||||
HazardCategory: "noise_vibration", NormRef: "ISO 5007:2003, Richtlinie 2002/44/EG", RiskReduction: 20},
|
||||
{ID: "M474", ReductionType: "protection", SubType: "safeguard",
|
||||
NameDE: "Mechanische Abstuetzung Dreipunktanbau", NameEN: "Mechanical support for three-point hitch",
|
||||
DescriptionDE: "Mechanische Stuetzvorrichtung (Stuetzbock) die unter angehobene Anbaugeraete gestellt wird vor Arbeiten im Gefahrbereich.",
|
||||
HazardCategory: "mechanical", NormRef: "ISO 4254-1:2013 Abschnitt 4.8", RiskReduction: 30},
|
||||
{ID: "M461", ReductionType: "protection", SubType: "safeguard", Name: "Zapfwellenschutzhuelse nach ISO 5674", Description: "Formschluessige Schutzhuelse ueber Gelenkwelle und Zapfwellenstummel mit integrierten Sicherheitsketten.", HazardCategory: "mechanical", NormReferences: []string{"ISO 5674:2004", "ISO 4254-1:2013 Abschnitt 4.7"}},
|
||||
{ID: "M462", ReductionType: "design", SubType: "inherent_safety", Name: "Automatische Zapfwellenabschaltung", Description: "Sensorbasierte Erkennung der Schutzhuelsen-Position. Automatische Abschaltung bei entfernter Huelse.", HazardCategory: "mechanical", NormReferences: []string{"ISO 4254-1:2013"}},
|
||||
{ID: "M463", ReductionType: "design", SubType: "inherent_safety", Name: "ROPS/FOPS Ueberrollschutzstruktur", Description: "Normgerechte Ueberrollschutzstruktur (ROPS) mit Sicherheitsgurt. Bei Forstarbeiten zusaetzlich FOPS.", HazardCategory: "mechanical", NormReferences: []string{"ISO 3471:2008", "ISO 4254-1:2013 Abschnitt 4.3"}},
|
||||
{ID: "M464", ReductionType: "information", SubType: "instruction", Name: "Hangfahrt-Schulung und Neigungsanzeige", Description: "Elektronische Neigungsanzeige in der Kabine mit Warnung ab 15 Grad. Pflichtschulung fuer Hangfahrten.", HazardCategory: "mechanical", NormReferences: []string{"ISO 4254-1:2013 Abschnitt 6"}},
|
||||
{ID: "M465", ReductionType: "protection", SubType: "safeguard", Name: "Schneidwerk-Verriegelung mit Nachlaufueberwachung", Description: "Verriegelter Zugang zum Schneidwerk. Oeffnung erst moeglich nach Stillstandskontrolle.", HazardCategory: "mechanical", NormReferences: []string{"ISO 4254-7:2017 Abschnitt 4.3"}},
|
||||
{ID: "M466", ReductionType: "design", SubType: "inherent_safety", Name: "Berstschutz-Hydraulikleitungen", Description: "Schlauchbruchventile an heb-/senk-relevanten Hydraulikzylindern. Schlauchleitungen mit Gewebeschlauch-Ueberziehern.", HazardCategory: "pneumatic_hydraulic", NormReferences: []string{"ISO 4254-1:2013 Abschnitt 4.10"}},
|
||||
{ID: "M467", ReductionType: "protection", SubType: "safeguard", Name: "Geschlossenes Befuellsystem Feldspritze", Description: "Geschlossenes Chemikalien-Umfuellsystem (CTS) verhindert Hautkontakt beim Befuellen der Feldspritze.", HazardCategory: "material_environmental", NormReferences: []string{"ISO 4254-6:2020", "Pflanzenschutz-Anwendungsverordnung"}},
|
||||
{ID: "M468", ReductionType: "design", SubType: "inherent_safety", Name: "Explosionsschutz Getreidesilo nach ATEX", Description: "ATEX-konforme Ausfuehrung: Explosionsdruckentlastung, Funkenerkennung, Inertisierung, Erdung.", HazardCategory: "fire_explosion", NormReferences: []string{"ATEX 2014/34/EU", "EN 14491"}},
|
||||
{ID: "M469", ReductionType: "information", SubType: "instruction", Name: "Silo-Zugangsverfahren mit Rettungskonzept", Description: "Schriftliches Verfahren fuer Silobetreten: Freimessung, Anseilschutz, Sicherungsposten.", HazardCategory: "mechanical", NormReferences: []string{"DGUV Regel 113-004", "ISO 4254-1:2013"}},
|
||||
{ID: "M470", ReductionType: "design", SubType: "inherent_safety", Name: "Personen-Erkennung autonomer Traktor", Description: "Redundantes Personenerkennungssystem (LiDAR + Kamera + Radar) mit automatischem Not-Stopp. PL d.", HazardCategory: "mechanical", NormReferences: []string{"ISO 18497:2018", "ISO 4254-1:2013"}},
|
||||
{ID: "M471", ReductionType: "protection", SubType: "safeguard", Name: "Geo-Fencing und Geschwindigkeitsbegrenzung", Description: "GPS-basierte Begrenzung des Einsatzgebiets. Automatische Geschwindigkeitsreduktion bei Annaeherung.", HazardCategory: "mechanical", NormReferences: []string{"ISO 18497:2018"}},
|
||||
{ID: "M472", ReductionType: "protection", SubType: "safeguard", Name: "Schallisolierte Fahrerkabine", Description: "Fahrerkabine mit Schalldaemmung auf < 80 dB(A) Innenpegel. Klimaanlage fuer geschlossenen Betrieb.", HazardCategory: "noise_vibration", NormReferences: []string{"ISO 4254-1:2013 Abschnitt 4.12"}},
|
||||
{ID: "M473", ReductionType: "design", SubType: "inherent_safety", Name: "Schwingungsgedaempfter Fahrersitz", Description: "Aktiv oder passiv gefederter Fahrersitz mit Schwingungsdaempfung. A(8) < 0.5 m/s2.", HazardCategory: "noise_vibration", NormReferences: []string{"ISO 5007:2003", "Richtlinie 2002/44/EG"}},
|
||||
{ID: "M474", ReductionType: "protection", SubType: "safeguard", Name: "Mechanische Abstuetzung Dreipunktanbau", Description: "Mechanische Stuetzvorrichtung die unter angehobene Anbaugeraete gestellt wird vor Arbeiten im Gefahrbereich.", HazardCategory: "mechanical", NormReferences: []string{"ISO 4254-1:2013 Abschnitt 4.8"}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,52 @@ func (fm *FailureModeEntry) CalculateRPZ() int {
|
||||
// RPZThresholdAction is the RPZ value above which corrective action is required.
|
||||
const RPZThresholdAction = 100
|
||||
|
||||
// CalculateAP computes the AIAG-VDA Action Priority (H/M/L).
|
||||
// Replaces pure RPN/RPZ with a 3D severity-occurrence-detection priority matrix
|
||||
// per the AIAG-VDA FMEA Handbook (2019). Returns "H", "M", or "L".
|
||||
func CalculateAP(s, o, d int) string {
|
||||
if s >= 9 {
|
||||
if o >= 4 || d >= 7 {
|
||||
return "H"
|
||||
}
|
||||
if o >= 2 || d >= 5 {
|
||||
return "M"
|
||||
}
|
||||
return "L"
|
||||
}
|
||||
if s >= 7 {
|
||||
if o >= 5 || d >= 8 {
|
||||
return "H"
|
||||
}
|
||||
if o >= 3 || d >= 5 {
|
||||
return "M"
|
||||
}
|
||||
return "L"
|
||||
}
|
||||
if s >= 5 {
|
||||
if o >= 7 || d >= 9 {
|
||||
return "H"
|
||||
}
|
||||
if o >= 4 || d >= 7 {
|
||||
return "M"
|
||||
}
|
||||
return "L"
|
||||
}
|
||||
// S < 5
|
||||
if o >= 8 && d >= 9 {
|
||||
return "H"
|
||||
}
|
||||
if o >= 6 || d >= 8 {
|
||||
return "M"
|
||||
}
|
||||
return "L"
|
||||
}
|
||||
|
||||
// CalculateAPForFM computes AP for a FailureModeEntry.
|
||||
func (fm *FailureModeEntry) CalculateAPForFM() string {
|
||||
return CalculateAP(fm.DefaultSeverity, fm.DefaultOccurrence, fm.DefaultDetection)
|
||||
}
|
||||
|
||||
// AssessmentType represents the type of risk assessment
|
||||
type AssessmentType string
|
||||
|
||||
|
||||
@@ -142,7 +142,17 @@ func matchNorm(norm NormReference, machineType string, hazardSet, tagSet map[str
|
||||
}
|
||||
}
|
||||
|
||||
// groupByType sorts suggestions by confidence and groups them by norm type.
|
||||
// Per-type caps for norm suggestions to avoid overwhelming the user.
|
||||
// A professional typically references 3-5 A-norms, 5-10 B-norms, and 3-8 C-norms.
|
||||
const (
|
||||
maxANorms = 5
|
||||
maxB1Norms = 8
|
||||
maxB2Norms = 10
|
||||
maxCNorms = 10
|
||||
)
|
||||
|
||||
// groupByType sorts suggestions by confidence, groups them by norm type,
|
||||
// and applies per-type caps to keep the list manageable.
|
||||
func groupByType(suggestions []NormSuggestion) *NormSuggestionResult {
|
||||
sort.Slice(suggestions, func(i, j int) bool {
|
||||
return suggestions[i].Confidence > suggestions[j].Confidence
|
||||
@@ -158,17 +168,25 @@ func groupByType(suggestions []NormSuggestion) *NormSuggestionResult {
|
||||
for _, s := range suggestions {
|
||||
switch s.Norm.NormType {
|
||||
case "A":
|
||||
result.ANorms = append(result.ANorms, s)
|
||||
if len(result.ANorms) < maxANorms {
|
||||
result.ANorms = append(result.ANorms, s)
|
||||
}
|
||||
case "B1":
|
||||
result.B1Norms = append(result.B1Norms, s)
|
||||
if len(result.B1Norms) < maxB1Norms {
|
||||
result.B1Norms = append(result.B1Norms, s)
|
||||
}
|
||||
case "B2":
|
||||
result.B2Norms = append(result.B2Norms, s)
|
||||
if len(result.B2Norms) < maxB2Norms {
|
||||
result.B2Norms = append(result.B2Norms, s)
|
||||
}
|
||||
case "C":
|
||||
result.CNorms = append(result.CNorms, s)
|
||||
if len(result.CNorms) < maxCNorms {
|
||||
result.CNorms = append(result.CNorms, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.Total = len(suggestions)
|
||||
result.Total = len(result.ANorms) + len(result.B1Norms) + len(result.B2Norms) + len(result.CNorms)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@ type MatchInput struct {
|
||||
// FailureModes are the active failure mode IDs relevant for this project.
|
||||
// Used to filter patterns that require specific failure modes.
|
||||
FailureModes []string `json:"failure_modes,omitempty"`
|
||||
// MachineTypes are the industry sectors / machine types for this project.
|
||||
// Patterns with MachineTypes filter only fire if at least one matches.
|
||||
// Empty = all patterns fire (backwards compatible).
|
||||
MachineTypes []string `json:"machine_types,omitempty"`
|
||||
}
|
||||
|
||||
// MatchOutput contains the results of pattern matching.
|
||||
@@ -62,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.
|
||||
@@ -90,44 +96,11 @@ type PatternEngine struct {
|
||||
}
|
||||
|
||||
// NewPatternEngine creates a PatternEngine with all pattern sources and resolver.
|
||||
// Pattern registration is in pattern_registry.go (collectAllPatterns).
|
||||
func NewPatternEngine() *PatternEngine {
|
||||
// Combine all pattern sources
|
||||
patterns := GetBuiltinHazardPatterns() // HP001-HP044
|
||||
patterns = append(patterns, GetExtendedHazardPatterns()...) // HP045+ from rule library
|
||||
patterns = append(patterns, GetPressHazardPatterns()...) // HP045-HP058 press-specific
|
||||
patterns = append(patterns, GetCobotHazardPatterns()...) // HP059-HP065 cobot-specific
|
||||
patterns = append(patterns, GetOperationalHazardPatterns()...) // HP066-HP093 operational states
|
||||
patterns = append(patterns, GetDGUVExtendedPatterns()...) // HP094-HP133 DGUV themes
|
||||
patterns = append(patterns, GetExtendedHazardPatterns2()...) // HP134-HP173 additional hazards
|
||||
patterns = append(patterns, GetElevatorPatterns()...) // HP174-HP198 elevator/lift
|
||||
patterns = append(patterns, GetAGVAgriPatterns()...) // HP199-HP228 AGV + agricultural
|
||||
patterns = append(patterns, GetFoodProcessingPatterns()...) // HP300-HP319 food processing
|
||||
patterns = append(patterns, GetPackagingPatterns()...) // HP320-HP334 packaging machines
|
||||
patterns = append(patterns, GetLaserPatterns()...) // HP335-HP349 laser machines
|
||||
patterns = append(patterns, GetMedicalDevicePatterns()...) // HP350-HP364 medical devices (IEC 60601)
|
||||
patterns = append(patterns, GetPressureEquipmentPatterns()...) // HP365-HP374 pressure equipment
|
||||
patterns = append(patterns, GetConstructionPatterns()...) // HP400-HP419 construction/crane
|
||||
patterns = append(patterns, GetForestryConveyorPatterns()...) // HP420-HP450 forestry/conveyor
|
||||
patterns = append(patterns, GetPlasticsMetalPatterns()...) // HP500-HP529 plastics + metalworking
|
||||
patterns = append(patterns, GetWeldingGlassTextilePatterns()...) // HP530-HP559 welding + glass + textile
|
||||
patterns = append(patterns, GetSpecificMachinePatterns()...) // HP730-HP755 pressure/wind/solar/battery
|
||||
patterns = append(patterns, GetSpecificMachinePatterns2()...) // HP756-HP784 escalator/pool/playground/fitness/laundry/glass
|
||||
patterns = append(patterns, GetCyberExtendedPatterns()...) // HP800-HP829 software faults/cyber-security
|
||||
patterns = append(patterns, GetCyberExtendedPatterns2()...) // HP830-HP844 AI-ML specific
|
||||
patterns = append(patterns, GetCyberExtendedPatterns3()...) // HP845-HP864 network/communication + HMI
|
||||
patterns = append(patterns, GetWorkshopPatterns()...) // HP600-HP664 cross-machine workshop
|
||||
patterns = append(patterns, GetMaintenanceExtPatterns()...) // HP700-HP729,HP900-HP934 maintenance lifecycle
|
||||
patterns = append(patterns, GetFinalPatternsA()...) // HP1000-HP1084 mechanical body-part variants
|
||||
patterns = append(patterns, GetFinalPatternsB()...) // HP1085-HP1169 electrical/thermal/chemical/bio/radiation
|
||||
patterns = append(patterns, GetFinalPatternsC()...) // HP1170-HP1254 software/control/org/ergonomic/fire
|
||||
patterns = append(patterns, GetFinalPatternsD()...) // HP1255-HP1335 lifecycle/special situations
|
||||
patterns = append(patterns, GetCNCHazardPatterns()...) // HP1400-HP1419 CNC/metalworking part 1 (Phase 3)
|
||||
patterns = append(patterns, GetCNCHazardPatternsExt()...) // HP1420-HP1434 CNC/metalworking part 2 (Phase 3)
|
||||
patterns = append(patterns, GetVDMAIndustryPatterns()...) // HP1500-HP1549 VDMA sectors (Phase 3)
|
||||
patterns = append(patterns, GetTextileAgriPatterns()...) // HP1550-HP1584 Textile + Agri (Phase 5)
|
||||
return &PatternEngine{
|
||||
resolver: NewTagResolver(),
|
||||
patterns: patterns,
|
||||
patterns: collectAllPatterns(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,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 {
|
||||
@@ -317,6 +292,22 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
||||
// patternMatches checks if a pattern fires given the resolved tag set, lifecycle phases,
|
||||
// operational states, and state transitions.
|
||||
func patternMatches(p HazardPattern, tagSet map[string]bool, input MatchInput) bool {
|
||||
// If pattern requires specific machine types, project must match at least one.
|
||||
// Patterns without MachineTypes fire for ALL projects (backwards compatible).
|
||||
if len(p.MachineTypes) > 0 && len(input.MachineTypes) > 0 {
|
||||
found := false
|
||||
mtSet := toSet(input.MachineTypes)
|
||||
for _, mt := range p.MachineTypes {
|
||||
if mtSet[mt] {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// All required component tags must be present (AND)
|
||||
for _, t := range p.RequiredComponentTags {
|
||||
if !tagSet[t] {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package iace
|
||||
|
||||
// collectAllPatterns gathers hazard patterns from all registered sources.
|
||||
// This function is called by NewPatternEngine() to build the complete pattern set.
|
||||
// New pattern sources are registered here.
|
||||
func collectAllPatterns() []HazardPattern {
|
||||
patterns := GetBuiltinHazardPatterns() // HP001-HP044
|
||||
patterns = append(patterns, GetExtendedHazardPatterns()...) // HP045+ from rule library
|
||||
patterns = append(patterns, GetPressHazardPatterns()...) // HP045-HP058 press-specific
|
||||
patterns = append(patterns, GetCobotHazardPatterns()...) // HP059-HP065 cobot-specific
|
||||
patterns = append(patterns, GetOperationalHazardPatterns()...) // HP066-HP093 operational states
|
||||
patterns = append(patterns, GetDGUVExtendedPatterns()...) // HP094-HP133 DGUV themes
|
||||
patterns = append(patterns, GetExtendedHazardPatterns2()...) // HP134-HP173 additional hazards
|
||||
patterns = append(patterns, GetElevatorPatterns()...) // HP174-HP198 elevator/lift
|
||||
patterns = append(patterns, GetAGVAgriPatterns()...) // HP199-HP228 AGV + agricultural
|
||||
patterns = append(patterns, GetFoodProcessingPatterns()...) // HP300-HP319 food processing
|
||||
patterns = append(patterns, GetPackagingPatterns()...) // HP320-HP334 packaging machines
|
||||
patterns = append(patterns, GetLaserPatterns()...) // HP335-HP349 laser machines
|
||||
patterns = append(patterns, GetMedicalDevicePatterns()...) // HP350-HP364 medical devices (IEC 60601)
|
||||
patterns = append(patterns, GetPressureEquipmentPatterns()...) // HP365-HP374 pressure equipment
|
||||
patterns = append(patterns, GetConstructionPatterns()...) // HP400-HP419 construction/crane
|
||||
patterns = append(patterns, GetForestryConveyorPatterns()...) // HP420-HP450 forestry/conveyor
|
||||
patterns = append(patterns, GetPlasticsMetalPatterns()...) // HP500-HP529 plastics + metalworking
|
||||
patterns = append(patterns, GetWeldingGlassTextilePatterns()...) // HP530-HP559 welding + glass + textile
|
||||
patterns = append(patterns, GetSpecificMachinePatterns()...) // HP730-HP755 pressure/wind/solar/battery
|
||||
patterns = append(patterns, GetSpecificMachinePatterns2()...) // HP756-HP784 escalator/pool/playground/fitness/laundry/glass
|
||||
patterns = append(patterns, GetCyberExtendedPatterns()...) // HP800-HP829 software faults/cyber-security
|
||||
patterns = append(patterns, GetCyberExtendedPatterns2()...) // HP830-HP844 AI-ML specific
|
||||
patterns = append(patterns, GetCyberExtendedPatterns3()...) // HP845-HP864 network/communication + HMI
|
||||
patterns = append(patterns, GetWorkshopPatterns()...) // HP600-HP664 cross-machine workshop
|
||||
patterns = append(patterns, GetMaintenanceExtPatterns()...) // HP700-HP729,HP900-HP934 maintenance lifecycle
|
||||
patterns = append(patterns, GetFinalPatternsA()...) // HP1000-HP1084 mechanical body-part variants
|
||||
patterns = append(patterns, GetFinalPatternsB()...) // HP1085-HP1169 electrical/thermal/chemical/bio/radiation
|
||||
patterns = append(patterns, GetFinalPatternsC()...) // HP1170-HP1254 software/control/org/ergonomic/fire
|
||||
patterns = append(patterns, GetFinalPatternsD()...) // HP1255-HP1335 lifecycle/special situations
|
||||
patterns = append(patterns, GetCNCHazardPatterns()...) // HP1400-HP1419 CNC/metalworking part 1 (Phase 3)
|
||||
patterns = append(patterns, GetCNCHazardPatternsExt()...) // HP1420-HP1434 CNC/metalworking part 2 (Phase 3)
|
||||
patterns = append(patterns, GetVDMAIndustryPatterns()...) // HP1500-HP1549 VDMA sectors (Phase 3)
|
||||
patterns = append(patterns, GetTextileAgriPatterns()...) // HP1550-HP1584 Textile + Agri (Phase 5)
|
||||
patterns = append(patterns, GetRobotCellPatterns()...) // HP1600-HP1649 Robot cell (GT benchmark)
|
||||
patterns = append(patterns, GetRobotCellPatternsExt()...) // HP1650-HP1699 Robot cell extended (GT gaps)
|
||||
return patterns
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user