Compare commits
3 Commits
21c01d6405
...
63bd6a7c6d
| Author | SHA1 | Date | |
|---|---|---|---|
| 63bd6a7c6d | |||
| 6cec1dcdba | |||
| 136dc4d553 |
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface FAQItem {
|
||||
q: string
|
||||
a: string
|
||||
}
|
||||
|
||||
const FAQ_ITEMS: FAQItem[] = [
|
||||
{
|
||||
q: "Was passiert wenn ein Unternehmen wegen unzureichender Datenschutzerklaerung oder Cookie-Richtlinie verklagt wird?",
|
||||
a: `Es gibt vier Durchsetzungswege:
|
||||
|
||||
**1. Bussgelder durch Aufsichtsbehoerden (Art. 83 DSGVO)**
|
||||
Aufsichtsbehoerden pruefen von Amts wegen oder auf Beschwerde — kein Klaeger noetig. Bussgelder bis 20 Mio. EUR oder 4% des Jahresumsatzes. Beispiele: CNIL gegen Google (150 Mio. EUR), Facebook (60 Mio. EUR), H&M (35 Mio. EUR). Auch KMU sind betroffen — der LfDI Baden-Wuerttemberg hat Bussgelder ab 10.000 EUR verhaengt.
|
||||
|
||||
**2. Abmahnungen durch Verbraucherschutzverbaende**
|
||||
Verbaende wie vzbv oder DUH koennen ohne individuellen Schaden klagen (§2 UKlaG). Das ist der groesste praktische Druck: Unterlassungsklage + Anwaltskosten (5.000-20.000 EUR pro Fall). Seit EuGH C-319/20 (Meta/vzbv) duerfen Verbaende DSGVO-Verstoesse auch ohne Betroffenenauftrag klagen.
|
||||
|
||||
**3. Individueller Schadensersatz (Art. 82 DSGVO)**
|
||||
Seit EuGH C-300/21 (Oesterreichische Post) genuegt bereits der "Kontrollverlust" ueber Daten als immaterieller Schaden — kein messbarer finanzieller Schaden noetig. Typisch: 100-5.000 EUR pro Betroffenem. Legaltech-Firmen wie NOYB buendeln Massenverfahren.
|
||||
|
||||
**4. Wettbewerber-Abmahnungen (UWG)**
|
||||
Seit 2021 eingeschraenkt, aber Impressums-Maengel oder fehlende Cookie-Einwilligung bleiben abmahnfaehig.
|
||||
|
||||
Die Aufsichtsbehoerden erhalten ueber 10.000 Beschwerden pro Jahr. Eine Beschwerde einzureichen ist kostenlos und mit einem Klick moeglich.`,
|
||||
},
|
||||
{
|
||||
q: "Wie funktioniert die Dokumentenpruefung?",
|
||||
a: `Die Pruefung laeuft in drei Schritten:
|
||||
|
||||
**1. Text-Extraktion** — Playwright laedt die Seite, expandiert Accordions/Tabs und extrahiert den vollstaendigen Text.
|
||||
|
||||
**2. Regex-Checks (138 Pruefpunkte)** — Zwei Ebenen: L1 prueft ob Pflichtangaben erwaehnt sind (z.B. "Verantwortlicher"), L2 prueft ob sie korrekt und vollstaendig sind (z.B. "Hat der Verantwortliche eine ladungsfaehige Anschrift mit PLZ?").
|
||||
|
||||
**3. LLM-Verifikation** — Jeder fehlgeschlagene Check wird von einem KI-Modell (Qwen) gegen den Originaltext gegengeprueft, um Fehlalarme zu eliminieren.
|
||||
|
||||
Das Ergebnis: Zwei Scores pro Dokument — Vollstaendigkeit (sind alle Pflichtangaben da?) und Korrektheit (sind sie richtig formuliert?). Jeder fehlende Punkt hat eine konkrete Handlungsanweisung mit Rechtsbezug.`,
|
||||
},
|
||||
{
|
||||
q: "Welche Dokumenttypen werden geprueft?",
|
||||
a: `Sieben Dokumenttypen mit jeweils eigener Checkliste:
|
||||
|
||||
- **Datenschutzinformation (DSI)** — Art. 13/14 DSGVO (31 Checks)
|
||||
- **Cookie-Richtlinie** — §25 TDDDG (15 Checks)
|
||||
- **Impressum** — §5 TMG / §18 MStV (16 Checks)
|
||||
- **AGB** — §305ff BGB (21 Checks)
|
||||
- **Widerrufsbelehrung** — §355 BGB (15 Checks)
|
||||
- **Social Media DSE** — Art. 26 DSGVO Joint Controller (20 Checks)
|
||||
- **DSFA** — Art. 35 DSGVO (18 Checks)
|
||||
|
||||
Sub-Sektionen (z.B. Cookie-Abschnitt innerhalb der DSI) werden automatisch erkannt und separat geprueft.`,
|
||||
},
|
||||
{
|
||||
q: "Wie zuverlaessig sind die Ergebnisse?",
|
||||
a: `Die Pruefung wurde gegen mehrere Ground-Truth-Websites validiert (IHK Konstanz, ETO Gruppe, BMW, Stadt Koeln, Sparkasse, Spiegel u.a.). Ergebnis: **0 False Positives** bei validierten Testfaellen — jeder rote Punkt ist ein echtes Finding.
|
||||
|
||||
Durch die LLM-Verifikation werden Regex-Fehlalarme (z.B. durch ungewoehnliche Formatierung oder Soft Hyphens im HTML) automatisch korrigiert. Trotzdem gilt: Das Tool ersetzt keine Rechtsberatung, sondern identifiziert Handlungsbedarf.`,
|
||||
},
|
||||
{
|
||||
q: "Was kostet ein Verstoss gegen die DSGVO in der Praxis?",
|
||||
a: `Bussgelder nach Art. 83 DSGVO staffeln sich in zwei Stufen:
|
||||
|
||||
- **Bis 10 Mio. EUR / 2% Umsatz**: Verstoesse gegen technische/organisatorische Pflichten (Art. 25, 28, 32)
|
||||
- **Bis 20 Mio. EUR / 4% Umsatz**: Verstoesse gegen Grundsaetze, Betroffenenrechte, Drittlandtransfer
|
||||
|
||||
Typische Praxis-Bussgelder in Deutschland: 5.000-50.000 EUR fuer KMU, 100.000-1 Mio. EUR fuer groessere Unternehmen. Dazu kommen Anwaltskosten bei Abmahnungen (5.000-20.000 EUR pro Fall) und Reputationsschaden.`,
|
||||
},
|
||||
]
|
||||
|
||||
export function ComplianceFAQ() {
|
||||
const [open, setOpen] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-xl overflow-hidden">
|
||||
<div className="px-4 py-3 bg-gray-50 border-b border-gray-200">
|
||||
<h3 className="text-sm font-semibold text-gray-800">Haeufige Fragen</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{FAQ_ITEMS.map((item, i) => (
|
||||
<div key={i}>
|
||||
<button
|
||||
onClick={() => setOpen(open === i ? null : i)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 text-left hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium text-gray-900 pr-4">{item.q}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-gray-400 shrink-0 transition-transform ${open === i ? 'rotate-180' : ''}`}
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
{open === i && (
|
||||
<div className="px-4 pb-4 text-sm text-gray-600 prose prose-sm max-w-none">
|
||||
{item.a.split('\n\n').map((para, pi) => (
|
||||
<p key={pi} className="mb-2 last:mb-0" dangerouslySetInnerHTML={{
|
||||
__html: para
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\n- /g, '<br/>• ')
|
||||
.replace(/\n/g, '<br/>')
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { FollowUpQuestions } from './_components/FollowUpQuestions'
|
||||
import { ScanResult } from './_components/ScanResult'
|
||||
import { DocCheckTab } from './_components/DocCheckTab'
|
||||
import { BannerCheckTab } from './_components/BannerCheckTab'
|
||||
import { ComplianceFAQ } from './_components/ComplianceFAQ'
|
||||
|
||||
type AnalysisMode = 'pre_launch' | 'post_launch'
|
||||
type AnalysisTab = 'quick' | 'scan' | 'doc-check' | 'banner-check'
|
||||
@@ -311,6 +312,8 @@ export default function AgentPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* FAQ */}
|
||||
<ComplianceFAQ />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,25 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
|
||||
const [mitCounts, setMitCounts] = useState<Record<string, number>>({})
|
||||
const [edits, setEdits] = useState<Record<string, EditState>>({})
|
||||
const [saving, setSaving] = useState<string | null>(null)
|
||||
const [normsByCategory, setNormsByCategory] = useState<Record<string, string[]>>({})
|
||||
|
||||
// Fetch norms library and build category→norm-numbers map
|
||||
useEffect(() => {
|
||||
fetch(`/api/sdk/v1/iace/norms-library`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(json => {
|
||||
if (!json?.norms) return
|
||||
const map: Record<string, string[]> = {}
|
||||
for (const n of json.norms) {
|
||||
for (const cat of (n.hazard_cats || [])) {
|
||||
if (!map[cat]) map[cat] = []
|
||||
if (map[cat].length < 3) map[cat].push(n.number)
|
||||
}
|
||||
}
|
||||
setNormsByCategory(map)
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
// Fetch mitigation counts per hazard
|
||||
useEffect(() => {
|
||||
@@ -226,6 +245,11 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions,
|
||||
<span className="inline-block px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-[10px] font-medium">
|
||||
{CATEGORY_LABELS[h.category] || h.category}
|
||||
</span>
|
||||
{normsByCategory[h.category]?.length > 0 && (
|
||||
<div className="text-[9px] text-blue-500 mt-0.5 truncate" title={normsByCategory[h.category].join(', ')}>
|
||||
{normsByCategory[h.category].slice(0, 2).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
{/* Initial S/E/P/RPZ/Risk */}
|
||||
<td className="px-2 py-2 text-center text-gray-700 dark:text-gray-300">{initS}</td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { REDUCTION_TYPES, Mitigation } from './_components/types'
|
||||
import { HierarchyWarning } from './_components/HierarchyWarning'
|
||||
@@ -21,6 +21,24 @@ export default function MitigationsPage() {
|
||||
fetchMeasuresLibrary, handleSubmit, handleAddSuggestedMeasure, handleVerify, handleDelete,
|
||||
} = useMitigations(projectId)
|
||||
|
||||
const [measureNorms, setMeasureNorms] = useState<Record<string, string[]>>({})
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/sdk/v1/iace/protective-measures-library')
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(json => {
|
||||
if (!json?.protective_measures) return
|
||||
const map: Record<string, string[]> = {}
|
||||
for (const m of json.protective_measures) {
|
||||
if (m.norm_references?.length > 0) {
|
||||
map[(m.name || '').toLowerCase()] = m.norm_references
|
||||
}
|
||||
}
|
||||
setMeasureNorms(map)
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const [showForm, setShowForm] = useState(false)
|
||||
const [preselectedType, setPreselectedType] = useState<'design' | 'protection' | 'information' | undefined>()
|
||||
const [showLibrary, setShowLibrary] = useState(false)
|
||||
@@ -195,6 +213,12 @@ export default function MitigationsPage() {
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
|
||||
{m.description && <div className="text-xs text-gray-400 mt-0.5">{m.description}</div>}
|
||||
{(() => {
|
||||
const refs = measureNorms[(m.title || '').toLowerCase()]
|
||||
return refs?.length > 0 ? (
|
||||
<div className="text-[9px] text-blue-500 mt-0.5">Normen: {refs.join(', ')}</div>
|
||||
) : null
|
||||
})()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{(m.linked_hazard_names || []).join(', ') || '-'}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { useParams } from 'next/navigation'
|
||||
import { SuggestedNorms } from '../_components/SuggestedNorms'
|
||||
|
||||
export default function NormsPage() {
|
||||
const params = useParams()
|
||||
const projectId = params.projectId as string
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page header */}
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">Normenrecherche</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Relevante Normen fuer Ihr Produkt, automatisch ermittelt aus Maschinentyp, Gefaehrdungen
|
||||
und Komponenten. Ergaenzen Sie bei Bedarf weitere Normen manuell.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Info banner */}
|
||||
<div className="p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" 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>
|
||||
<div className="text-xs text-blue-800 dark:text-blue-300">
|
||||
<strong>A-Normen</strong> (z.B. ISO 12100) gelten fuer alle Maschinen.{' '}
|
||||
<strong>B-Normen</strong> decken Sicherheitsaspekte ab (B1: Grundnormen, B2: Schutzeinrichtungen).{' '}
|
||||
<strong>C-Normen</strong> sind maschinenspezifisch und erzeugen eine Konformitaetsvermutung.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggested norms component — rendered expanded (not collapsed by default) */}
|
||||
<SuggestedNorms projectId={projectId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ const IACE_NAV_ITEMS = [
|
||||
{ id: 'overview', label: 'Uebersicht', href: '', icon: 'grid' },
|
||||
{ id: 'order', label: 'Auftrag', href: '/order', icon: 'briefcase' },
|
||||
{ id: 'interview', label: 'Grenzen & Verwendung', href: '/interview', icon: 'chat' },
|
||||
{ id: 'norms', label: 'Normenrecherche', href: '/norms', icon: 'book' },
|
||||
{ id: 'components', label: 'Komponenten', href: '/components', icon: 'cube' },
|
||||
{ id: 'hazards', label: 'Hazard Log', href: '/hazards', icon: 'warning' },
|
||||
{ id: 'mitigations', label: 'Massnahmen', href: '/mitigations', icon: 'shield' },
|
||||
@@ -91,6 +92,12 @@ function NavIcon({ icon, className }: { icon: string; className?: string }) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
)
|
||||
case 'book':
|
||||
return (
|
||||
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -148,19 +148,19 @@ func (h *IACEHandler) ListHazards(c *gin.Context) {
|
||||
hazards = []iace.Hazard{}
|
||||
}
|
||||
|
||||
// Enrich hazards with latest risk assessment
|
||||
// Enrich hazards with latest risk assessment (single batch query instead of N+1)
|
||||
type enrichedHazard struct {
|
||||
iace.Hazard
|
||||
RiskAssessment interface{} `json:"risk_assessment"`
|
||||
}
|
||||
|
||||
assessmentMap, _ := h.store.GetLatestAssessmentsByProject(c.Request.Context(), projectID)
|
||||
|
||||
enriched := make([]enrichedHazard, len(hazards))
|
||||
for i, hz := range hazards {
|
||||
enriched[i] = enrichedHazard{Hazard: hz}
|
||||
// Get latest assessment for this hazard
|
||||
assessments, err := h.store.ListAssessments(c.Request.Context(), hz.ID)
|
||||
if err == nil && len(assessments) > 0 {
|
||||
enriched[i].RiskAssessment = assessments[len(assessments)-1]
|
||||
if ra, ok := assessmentMap[hz.ID]; ok {
|
||||
enriched[i].RiskAssessment = ra
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,49 @@ func (s *Store) ListAssessments(ctx context.Context, hazardID uuid.UUID) ([]Risk
|
||||
return assessments, nil
|
||||
}
|
||||
|
||||
// GetLatestAssessmentsByProject fetches the latest risk assessment for ALL hazards
|
||||
// of a project in a single query. Returns map[hazardID]RiskAssessment.
|
||||
func (s *Store) GetLatestAssessmentsByProject(ctx context.Context, projectID uuid.UUID) (map[uuid.UUID]RiskAssessment, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT DISTINCT ON (ra.hazard_id)
|
||||
ra.id, ra.hazard_id, ra.version, ra.assessment_type,
|
||||
ra.severity, ra.exposure, ra.probability,
|
||||
ra.inherent_risk, ra.control_maturity, ra.control_coverage,
|
||||
ra.test_evidence_strength, ra.c_eff, ra.residual_risk,
|
||||
ra.risk_level, ra.is_acceptable, ra.acceptance_justification,
|
||||
ra.assessed_by, ra.created_at
|
||||
FROM iace_risk_assessments ra
|
||||
JOIN iace_hazards h ON h.id = ra.hazard_id
|
||||
WHERE h.project_id = $1
|
||||
ORDER BY ra.hazard_id, ra.version DESC, ra.created_at DESC
|
||||
`, projectID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get latest assessments by project: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make(map[uuid.UUID]RiskAssessment)
|
||||
for rows.Next() {
|
||||
var a RiskAssessment
|
||||
var assessmentType, riskLevel string
|
||||
err := rows.Scan(
|
||||
&a.ID, &a.HazardID, &a.Version, &assessmentType,
|
||||
&a.Severity, &a.Exposure, &a.Probability,
|
||||
&a.InherentRisk, &a.ControlMaturity, &a.ControlCoverage,
|
||||
&a.TestEvidenceStrength, &a.CEff, &a.ResidualRisk,
|
||||
&riskLevel, &a.IsAcceptable, &a.AcceptanceJustification,
|
||||
&a.AssessedBy, &a.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan assessment: %w", err)
|
||||
}
|
||||
a.AssessmentType = AssessmentType(assessmentType)
|
||||
a.RiskLevel = RiskLevel(riskLevel)
|
||||
result[a.HazardID] = a
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Risk Summary (Aggregated View)
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user