From bb9aacc3d378b21e235917fc8d0d20992f79b522 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 11 Jun 2026 00:12:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(agent):=20Abstellma=C3=9Fnahmen=20+=20Tick?= =?UTF-8?q?et-Formulierung=20(Schritt=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RemediationPlan: aus den offenen Punkten (result.results, Haupt-Engine) je Finding eine Massnahme + fertigen Ticket-Text ableiten, nach Prioritaet sortiert, mit Kopieren + JSON-Export als Uebergabe. SCOPE: BreakPilot formuliert nur — Ticketsystem/Jira/Feedback-Loop baut ein anderes Team. Co-Authored-By: Claude Opus 4.7 --- .../_components/ComplianceResultTabs.tsx | 4 + .../sdk/agent/_components/RemediationPlan.tsx | 140 ++++++++++++++++++ .../__tests__/RemediationPlan.test.tsx | 38 +++++ 3 files changed, 182 insertions(+) create mode 100644 admin-compliance/app/sdk/agent/_components/RemediationPlan.tsx create mode 100644 admin-compliance/app/sdk/agent/_components/__tests__/RemediationPlan.test.tsx diff --git a/admin-compliance/app/sdk/agent/_components/ComplianceResultTabs.tsx b/admin-compliance/app/sdk/agent/_components/ComplianceResultTabs.tsx index 2ed00cd8..31a12522 100644 --- a/admin-compliance/app/sdk/agent/_components/ComplianceResultTabs.tsx +++ b/admin-compliance/app/sdk/agent/_components/ComplianceResultTabs.tsx @@ -13,6 +13,7 @@ import React, { useState } from 'react' import { ChecklistView, DOC_TYPE_LABELS, type DocResult } from './ChecklistView' import { DocResultView } from './DocResultView' import { MigrationPanel } from './MigrationPanel' +import { RemediationPlan } from './RemediationPlan' import { ResultSummary } from './ResultSummary' export function ComplianceResultTabs({ results }: { results: any }) { @@ -147,6 +148,9 @@ export function ComplianceResultTabs({ results }: { results: any }) { ) : null} + {/* Abstellmaßnahmen + Ticket-Formulierung (Übergabe an anderes Team) */} + + {/* Check-Footer (themenübergreifend) */} {results.email_status && (
diff --git a/admin-compliance/app/sdk/agent/_components/RemediationPlan.tsx b/admin-compliance/app/sdk/agent/_components/RemediationPlan.tsx new file mode 100644 index 00000000..501eb79c --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/RemediationPlan.tsx @@ -0,0 +1,140 @@ +'use client' + +/** + * RemediationPlan — Abstellmaßnahmen + Ticket-Formulierung. + * + * Aus den offenen Punkten (result.results, Haupt-Engine) je Finding eine + * Maßnahme + einen fertigen Ticket-Text ableiten und übergabebereit machen + * (Kopieren / JSON-Export). SCOPE: BreakPilot formuliert NUR — Ticketsystem, + * Jira-Sync und Feedback-Loop baut ein anderes Team. Keine zweite Engine. + */ + +import React, { useState } from 'react' + +import { DOC_TYPE_LABELS, type DocResult } from './ChecklistView' + +type Priority = 'Hoch' | 'Mittel' | 'Niedrig' + +interface Remediation { + docType: string + docLabel: string + checkLabel: string + action: string + ticketTitle: string + ticketBody: string + priority: Priority +} + +const PRIO_RANK: Record = { Hoch: 0, Mittel: 1, Niedrig: 2 } +const PRIO_COLOR: Record = { + Hoch: 'bg-red-100 text-red-700', + Mittel: 'bg-amber-100 text-amber-700', + Niedrig: 'bg-blue-100 text-blue-700', +} + +function toPriority(sev: string): Priority { + const s = (sev || '').toUpperCase() + if (s === 'HIGH' || s === 'CRITICAL') return 'Hoch' + if (s === 'MEDIUM') return 'Mittel' + return 'Niedrig' +} + +function buildRemediations(docs: DocResult[]): Remediation[] { + const out: Remediation[] = [] + for (const d of docs) { + if (d.error) continue + const docLabel = DOC_TYPE_LABELS[d.doc_type] || d.doc_type + const failed = d.checks.filter( + c => !c.passed && !c.skipped && c.severity !== 'INFO', + ) + for (const c of failed) { + const action = c.hint || `${c.label} im ${docLabel} ergänzen.` + out.push({ + docType: d.doc_type, + docLabel, + checkLabel: c.label, + action, + ticketTitle: `Compliance: ${docLabel} – ${c.label}`, + ticketBody: + `Dokument: ${docLabel}\nPrüfpunkt: ${c.label}\n` + + `Status: nicht erfüllt\nMaßnahme: ${action}`, + priority: toPriority(c.severity), + }) + } + } + return out.sort((a, b) => PRIO_RANK[a.priority] - PRIO_RANK[b.priority]) +} + +export function RemediationPlan({ results }: { results: any }) { + const items = buildRemediations(results.results || []) + const [copied, setCopied] = useState(null) + + if (items.length === 0) { + return ( +
+ Keine offenen Pflichtangaben — kein Handlungsbedarf. +
+ ) + } + + function copyTicket(i: number, body: string) { + navigator.clipboard?.writeText(body) + setCopied(i) + window.setTimeout(() => setCopied(null), 1500) + } + + function exportAll() { + const payload = items.map(it => ({ + title: it.ticketTitle, + body: it.ticketBody, + priority: it.priority, + doc_type: it.docType, + })) + const blob = new Blob([JSON.stringify(payload, null, 2)], { + type: 'application/json', + }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'breakpilot-tickets.json' + a.click() + URL.revokeObjectURL(url) + } + + return ( +
+
+

+ Abstellmaßnahmen & Tickets ({items.length}) +

+ +
+
+ {items.map((it, i) => ( +
+
+ + {it.priority} + + + {it.docLabel}: {it.checkLabel} + +
+
{it.action}
+ +
+ ))} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/__tests__/RemediationPlan.test.tsx b/admin-compliance/app/sdk/agent/_components/__tests__/RemediationPlan.test.tsx new file mode 100644 index 00000000..e8f58549 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/__tests__/RemediationPlan.test.tsx @@ -0,0 +1,38 @@ +import { describe, it, expect } from 'vitest' +import { render, screen } from '@testing-library/react' + +import { RemediationPlan } from '../RemediationPlan' + +describe('RemediationPlan', () => { + it('leitet Maßnahmen nur aus echten offenen Punkten ab', () => { + const results = { + results: [ + { doc_type: 'impressum', error: '', completeness_pct: 50, checks: [ + { id: 'a', label: 'Registernummer', passed: false, severity: 'HIGH', matched_text: '', level: 1, hint: 'HRB ergänzen' }, + { id: 'b', label: 'Telefon', passed: false, severity: 'MEDIUM', matched_text: '', level: 1 }, + { id: 'c', label: 'OK-Feld', passed: true, severity: 'HIGH', matched_text: 'x', level: 1 }, + { id: 'd', label: 'Info-Hinweis', passed: false, severity: 'INFO', matched_text: '', level: 1 }, + ] }, + ], + } + render() + // 2 Maßnahmen (HIGH + MEDIUM); OK + INFO ausgeschlossen + expect(screen.getByText(/Abstellmaßnahmen & Tickets \(2\)/)).toBeInTheDocument() + expect(screen.getByText(/Registernummer/)).toBeInTheDocument() + expect(screen.getByText('HRB ergänzen')).toBeInTheDocument() // hint = Maßnahme + expect(screen.queryByText(/Info-Hinweis/)).not.toBeInTheDocument() + expect(screen.queryByText(/OK-Feld/)).not.toBeInTheDocument() + }) + + it('zeigt Erfolg, wenn keine offenen Punkte', () => { + const results = { + results: [ + { doc_type: 'impressum', error: '', completeness_pct: 100, checks: [ + { id: 'a', label: 'X', passed: true, severity: 'HIGH', matched_text: 'x', level: 1 }, + ] }, + ], + } + render() + expect(screen.getByText(/kein Handlungsbedarf/)).toBeInTheDocument() + }) +})