Compare commits
2 Commits
081e4f057a
...
6263462ba3
| Author | SHA1 | Date | |
|---|---|---|---|
| 6263462ba3 | |||
| eb48c5bd1e |
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { ChecklistView } from './ChecklistView'
|
||||
import { ResultsTabsView } from './ResultsTabsView'
|
||||
import { PreScanWizard, useScanContext, isContextComplete } from './PreScanWizard'
|
||||
import { safeSetItem } from './storageHelpers'
|
||||
|
||||
@@ -312,41 +313,9 @@ export function DocCheckTab() {
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-700">{error}</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{/* Results — als Tab-Ansicht (Übersicht/Cookies/DSE/Impressum/AGB/Banner/Mail) */}
|
||||
{results && results.results && (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
|
||||
<ChecklistView results={results.results} />
|
||||
|
||||
{/* Cookie Banner Result */}
|
||||
{results.cookie_banner_result && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-200">
|
||||
<h4 className="text-sm font-semibold text-gray-800 mb-2">Cookie-Banner</h4>
|
||||
<div className="text-sm text-gray-600">
|
||||
{results.cookie_banner_result.banner_detected
|
||||
? `Banner erkannt: ${results.cookie_banner_result.banner_provider || 'unbekannt'}`
|
||||
: 'Kein Banner erkannt'}
|
||||
</div>
|
||||
{results.cookie_banner_result.banner_checks?.violations?.length > 0 && (
|
||||
<div className="mt-2 space-y-1">
|
||||
{results.cookie_banner_result.banner_checks.violations.map((v: any, i: number) => (
|
||||
<div key={i} className="text-xs text-red-600 flex items-start gap-1.5">
|
||||
<span className="shrink-0 mt-0.5">!!</span>
|
||||
<span>{v.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Status */}
|
||||
{results.email_status && (
|
||||
<div className="mt-3 text-xs text-gray-500 flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${results.email_status === 'sent' ? 'bg-green-400' : 'bg-gray-300'}`} />
|
||||
E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ResultsTabsView results={results} />
|
||||
)}
|
||||
|
||||
{/* History */}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* ResultsTabsView — strukturierte Tab-Ansicht der Audit-Ergebnisse.
|
||||
*
|
||||
* Statt einer langen Scroll-Seite gibt es:
|
||||
* 1. Übersicht (Score + GF-Kurzfassung)
|
||||
* 2. Cookies (3-Quellen-Compliance-Vergleich + Vendor-/Cookie-Listen)
|
||||
* 3. Datenschutzerklärung
|
||||
* 4. Impressum
|
||||
* 5. AGB / Widerruf
|
||||
* 6. Banner (Cookie-Banner-Checks)
|
||||
* 7. Vollständige Mail (HTML-Preview)
|
||||
*
|
||||
* Tab-Headers sticky oben, Content scrollbar unten.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import { ChecklistView } from './ChecklistView'
|
||||
|
||||
interface ResultsTabsViewProps {
|
||||
results: any
|
||||
}
|
||||
|
||||
type TabId = 'overview' | 'cookies' | 'dse' | 'impressum' | 'agb' | 'banner' | 'mail'
|
||||
|
||||
const TABS: { id: TabId; label: string; icon: string }[] = [
|
||||
{ id: 'overview', label: 'Übersicht', icon: '◉' },
|
||||
{ id: 'cookies', label: 'Cookies & VVT', icon: '🍪' },
|
||||
{ id: 'dse', label: 'Datenschutzerkl.', icon: '📄' },
|
||||
{ id: 'impressum', label: 'Impressum', icon: '🏢' },
|
||||
{ id: 'agb', label: 'AGB / Widerruf', icon: '⚖️' },
|
||||
{ id: 'banner', label: 'Cookie-Banner', icon: '🎛' },
|
||||
{ id: 'mail', label: 'Mail-Vorschau', icon: '✉️' },
|
||||
]
|
||||
|
||||
export function ResultsTabsView({ results }: ResultsTabsViewProps) {
|
||||
const [active, setActive] = useState<TabId>('overview')
|
||||
|
||||
const r = results || {}
|
||||
const docs: any[] = r.results || []
|
||||
const banner = r.banner_result || r.cookie_banner_result || {}
|
||||
const cmpVendors: any[] = r.cmp_vendors || []
|
||||
const cookieAudit = r.cookie_audit || {}
|
||||
|
||||
const docsByType = useMemo(() => {
|
||||
const m: Record<string, any> = {}
|
||||
for (const d of docs) {
|
||||
const t = (d.doc_type || '').toLowerCase()
|
||||
if (!m[t]) m[t] = d
|
||||
}
|
||||
return m
|
||||
}, [docs])
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden bg-white">
|
||||
{/* Sticky Tab-Header */}
|
||||
<div className="flex border-b border-gray-200 bg-gray-50 overflow-x-auto sticky top-0 z-10">
|
||||
{TABS.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => setActive(t.id)}
|
||||
className={`px-4 py-3 text-sm font-medium whitespace-nowrap border-b-2 transition-colors ${
|
||||
active === t.id
|
||||
? 'border-purple-600 text-purple-700 bg-white'
|
||||
: 'border-transparent text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-1.5">{t.icon}</span>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab-Content */}
|
||||
<div className="p-4 min-h-[400px]">
|
||||
{active === 'overview' && <OverviewTab results={r} />}
|
||||
{active === 'cookies' && (
|
||||
<CookiesTab
|
||||
audit={cookieAudit}
|
||||
vendors={cmpVendors}
|
||||
banner={banner}
|
||||
/>
|
||||
)}
|
||||
{active === 'dse' && <DocTab doc={docsByType['dse']} label="Datenschutzerklärung" />}
|
||||
{active === 'impressum' && <DocTab doc={docsByType['impressum']} label="Impressum" />}
|
||||
{active === 'agb' && <AgbWiderrufTab docs={docsByType} />}
|
||||
{active === 'banner' && <BannerTab banner={banner} />}
|
||||
{active === 'mail' && <MailPreviewTab results={r} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Übersicht ──────────────────────────────────────────────────────────
|
||||
function OverviewTab({ results }: { results: any }) {
|
||||
const totalDocs = results.total_documents || (results.results?.length ?? 0)
|
||||
const totalFindings = results.total_findings ?? 0
|
||||
const banner = results.banner_result || results.cookie_banner_result || {}
|
||||
const score = banner.compliance_score ?? banner.completeness_pct ?? null
|
||||
const emailStatus = results.email_status
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Kpi label="Geprüfte Dokumente" value={totalDocs} />
|
||||
<Kpi label="Findings gesamt" value={totalFindings} tone={totalFindings > 5 ? 'warn' : 'ok'} />
|
||||
<Kpi label="Vendors erkannt" value={results.cmp_vendors?.length || 0} />
|
||||
<Kpi label="Score" value={score !== null ? `${score}%` : '—'}
|
||||
tone={score === null ? 'neutral' : score >= 80 ? 'ok' : score >= 60 ? 'warn' : 'bad'} />
|
||||
</div>
|
||||
|
||||
{emailStatus && (
|
||||
<div className={`text-sm px-3 py-2 rounded ${
|
||||
emailStatus === 'sent' ? 'bg-green-50 text-green-800' : 'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
E-Mail: {emailStatus === 'sent' ? '✓ Gesendet an Empfänger' : emailStatus}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded p-3 text-xs text-blue-900">
|
||||
<strong>Wo welcher Inhalt steckt:</strong> in den Tabs oben findest du die
|
||||
Detail-Auswertung pro Doc-Typ. Im Cookie-Tab steht der 3-Quellen-Compliance-
|
||||
Vergleich (deklariert vs Browser vs Library) — das ist der wichtigste
|
||||
rechtliche Knackpunkt. Banner-Tab zeigt die echten Browser-Phasen-Checks.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Kpi({ label, value, tone = 'neutral' }: { label: string; value: any; tone?: string }) {
|
||||
const colors: Record<string, string> = {
|
||||
ok: 'text-green-700 bg-green-50 border-green-200',
|
||||
warn: 'text-amber-700 bg-amber-50 border-amber-200',
|
||||
bad: 'text-red-700 bg-red-50 border-red-200',
|
||||
neutral: 'text-gray-700 bg-gray-50 border-gray-200',
|
||||
}
|
||||
return (
|
||||
<div className={`border rounded p-3 ${colors[tone]}`}>
|
||||
<div className="text-[10px] uppercase tracking-wider opacity-70">{label}</div>
|
||||
<div className="text-2xl font-bold mt-1">{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Cookies & VVT ──────────────────────────────────────────────────────
|
||||
function CookiesTab({ audit, vendors, banner }: { audit: any; vendors: any[]; banner: any }) {
|
||||
const declared = audit?.declared_count ?? 0
|
||||
const browser = audit?.browser_count ?? 0
|
||||
const both = (audit?.compliant ?? []).length
|
||||
const undecl = (audit?.undeclared_in_browser ?? []).length
|
||||
const decOnly = (audit?.declared_not_loaded ?? []).length
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Top-Bar mit Counts */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-5 gap-2">
|
||||
<Kpi label="Deklariert" value={declared} />
|
||||
<Kpi label="Im Browser" value={browser} />
|
||||
<Kpi label="Compliant" value={both} tone="ok" />
|
||||
<Kpi label="Undokumentiert" value={undecl} tone={undecl > 0 ? 'bad' : 'ok'} />
|
||||
<Kpi label="Nicht geladen" value={decOnly} tone={decOnly > 0 ? 'warn' : 'neutral'} />
|
||||
</div>
|
||||
|
||||
{/* 3-Spalten-Vergleichstabelle */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<CookieColumn
|
||||
title={`❌ Undokumentiert (${undecl})`}
|
||||
tone="bad"
|
||||
subtitle="Geladen ABER nicht in der Richtlinie — Art. 13(1)(c) DSGVO Verstoß"
|
||||
cookies={audit?.undeclared_in_browser ?? []}
|
||||
/>
|
||||
<CookieColumn
|
||||
title={`✓ Compliant (${both})`}
|
||||
tone="ok"
|
||||
subtitle="Beide Quellen stimmen überein"
|
||||
cookies={audit?.compliant ?? []}
|
||||
/>
|
||||
<CookieColumn
|
||||
title={`⚠️ Nicht geladen (${decOnly})`}
|
||||
tone="warn"
|
||||
subtitle="In Richtlinie deklariert, aber bei diesem Lauf nicht im Browser"
|
||||
cookies={audit?.declared_not_loaded ?? []}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Vendor-Liste (deduped) */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2 text-gray-800">
|
||||
Vendor-Liste ({vendors.length} unique nach Deduplizierung)
|
||||
</h3>
|
||||
<div className="overflow-x-auto border border-gray-200 rounded">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="text-left px-3 py-2">Vendor</th>
|
||||
<th className="text-left px-3 py-2">Kategorie</th>
|
||||
<th className="text-left px-3 py-2">Quelle</th>
|
||||
<th className="text-right px-3 py-2">Cookies</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vendors.map((v, i) => (
|
||||
<tr key={i} className="border-t border-gray-100 hover:bg-gray-50">
|
||||
<td className="px-3 py-2 font-medium">{v.name}</td>
|
||||
<td className="px-3 py-2 text-gray-600">{v.category || '—'}</td>
|
||||
<td className="px-3 py-2 text-gray-500 font-mono text-[10px]">
|
||||
{v.source || '—'}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">{(v.cookies || []).length}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CookieColumn({ title, tone, subtitle, cookies }: {
|
||||
title: string; tone: string; subtitle: string; cookies: string[]
|
||||
}) {
|
||||
const colors: Record<string, string> = {
|
||||
bad: 'bg-red-50 border-red-200 text-red-900',
|
||||
ok: 'bg-green-50 border-green-200 text-green-900',
|
||||
warn: 'bg-amber-50 border-amber-200 text-amber-900',
|
||||
}
|
||||
return (
|
||||
<div className={`border rounded p-3 ${colors[tone]}`}>
|
||||
<div className="text-xs font-semibold mb-1">{title}</div>
|
||||
<div className="text-[10px] opacity-80 mb-2">{subtitle}</div>
|
||||
<div className="font-mono text-[10px] max-h-56 overflow-auto">
|
||||
{cookies.length === 0 && <span className="opacity-60">— keine —</span>}
|
||||
{cookies.map((c, i) => (
|
||||
<div key={i} className="py-0.5">{c}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Generic Doc-Tab ────────────────────────────────────────────────────
|
||||
function DocTab({ doc, label }: { doc: any; label: string }) {
|
||||
if (!doc) return <Empty label={label} />
|
||||
const checks = doc.checks || []
|
||||
const failed = checks.filter((c: any) => !c.passed && !c.skipped)
|
||||
const passed = checks.filter((c: any) => c.passed)
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold">{label}</h3>
|
||||
<div className="text-xs text-gray-600">
|
||||
{doc.word_count?.toLocaleString('de-DE') || 0} Wörter ·{' '}
|
||||
<span className="text-red-600">{failed.length} Findings</span> ·{' '}
|
||||
<span className="text-green-600">{passed.length} OK</span>
|
||||
</div>
|
||||
</div>
|
||||
{doc.url && (
|
||||
<a href={doc.url} target="_blank" rel="noreferrer"
|
||||
className="text-xs text-blue-600 hover:underline break-all">
|
||||
{doc.url}
|
||||
</a>
|
||||
)}
|
||||
<ChecklistView results={[doc]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgbWiderrufTab({ docs }: { docs: Record<string, any> }) {
|
||||
const agb = docs['agb'] || docs['nutzungsbedingungen']
|
||||
const wid = docs['widerruf']
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">AGB / Nutzungsbedingungen</h3>
|
||||
{agb ? <ChecklistView results={[agb]} /> : <Empty label="AGB" inline />}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold mb-2">Widerrufsbelehrung</h3>
|
||||
{wid ? <ChecklistView results={[wid]} /> : <Empty label="Widerruf" inline />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function BannerTab({ banner }: { banner: any }) {
|
||||
if (!banner || Object.keys(banner).length === 0) return <Empty label="Cookie-Banner" />
|
||||
const phases = banner.phases || {}
|
||||
const violations = banner.banner_checks?.violations || []
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs text-gray-700">
|
||||
Banner erkannt: <strong>{banner.banner_detected ? 'Ja' : 'Nein'}</strong> ·{' '}
|
||||
Provider: <strong>{banner.banner_provider || '—'}</strong> ·{' '}
|
||||
Verstöße: <strong>{violations.length}</strong>
|
||||
</div>
|
||||
{violations.length > 0 && (
|
||||
<div className="border border-red-200 bg-red-50 rounded p-3">
|
||||
<div className="text-xs font-semibold text-red-800 mb-2">Verstöße</div>
|
||||
<ul className="text-xs text-red-900 space-y-1">
|
||||
{violations.map((v: any, i: number) => (
|
||||
<li key={i}>• {v.label || v.message || JSON.stringify(v)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{Object.entries(phases).map(([name, ph]: [string, any]) => (
|
||||
<div key={name} className="border border-gray-200 rounded p-2">
|
||||
<div className="text-[10px] uppercase text-gray-500">{name}</div>
|
||||
<div className="text-xs mt-1">
|
||||
Cookies: <strong>{ph.cookies?.length || 0}</strong>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
Vendors: <strong>{ph.vendors?.length || 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MailPreviewTab({ results }: { results: any }) {
|
||||
return (
|
||||
<div className="text-xs text-gray-600 space-y-2">
|
||||
<p>
|
||||
Die vollständige Mail wurde {results.email_status === 'sent' ? 'gesendet' : 'erstellt'}.
|
||||
Snapshot-ID:{' '}
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded">{results.check_id || '—'}</code>
|
||||
</p>
|
||||
{results.check_id && (
|
||||
<a
|
||||
href={`/api/compliance/agent/snapshots/${results.check_id}/pdf`}
|
||||
target="_blank" rel="noreferrer"
|
||||
className="inline-block text-purple-600 hover:underline"
|
||||
>
|
||||
→ PDF der Mail herunterladen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Empty({ label, inline }: { label: string; inline?: boolean }) {
|
||||
return (
|
||||
<div className={`text-xs text-gray-500 ${inline ? '' : 'py-8 text-center'}`}>
|
||||
Keine Daten für „{label}" in diesem Lauf.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package iace
|
||||
|
||||
// Minimum-distance library — Task #18.
|
||||
//
|
||||
// Anchor source: OSHA 29 CFR 1910 Subpart O (US Federal Public Domain,
|
||||
// 17 U.S.C. §105). The values below are reproduced verbatim from the
|
||||
// Federal Code; conversions to metric are mathematical and carry no
|
||||
// copyright. Engineering rounding to safe-side mm values is BreakPilot's
|
||||
// recommendation and labelled as such.
|
||||
//
|
||||
// EU norm equivalents (EN ISO 13857, EN 349, EN 13855, EN 1010) are
|
||||
// referenced by identifier only — no values are reproduced, because
|
||||
// DIN/Beuth retain copyright on the wording. The DINComparisonNote
|
||||
// field carries a human-curated judgement on whether the EU norm is
|
||||
// stricter / looser / equivalent — this is a qualitative observation
|
||||
// about a publicly available document, not a copy of its text.
|
||||
//
|
||||
// See LICENSE_RULES.md and project_attribution_strategy.md for the
|
||||
// licensing logic. The OSHA values are R1 (verbatim public domain);
|
||||
// the recommended metric values are BreakPilot engineering output (R3
|
||||
// own-work). DIN references are R3 identifier-only.
|
||||
|
||||
// MinimumDistanceUnit denotes the original unit system of the source.
|
||||
type MinimumDistanceUnit string
|
||||
|
||||
const (
|
||||
UnitInch MinimumDistanceUnit = "inch"
|
||||
UnitFoot MinimumDistanceUnit = "foot"
|
||||
UnitMeter MinimumDistanceUnit = "meter"
|
||||
UnitMM MinimumDistanceUnit = "mm"
|
||||
)
|
||||
|
||||
// MinimumDistance is the data contract for a single safety-distance rule.
|
||||
// It can be (a) a fixed gap value, (b) a distance range, or (c) a formula
|
||||
// like OSHA's Ds = 63 in/s × Ts (hand-speed constant).
|
||||
type MinimumDistance struct {
|
||||
ID string `json:"id"` // MD_OSHA_001
|
||||
// Source identifier — full CFR citation or norm reference.
|
||||
SourceCFR string `json:"source_cfr,omitempty"` // "29 CFR §1910.217(c)(1)(i)"
|
||||
SourceTable string `json:"source_table,omitempty"` // "Table O-10"
|
||||
License string `json:"license"` // "US Federal Public Domain"
|
||||
LicenseRule int `json:"license_rule"` // 1 / 2 / 3 (see LICENSE_RULES.md)
|
||||
|
||||
// Original verbatim value in the source's own unit.
|
||||
OriginalUnit MinimumDistanceUnit `json:"original_unit"`
|
||||
OriginalValue float64 `json:"original_value,omitempty"`
|
||||
OriginalMin float64 `json:"original_min,omitempty"`
|
||||
OriginalMax float64 `json:"original_max,omitempty"`
|
||||
|
||||
// Exact conversion to mm — no engineering rounding.
|
||||
ExactMM float64 `json:"exact_mm,omitempty"`
|
||||
ExactMinMM float64 `json:"exact_min_mm,omitempty"`
|
||||
ExactMaxMM float64 `json:"exact_max_mm,omitempty"`
|
||||
|
||||
// Engineering-recommended metric value with safe-side rounding.
|
||||
// For minimum distances: rounded up. For maximum opening widths:
|
||||
// rounded down.
|
||||
RecommendedMM int `json:"recommended_mm,omitempty"`
|
||||
RecommendedMinMM int `json:"recommended_min_mm,omitempty"`
|
||||
RecommendedMaxMM int `json:"recommended_max_mm,omitempty"`
|
||||
RoundingNote string `json:"rounding_note,omitempty"`
|
||||
|
||||
// Optional formula constant (e.g. OSHA hand-speed 63 in/s).
|
||||
FormulaInchPerSecond float64 `json:"formula_inch_per_second,omitempty"`
|
||||
FormulaMMPerSecond float64 `json:"formula_mm_per_second,omitempty"`
|
||||
FormulaDescription string `json:"formula_description,omitempty"`
|
||||
|
||||
Context string `json:"context"` // "Point of Operation Guarding mechanical presses"
|
||||
BodyPart string `json:"body_part,omitempty"` // "finger" / "hand" / "head" / "foot" / "body"
|
||||
HazardTags []string `json:"hazard_tags,omitempty"` // [crush_point, cutting_part, ...]
|
||||
|
||||
// EU norm cross-reference — IDENTIFIER ONLY, no values reproduced.
|
||||
EUNormHints []EUNormHint `json:"eu_norm_hints,omitempty"`
|
||||
}
|
||||
|
||||
// EUNormHint references an EU standard by identifier without reproducing
|
||||
// any value or text from it. The DINComparisonNote is a human-curated
|
||||
// qualitative judgement (stricter / equivalent / looser) — not a copy.
|
||||
type EUNormHint struct {
|
||||
Norm string `json:"norm"` // "EN ISO 13857"
|
||||
Section string `json:"section,omitempty"` // "Tab. 4, Schutz gegen Hineingreifen"
|
||||
DINComparisonNote string `json:"din_comparison_note,omitempty"`
|
||||
}
|
||||
|
||||
// GetOSHAMinimumDistances returns the verbatim OSHA values for
|
||||
// machine-guarding distances. All values are US Federal Public Domain
|
||||
// (17 U.S.C. §105). Engineering rounding is BreakPilot's safe-side
|
||||
// recommendation; OSHA values themselves are unchanged.
|
||||
func GetOSHAMinimumDistances() []MinimumDistance {
|
||||
return []MinimumDistance{
|
||||
// OSHA Table O-10 row 1 — verbatim values, mathematical conversion,
|
||||
// safe-side rounded engineering recommendation.
|
||||
{
|
||||
ID: "MD_OSHA_O10_R1",
|
||||
SourceCFR: "29 CFR §1910.217(c)(1)(i)",
|
||||
SourceTable: "Table O-10 row 1",
|
||||
License: "US Federal Public Domain (17 U.S.C. §105)",
|
||||
LicenseRule: 1,
|
||||
OriginalUnit: UnitInch,
|
||||
OriginalMin: 0.5, OriginalMax: 1.5, OriginalValue: 0.25,
|
||||
ExactMinMM: 12.7, ExactMaxMM: 38.1, ExactMM: 6.35,
|
||||
RecommendedMinMM: 15, RecommendedMaxMM: 40, RecommendedMM: 6,
|
||||
RoundingNote: "Distance auf 5-mm-Raster aufgerundet, opening auf 1-mm-Raster abgerundet (konservativ in beide Richtungen).",
|
||||
Context: "Point-of-Operation Guarding bei mechanischen Pressen",
|
||||
BodyPart: "finger",
|
||||
HazardTags: []string{"crush_point", "cutting_part"},
|
||||
EUNormHints: []EUNormHint{
|
||||
{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
|
||||
DINComparisonNote: "Andere Methodik (Reichweitenmodell). Unabhaengig pruefen — Werte koennen abweichen."},
|
||||
},
|
||||
},
|
||||
// OSHA Table O-10 row 4 — used as a worked example in the strategy
|
||||
// discussion. Distance 3.5-5.5 in, opening max 5/8 in.
|
||||
{
|
||||
ID: "MD_OSHA_O10_R4",
|
||||
SourceCFR: "29 CFR §1910.217(c)(1)(i)",
|
||||
SourceTable: "Table O-10 row 4",
|
||||
License: "US Federal Public Domain (17 U.S.C. §105)",
|
||||
LicenseRule: 1,
|
||||
OriginalUnit: UnitInch,
|
||||
OriginalMin: 3.5, OriginalMax: 5.5, OriginalValue: 0.625,
|
||||
ExactMinMM: 88.9, ExactMaxMM: 139.7, ExactMM: 15.875,
|
||||
RecommendedMinMM: 90, RecommendedMaxMM: 140, RecommendedMM: 15,
|
||||
RoundingNote: "Distance 88.9→90 (+1.1 mm), 139.7→140 (+0.3 mm) aufgerundet; Opening 15.875→15 (-0.875 mm) abgerundet.",
|
||||
Context: "Point-of-Operation Guarding bei mechanischen Pressen",
|
||||
BodyPart: "finger",
|
||||
HazardTags: []string{"crush_point", "cutting_part"},
|
||||
EUNormHints: []EUNormHint{
|
||||
{Norm: "EN ISO 13857", Section: "Tab. 4 (Hineingreifen)",
|
||||
DINComparisonNote: "Andere Methodik (Reichweitenmodell). Compliance-Annotation pflegen."},
|
||||
},
|
||||
},
|
||||
// OSHA §1910.212(a)(5) — fan blade guards. Verbatim 1/2 inch.
|
||||
{
|
||||
ID: "MD_OSHA_212_FAN",
|
||||
SourceCFR: "29 CFR §1910.212(a)(5)",
|
||||
License: "US Federal Public Domain (17 U.S.C. §105)",
|
||||
LicenseRule: 1,
|
||||
OriginalUnit: UnitInch,
|
||||
OriginalValue: 0.5,
|
||||
ExactMM: 12.7,
|
||||
RecommendedMM: 12,
|
||||
RoundingNote: "Luefterblatt-Schutzgitter: max. Spaltoeffnung 1/2 in = 12.7 mm. Konservativ auf 12 mm abgerundet.",
|
||||
Context: "Lüfterblätter unter 7 ft (2.13 m) Höhe",
|
||||
BodyPart: "finger",
|
||||
HazardTags: []string{"rotating_part", "cutting_part"},
|
||||
EUNormHints: []EUNormHint{
|
||||
{Norm: "EN ISO 13857", Section: "Tab. 4",
|
||||
DINComparisonNote: "DIN-Wert pruefen."},
|
||||
},
|
||||
},
|
||||
// OSHA §1910.217 Hand-Speed Constant — formula Ds = 63 in/s × Ts
|
||||
{
|
||||
ID: "MD_OSHA_217_PSDI",
|
||||
SourceCFR: "29 CFR §1910.217 (Ds = 63 in/s × Ts)",
|
||||
License: "US Federal Public Domain (17 U.S.C. §105)",
|
||||
LicenseRule: 1,
|
||||
OriginalUnit: UnitInch,
|
||||
FormulaInchPerSecond: 63.0,
|
||||
FormulaMMPerSecond: 1600.2,
|
||||
FormulaDescription: "Hand-Speed-Konstante 63 in/s ≈ 1600 mm/s. " +
|
||||
"Ds (Mindestabstand) = 63 × Ts (Stoppzeit Presse in Sekunden).",
|
||||
Context: "PSDI Presence-Sensing Device Initiation und Two-Hand-Trip",
|
||||
BodyPart: "hand",
|
||||
HazardTags: []string{"crush_point", "high_speed"},
|
||||
EUNormHints: []EUNormHint{
|
||||
{Norm: "EN 13855", Section: "Sicherheitsabstaende",
|
||||
DINComparisonNote: "EN 13855 nutzt andere Konstante (1600 mm/s ≈ identisch); EU-Norm unabhaengig pruefen."},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1570,6 +1570,7 @@ async def _run_compliance_check(check_id: str, req: ComplianceCheckRequest):
|
||||
} if banner_result else None),
|
||||
"tcf_vendors": vvt_entries if tcf_vendors else [],
|
||||
"cmp_vendors": cmp_vendors,
|
||||
"cookie_audit": cookie_audit if cookie_audit else None,
|
||||
"total_documents": len(results),
|
||||
"total_findings": total_findings,
|
||||
"email_status": email_result.get("status", "failed"),
|
||||
|
||||
Reference in New Issue
Block a user