diff --git a/admin-compliance/app/sdk/iace/[projectId]/components/_components/ComponentTreeNode.tsx b/admin-compliance/app/sdk/iace/[projectId]/components/_components/ComponentTreeNode.tsx index a67427a4..1e8c258a 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/components/_components/ComponentTreeNode.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/components/_components/ComponentTreeNode.tsx @@ -10,13 +10,19 @@ export function ComponentTreeNode({ onEdit, onDelete, onAddChild, + onMarkAbsent, + onToggleCE, }: { component: Component depth: number onEdit: (c: Component) => void onDelete: (id: string) => void onAddChild: (parentId: string) => void + onMarkAbsent?: (id: string) => void + onToggleCE?: (id: string, value: boolean) => void }) { + const ceMarked = !!component.ce_marked + const safetyRelevant = !!(component.is_safety_relevant ?? component.safety_relevant) const [expanded, setExpanded] = useState(true) const hasChildren = component.children && component.children.length > 0 @@ -53,6 +59,18 @@ export function ComponentTreeNode({ Bibliothek )} + {ceMarked && ( + + CE + + )} + {ceMarked && safetyRelevant && ( + + ⚠ Sicherheitsfunktion validieren (PL/SIL) + + )} {component.description && ( @@ -74,7 +92,24 @@ export function ComponentTreeNode({ - + )} + {onMarkAbsent && ( + + )} + + ))} + + + ))} + + + ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/components/_components/types.ts b/admin-compliance/app/sdk/iace/[projectId]/components/_components/types.ts index fd13c66e..32d85fab 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/components/_components/types.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/components/_components/types.ts @@ -1,3 +1,5 @@ +export type PresenceStatus = 'vorhanden' | 'nicht_vorhanden' | 'geloescht' + export interface Component { id: string name: string @@ -5,7 +7,16 @@ export interface Component { version: string description: string safety_relevant: boolean + // is_safety_relevant is the backend's field name (the form's `safety_relevant` + // does not currently round-trip). Read this when deriving CE obligations. + is_safety_relevant?: boolean + // ce_marked: bought component carrying its own CE / DoC (robot, actuator, SPS). + // Safe semantics — no hazard suppression; drives provenance + the PL/SIL + // validation note when also safety-relevant. ce_marked?: boolean + // presence_status: 'vorhanden' (default) | 'nicht_vorhanden' (engine negation + // verdict, awaiting expert review) | 'geloescht' (soft-deleted, restorable). + presence_status?: PresenceStatus parent_id: string | null children: Component[] library_component_id?: string diff --git a/admin-compliance/app/sdk/iace/[projectId]/components/_hooks/useComponents.ts b/admin-compliance/app/sdk/iace/[projectId]/components/_hooks/useComponents.ts index 57698e44..c4bce2b9 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/components/_hooks/useComponents.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/components/_hooks/useComponents.ts @@ -1,7 +1,7 @@ 'use client' import { useState, useEffect } from 'react' -import { Component, ComponentFormData, LibraryComponent, EnergySource, buildTree } from '../_components/types' +import { Component, ComponentFormData, LibraryComponent, EnergySource, PresenceStatus, buildTree } from '../_components/types' export function useComponents(projectId: string) { const [components, setComponents] = useState([]) @@ -47,16 +47,43 @@ export function useComponents(projectId: string) { return false } - async function handleDelete(id: string) { - if (!confirm('Komponente wirklich loeschen? Unterkomponenten werden ebenfalls entfernt.')) return + // Move a component between presence states (vorhanden / nicht_vorhanden / + // geloescht). Used for the expert's bidirectional review of auto-detected + // components. + async function setPresence(id: string, status: PresenceStatus) { try { - const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/components/${id}`, { method: 'DELETE' }) + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/components/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ presence_status: status }), + }) if (res.ok) await fetchComponents() } catch (err) { - console.error('Failed to delete component:', err) + console.error('Failed to set presence:', err) } } + // Mark a component as a bought CE product (robot, actuator, SPS ...). Safe + // semantics: no hazard suppression — only provenance + PL/SIL note. + async function setCEMarked(id: string, value: boolean) { + try { + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/components/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ce_marked: value }), + }) + if (res.ok) await fetchComponents() + } catch (err) { + console.error('Failed to set ce_marked:', err) + } + } + + // Soft-delete: the component moves to the "Geloescht" list (restorable); it is + // not removed from the project, so nothing silently disappears. + async function handleDelete(id: string) { + await setPresence(id, 'geloescht') + } + async function handleAddFromLibrary(libraryComps: LibraryComponent[], energySrcs: EnergySource[]) { const energySourceIds = energySrcs.map(e => e.id) for (const comp of libraryComps) { @@ -88,5 +115,7 @@ export function useComponents(projectId: string) { handleSubmit, handleDelete, handleAddFromLibrary, + setPresence, + setCEMarked, } } diff --git a/admin-compliance/app/sdk/iace/[projectId]/components/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/components/page.tsx index 1809a5ed..9ac8bd7f 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/components/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/components/page.tsx @@ -2,17 +2,25 @@ import { useState } from 'react' import { useParams } from 'next/navigation' -import { Component } from './_components/types' +import { Component, buildTree } from './_components/types' import { ComponentTreeNode } from './_components/ComponentTreeNode' import { ComponentForm } from './_components/ComponentForm' import { ComponentLibraryModal } from './_components/ComponentLibraryModal' +import { PresenceSection } from './_components/PresenceSection' import { useComponents } from './_hooks/useComponents' export default function ComponentsPage() { const params = useParams() const projectId = params.projectId as string - const { loading, tree, handleSubmit, handleDelete, handleAddFromLibrary } = useComponents(projectId) + const { loading, components, handleSubmit, handleDelete, handleAddFromLibrary, setPresence, setCEMarked } = useComponents(projectId) + + // Split auto-detected components by presence so the expert can review the + // engine's best-effort negation verdicts and move items in both directions. + const present = components.filter((c) => !c.presence_status || c.presence_status === 'vorhanden') + const negated = components.filter((c) => c.presence_status === 'nicht_vorhanden') + const deleted = components.filter((c) => c.presence_status === 'geloescht') + const tree = buildTree(present) const [showForm, setShowForm] = useState(false) const [editingComponent, setEditingComponent] = useState(null) @@ -110,7 +118,9 @@ export default function ComponentsPage() {
{tree.map((component) => ( + onEdit={handleEdit} onDelete={handleDelete} onAddChild={handleAddChild} + onMarkAbsent={(id) => setPresence(id, 'nicht_vorhanden')} + onToggleCE={setCEMarked} /> ))}
@@ -140,6 +150,27 @@ export default function ComponentsPage() { ) )} + + setPresence(id, 'vorhanden') }, + { label: 'Loeschen', variant: 'danger', onClick: handleDelete }, + ]} + /> + + setPresence(id, 'vorhanden') }, + ]} + /> ) } diff --git a/admin-compliance/app/sdk/iace/[projectId]/knowledge-graph/_hooks/useKnowledgeGraph.ts b/admin-compliance/app/sdk/iace/[projectId]/knowledge-graph/_hooks/useKnowledgeGraph.ts index b64f0c38..b5884c87 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/knowledge-graph/_hooks/useKnowledgeGraph.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/knowledge-graph/_hooks/useKnowledgeGraph.ts @@ -3,7 +3,7 @@ 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 Hazard { id: string; name: string; category: string; operational_states?: string[]; component_id?: string } interface Mitigation { id: string; name?: string; title?: string; reduction_type: string; hazard_id?: string; linked_hazard_ids?: string[] } export interface GraphNode { @@ -56,6 +56,7 @@ export function useKnowledgeGraph(projectId: string) { setHazards((j.hazards || j || []).map((h: Record) => ({ id: h.id as string, name: h.name as string, category: h.category as string || '', operational_states: (h.operational_states || []) as string[], + component_id: (h.component_id || '') as string, }))) } if (mitRes.ok) { @@ -89,17 +90,20 @@ export function useKnowledgeGraph(projectId: string) { }) // Hazard nodes + const compIdSet = new Set(components.map((c) => c.id)) 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) { + // Edge: the component that actually causes this hazard → hazard. + // Only drawn when the hazard carries a component_id that maps to a known + // component node (no synthetic "all from the first component" edges). + if (h.component_id && compIdSet.has(h.component_id)) { graphEdges.push({ id: `e-comp-haz-${h.id}`, - source: `comp-${components[0].id}`, + source: `comp-${h.component_id}`, target: `haz-${h.id}`, label: 'erzeugt', }) diff --git a/admin-compliance/app/sdk/iace/page.tsx b/admin-compliance/app/sdk/iace/page.tsx index b792e0be..c86e21f8 100644 --- a/admin-compliance/app/sdk/iace/page.tsx +++ b/admin-compliance/app/sdk/iace/page.tsx @@ -134,9 +134,14 @@ export default function IACEDashboardPage() { machine_type: '', manufacturer: '', }) + const [machineTypes, setMachineTypes] = useState<{ key: string; label_de: string; group: string }[]>([]) useEffect(() => { fetchProjects() + fetch('/api/sdk/v1/iace/machine-types') + .then((r) => (r.ok ? r.json() : null)) + .then((j) => j && setMachineTypes(j.machine_types || [])) + .catch((err) => console.error('Failed to fetch machine types:', err)) }, []) async function fetchProjects() { @@ -308,13 +313,23 @@ export default function IACEDashboardPage() { - setFormData({ ...formData, machine_type: e.target.value })} - placeholder="z.B. Industrieroboter" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent dark:bg-gray-700 dark:border-gray-600 dark:text-white" - /> + > + + {Array.from(new Set(machineTypes.map((m) => m.group))).map((group) => ( + + {machineTypes.filter((m) => m.group === group).map((m) => ( + + ))} + + ))} + +

+ Steuert, welche maschinenspezifischen Gefährdungs-Patterns greifen — bitte aus der Liste wählen. +