From 9bdaa28038d4435470b30eb0a8a5b26ef96b573f Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 22 May 2026 09:50:41 +0200 Subject: [PATCH 1/2] feat(ui): Branchen-Benchmark Sidebar-Link unter Compliance Agent (P107) --- admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx b/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx index 2eb34d74..ed8a399f 100644 --- a/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx +++ b/admin-compliance/components/sdk/Sidebar/SidebarModuleList.tsx @@ -73,6 +73,7 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side } label="EU Registrierung" isActive={pathname?.startsWith('/sdk/ai-registration') ?? false} collapsed={collapsed} projectId={projectId} /> } label="Compliance Optimizer" isActive={pathname?.startsWith('/sdk/compliance-optimizer') ?? false} collapsed={collapsed} projectId={projectId} /> } label="Compliance Agent" isActive={pathname?.startsWith('/sdk/agent') ?? false} collapsed={collapsed} projectId={projectId} /> + } label="Branchen-Benchmark" isActive={pathname?.startsWith('/sdk/benchmark') ?? false} collapsed={collapsed} projectId={projectId} /> {/* CRA Compliance */} From 872145d883d389e3c17a6f64867bd6f082d7db36 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 22 May 2026 09:56:05 +0200 Subject: [PATCH 2/2] feat(iace-fmea): KI-Vorschlag Uebernehmen/Ablehnen flow + AP unit tests Closes the loose end from IACE Phase 5 handover: the LLM FM-suggest button existed and the backend endpoint was wired, but accepted suggestions had no path into the FMEA worksheet. Hook (useFMEA.ts): - acceptSuggestion(fm, componentId): builds an FMEARow from FM defaults, prepends to rows (sorted by RPZ), removes the FM from suggestions. No-ops + drops the suggestion when (component, fm.id) is already in rows. - rejectSuggestion(fmId): drops the FM from suggestions list. Page (fmea/page.tsx): - Suggestion cards now have explicit Uebernehmen / Ablehnen buttons. - Counter "X Vorschlaege uebernommen" tracks accept count for the run. - RPZ in each suggestion is colour-coded (red >200, orange >100). - Hinweis line explains S/O/D adjustability after acceptance. - acceptedCount auto-resets when suggesting starts or panel closes. Tests (useFMEA.test.ts): - 8 calculateAP cases covering AIAG-VDA 2019 boundary points for severity 10 / 9 / 7 / 5 / 3, validating the H/M/L action priority matrix. LOC: fmea/page.tsx hits 320 (soft target 300, well under 500 hard cap). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[projectId]/fmea/_hooks/useFMEA.test.ts | 36 +++++++++ .../iace/[projectId]/fmea/_hooks/useFMEA.ts | 49 +++++++++++- .../app/sdk/iace/[projectId]/fmea/page.tsx | 77 +++++++++++++++---- 3 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.test.ts diff --git a/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.test.ts b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.test.ts new file mode 100644 index 00000000..616762b1 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' +import { calculateAP } from './useFMEA' + +describe('calculateAP — AIAG-VDA 2019 Handbook Action Priority', () => { + it('returns H for severity 10 with mid occurrence', () => { + expect(calculateAP(10, 5, 5)).toBe('H') + }) + + it('returns H for severity 9 with low detection', () => { + expect(calculateAP(9, 4, 7)).toBe('H') + }) + + it('returns M for severity 9 with low occurrence and good detection', () => { + expect(calculateAP(9, 2, 5)).toBe('M') + }) + + it('returns L for severity 9 with very low occurrence and detection', () => { + expect(calculateAP(9, 1, 4)).toBe('L') + }) + + it('returns H for severity 7 with high occurrence', () => { + expect(calculateAP(7, 5, 1)).toBe('H') + }) + + it('returns M for severity 7 with mid occurrence', () => { + expect(calculateAP(7, 3, 5)).toBe('M') + }) + + it('returns L for low-severity well-controlled mode', () => { + expect(calculateAP(3, 1, 1)).toBe('L') + }) + + it('returns L for severity 5 with very low occurrence and detection', () => { + expect(calculateAP(5, 1, 1)).toBe('L') + }) +}) diff --git a/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts index b95be21f..adb83e5d 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/fmea/_hooks/useFMEA.ts @@ -156,5 +156,52 @@ export function useFMEA(projectId: string) { // 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 } + /** + * Accept a suggested FM: build an FMEA row from the FM defaults, prepend it + * to the table state, and remove the FM from the suggestion list. + * Returns false if the (component, fm.id) combo already exists in rows. + */ + function acceptSuggestion(fm: FailureMode, componentId: string): boolean { + const comp = components.find((c) => c.id === componentId) + if (!comp) return false + const dup = rows.find((r) => r.component.id === componentId && r.failureMode.id === fm.id) + if (dup) { + // Still drop the suggestion so the UI does not keep offering it. + setSuggestions((prev) => prev.filter((s) => s.id !== fm.id)) + return false + } + const s = fm.default_severity || 5 + const o = fm.default_occurrence || 5 + const d = fm.default_detection || 5 + const newRow: FMEARow = { + component: comp, + failureMode: fm, + severity: s, + occurrence: o, + detection: d, + rpz: s * o * d, + ap: calculateAP(s, o, d), + } + setRows((prev) => [newRow, ...prev].sort((a, b) => b.rpz - a.rpz)) + setSuggestions((prev) => prev.filter((sg) => sg.id !== fm.id)) + return true + } + + function rejectSuggestion(fmId: string) { + setSuggestions((prev) => prev.filter((sg) => sg.id !== fmId)) + } + + return { + rows, + loading, + stats, + components, + suggestFMs, + suggesting, + suggestions, + suggestSource, + setSuggestions, + acceptSuggestion, + rejectSuggestion, + } } diff --git a/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx index 44cb7310..d4f2fac7 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useParams } from 'next/navigation' import { useFMEA, type FMEARow } from './_hooks/useFMEA' @@ -27,8 +27,17 @@ function rpzLabel(rpz: number): string { export default function FMEAPage() { const { projectId } = useParams<{ projectId: string }>() - const { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions } = useFMEA(projectId) + const { rows, loading, stats, components, suggestFMs, suggesting, suggestions, suggestSource, setSuggestions, acceptSuggestion, rejectSuggestion } = useFMEA(projectId) const [suggestComp, setSuggestComp] = useState(null) + const [acceptedCount, setAcceptedCount] = useState(0) + + // Reset accepted-count when a fresh suggestion run is loaded or the panel closes. + useEffect(() => { + if (suggesting) setAcceptedCount(0) + }, [suggesting]) + useEffect(() => { + if (suggestions.length === 0) setAcceptedCount(0) + }, [suggestions.length]) if (loading) { return ( @@ -97,26 +106,60 @@ export default function FMEAPage() { {suggestions.length > 0 && (
-

- KI-Vorschlaege ({suggestions.length}) — {suggestSource === 'llm' ? 'LLM-generiert' : 'Bibliothek'} -

+
+

+ KI-Vorschlaege ({suggestions.length}) — {suggestSource === 'llm' ? 'LLM-generiert' : 'Bibliothek-Fallback'} +

+ {acceptedCount > 0 && ( +
+ {acceptedCount} Vorschlag{acceptedCount > 1 ? 'e' : ''} uebernommen +
+ )} +
- {suggestions.map((fm, i) => ( -
-
-
{fm.name_de}
-
{fm.effect}
-
- S={fm.default_severity} - O={fm.default_occurrence} - D={fm.default_detection} - RPZ={fm.default_severity * fm.default_occurrence * fm.default_detection} + {suggestions.map((fm) => { + const rpz = fm.default_severity * fm.default_occurrence * fm.default_detection + return ( +
+
+
{fm.name_de}
+
{fm.effect}
+
+ S={fm.default_severity} + O={fm.default_occurrence} + D={fm.default_detection} + 200 ? 'text-red-600' : rpz > 100 ? 'text-orange-600' : 'text-gray-500'}`}>RPZ={rpz} +
+
+
+ +
-
- ))} + ) + })} +
+
+ Hinweis: Uebernommene Fehlermodi erscheinen sofort in der Tabelle unten. Bewertung (S/O/D) ist anpassbar — Standardwerte aus der Bibliothek.
)}