From 239702fdcadd8fa3853775424c4c83c377855ff7 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 28 Jun 2026 17:12:40 +0200 Subject: [PATCH] feat(admin): ETO / Onboarding-Advisor test page (thin operator surface over the advisor endpoint) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A focused client page at /sdk/onboarding-advisor that exercises POST /api/compliance/onboarding/ advisor-start through the existing compliance proxy: pick certifications + target + scanner findings (observation / partial / requirement) and render the result — headline, silent-intake summary, auto-detected (green), indications (amber), next-best questions with WHY, inferred (Welt-1) vs rejected assumptions, capability delta, evidence requests, completeness. NOT the regulation gap engine (/sdk/gap-analysis is a different flow). No new backend; calls only the existing endpoint. 195 lines. --- .../app/sdk/onboarding-advisor/page.tsx | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 admin-compliance/app/sdk/onboarding-advisor/page.tsx diff --git a/admin-compliance/app/sdk/onboarding-advisor/page.tsx b/admin-compliance/app/sdk/onboarding-advisor/page.tsx new file mode 100644 index 00000000..53a54635 --- /dev/null +++ b/admin-compliance/app/sdk/onboarding-advisor/page.tsx @@ -0,0 +1,195 @@ +'use client' + +// ETO / Onboarding-Advisor — thin operator surface over POST /api/compliance/onboarding/advisor-start. +// Certifications + target + scanner findings -> Silent Pass -> Advisor. NOT the regulation gap engine +// (/sdk/gap-analysis is a different flow: product -> applicable regulations). This tests the cert->delta +// case: "TISAX/ISO27001 -> CRA, what is auto-detected, what stays an open question?". No new backend. + +import React, { useEffect, useState } from 'react' + +const CERTS = ['ISO27001', 'TISAX', 'ISO9001', 'IEC62443', 'ISO13485', 'ISO14001', 'ASPICE', 'IATF16949'] + +// label -> {signal_id, source_type} — demonstrates all three signal KINDS (observation / partial / requirement) +const FINDINGS: Array<{ label: string; signal_id: string; source_type: string; kind: string }> = [ + { label: 'SBOM im Repo (CycloneDX/SPDX)', signal_id: 'cyclonedx_found', source_type: 'repository', kind: 'observation' }, + { label: 'security.txt / CVD-Policy veröffentlicht', signal_id: 'security_txt', source_type: 'website', kind: 'observation' }, + { label: 'Signierte Releases', signal_id: 'signed_releases', source_type: 'repository', kind: 'observation' }, + { label: 'Produkt-Risikobewertung (Dokument)', signal_id: 'risk_assessment_pdf', source_type: 'document', kind: 'observation' }, + { label: 'CI-Pipeline vorhanden (nur Indikation)', signal_id: 'github_actions_ci', source_type: 'repository', kind: 'partial' }, + { label: 'Cloud-/vernetztes Produkt', signal_id: 'cloud_hosted', source_type: 'product', kind: 'observation' }, + { label: 'Ausschreibung FORDERT SBOM (Requirement)', signal_id: 'requires_sbom', source_type: 'tender', kind: 'requirement' }, + { label: 'OEM FORDERT PSIRT (Requirement)', signal_id: 'supplier_requires_psirt', source_type: 'oem', kind: 'requirement' }, +] + +interface Question { capability_id: string; question_intent: string; why: string; information_value: number; priority: string } +interface Inferred { certification: string; capabilities: string[]; statement: string } +interface Rejected { certification?: string; statement: string; reason: string } +interface Measure { capability_id: string; leverage: number; closes: string[] } +interface AdvisorResponse { + silent_intake_summary: string; headline: string; auto_detected: string[]; indications: string[] + inferred_assumptions: Inferred[]; rejected_assumptions: Rejected[]; top_5_questions: Question[] + capability_delta: string[]; top_measures: Measure[]; evidence_requests: string[] + unsupported_domains: string[]; completeness_summary: string +} + +const PROXY = '/api/sdk/v1/compliance/onboarding' + +function Chips({ items, tone }: { items: string[]; tone: string }) { + if (!items.length) return + return ( +
+ {items.map(c => {c})} +
+ ) +} + +function Section({ title, hint, children }: { title: string; hint?: string; children: React.ReactNode }) { + return ( +
+

{title}

+ {hint &&

{hint}

} +
{children}
+
+ ) +} + +export default function OnboardingAdvisorPage() { + const [targets, setTargets] = useState([]) + const [company, setCompany] = useState('Beispiel Maschinenbau') + const [industry, setIndustry] = useState('machine_builder') + const [certs, setCerts] = useState(['ISO27001', 'ISO9001']) + const [target, setTarget] = useState('CRA') + const [findings, setFindings] = useState(['cyclonedx_found', 'github_actions_ci', 'requires_sbom']) + const [knownEvidence, setKnownEvidence] = useState('CE-Prozess') + const [result, setResult] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + fetch(`${PROXY}/targets`).then(r => r.json()).then(d => { + if (Array.isArray(d.targets)) { setTargets(d.targets); if (!d.targets.includes('CRA') && d.targets[0]) setTarget(d.targets[0]) } + }).catch(() => {}) + }, []) + + const toggle = (list: string[], set: (v: string[]) => void, v: string) => + set(list.includes(v) ? list.filter(x => x !== v) : [...list, v]) + + const run = async () => { + setLoading(true); setError(''); setResult(null) + try { + const scanner_findings = FINDINGS.filter(f => findings.includes(f.signal_id)) + .map(f => ({ signal_id: f.signal_id, source_type: f.source_type })) + const res = await fetch(`${PROXY}/advisor-start`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + company, industry, products: [], markets: ['EU'], certifications: certs, + known_evidence: knownEvidence ? knownEvidence.split(',').map(s => s.trim()).filter(Boolean) : [], + target, scanner_findings, + }), + }) + if (!res.ok) throw new Error(await res.text()) + setResult(await res.json()) + } catch (e) { + setError(e instanceof Error ? e.message : 'Advisor fehlgeschlagen') + } finally { setLoading(false) } + } + + return ( +
+
+

ETO / Onboarding-Advisor

+

+ Zertifikate + Ziel + Scanner-Signale → Silent Pass → Capability-Delta + nächste beste Fragen. + Welt-1: ein Zertifikat legt nahe, beweist nichts (Verifikation erforderlich). +

+ +
+
+ + + + +
+ +
+
+ {CERTS.map(c => ( + + ))} +
+
+
+ +
+
+ {FINDINGS.map(f => ( + + ))} +
+
+ + + + {error &&
{error}
} + + {result && ( +
+
+
{result.headline}
+
{result.silent_intake_summary}
+
+
+
+
+
+
+ {result.top_5_questions.length ? ( +
    + {result.top_5_questions.map((q, i) => ( +
  1. +
    {i + 1}. {q.capability_id} ({q.question_intent})
    +
    {q.why}
    +
  2. + ))} +
+ ) : } +
+
+
+ {result.inferred_assumptions.length ? result.inferred_assumptions.map(a => ( +
{a.certification}: {a.capabilities.join(', ')}
+ )) : } +
+
+ {result.rejected_assumptions.length ? result.rejected_assumptions.map((a, i) => ( +
{a.statement}
+ )) : } +
+
+
+
+
+
+
+ {result.completeness_summary || '—'} +
+
+ )} +
+
+ ) +}