feat: Upselling-Funnel Assessment → Compliance Optimizer
Some checks failed
Build + Deploy / build-admin-compliance (push) Successful in 2m17s
Build + Deploy / build-backend-compliance (push) Successful in 3m22s
Build + Deploy / build-ai-sdk (push) Successful in 1m1s
Build + Deploy / build-developer-portal (push) Successful in 1m21s
Build + Deploy / build-tts (push) Failing after 1m32s
Build + Deploy / build-document-crawler (push) Successful in 37s
Build + Deploy / build-dsms-gateway (push) Successful in 24s
CI / branch-name (push) Has been skipped
Build + Deploy / trigger-orca (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m55s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Failing after 59s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 35s
CI / test-python-dsms-gateway (push) Successful in 26s
CI / validate-canonical-controls (push) Successful in 18s

Verbindet das kostenlose UCCA Assessment mit dem bezahlten
Compliance Optimizer durch gezielte CTAs:

- OptimizerUpsellCard: Kontextabhaengig (CONDITIONAL→prominent, YES→dezent)
- Assessment Detail: "Optimieren" Button + CTA-Block nach Ergebnis
- Advisory Board ResultView: CTA nach Wizard-Abschluss
- Optimizer "new": Auto-Submit bei ?from_assessment={id}
- Optimizer Liste + Detail: Links zum Quell-Assessment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-23 12:28:49 +02:00
parent f8fd329059
commit 5d53acf5dc
6 changed files with 148 additions and 3 deletions

View File

@@ -2,6 +2,7 @@
import React from 'react' import React from 'react'
import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard' import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard'
import { OptimizerUpsellCard } from '@/components/sdk/compliance-optimizer/OptimizerUpsellCard'
interface Props { interface Props {
result: unknown result: unknown
@@ -35,6 +36,13 @@ export function ResultView({ result, onGoToAssessment, onGoToOverview }: Props)
{r.result && ( {r.result && (
<AssessmentResultCard result={r.result as unknown as Parameters<typeof AssessmentResultCard>[0]['result']} /> <AssessmentResultCard result={r.result as unknown as Parameters<typeof AssessmentResultCard>[0]['result']} />
)} )}
{r.result && r.assessment?.id && (
<OptimizerUpsellCard
feasibility={(r.result as { feasibility?: string }).feasibility || 'YES'}
assessmentId={r.assessment.id}
riskScore={(r.result as { risk_score?: number }).risk_score}
/>
)}
</div> </div>
) )
} }

View File

@@ -41,6 +41,11 @@ export default function OptimizationDetailPage() {
<Link href="/sdk/compliance-optimizer" className="text-sm text-blue-600 hover:underline"> Zurueck</Link> <Link href="/sdk/compliance-optimizer" className="text-sm text-blue-600 hover:underline"> Zurueck</Link>
<h1 className="text-2xl font-bold text-gray-900 mt-1">{data.title || 'Optimierung'}</h1> <h1 className="text-2xl font-bold text-gray-900 mt-1">{data.title || 'Optimierung'}</h1>
<p className="text-sm text-gray-500">{new Date(data.created_at).toLocaleString('de-DE')} v{data.constraint_version}</p> <p className="text-sm text-gray-500">{new Date(data.created_at).toLocaleString('de-DE')} v{data.constraint_version}</p>
{data.assessment_id && (
<Link href={`/sdk/use-cases/${data.assessment_id}`} className="text-sm text-purple-600 hover:underline">
Basierend auf Assessment
</Link>
)}
</div> </div>
<ZoneBadge zone={data.is_compliant ? 'SAFE' : 'FORBIDDEN'} /> <ZoneBadge zone={data.is_compliant ? 'SAFE' : 'FORBIDDEN'} />
</div> </div>

View File

@@ -1,7 +1,7 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState, useEffect, Suspense } from 'react'
import { useRouter } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import { ZoneBadge } from '@/components/sdk/compliance-optimizer/ZoneBadge' import { ZoneBadge } from '@/components/sdk/compliance-optimizer/ZoneBadge'
interface DimensionField { interface DimensionField {
@@ -63,9 +63,21 @@ const TOGGLE_DIMENSIONS = [
{ key: 'logging_required', label: 'Protokollierungspflicht' }, { key: 'logging_required', label: 'Protokollierungspflicht' },
] ]
export default function NewOptimizationPage() { function NewOptimizationPageInner() {
const router = useRouter() const router = useRouter()
const searchParams = useSearchParams()
const fromAssessment = searchParams.get('from_assessment')
const [autoOptimizing, setAutoOptimizing] = useState(false)
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
useEffect(() => {
if (!fromAssessment) return
setAutoOptimizing(true)
fetch(`/api/sdk/v1/maximizer/optimize-from-assessment/${fromAssessment}`, { method: 'POST' })
.then(r => r.ok ? r.json() : Promise.reject('failed'))
.then(data => router.push(`/sdk/compliance-optimizer/${data.id}`))
.catch(() => setAutoOptimizing(false))
}, [fromAssessment, router])
const [config, setConfig] = useState<Record<string, string>>({ const [config, setConfig] = useState<Record<string, string>>({
automation_level: 'assistive', decision_binding: 'non_binding', decision_impact: 'low', automation_level: 'assistive', decision_binding: 'non_binding', decision_impact: 'low',
domain: 'general', data_type: 'non_personal', human_in_loop: 'required', domain: 'general', data_type: 'non_personal', human_in_loop: 'required',
@@ -104,6 +116,18 @@ export default function NewOptimizationPage() {
} }
} }
if (autoOptimizing) {
return (
<div className="max-w-4xl mx-auto p-6 text-center py-24">
<div className="animate-pulse">
<span className="text-4xl">📊</span>
<h2 className="text-xl font-bold text-gray-900 mt-4 mb-2">Optimierung laeuft...</h2>
<p className="text-sm text-gray-500">Assessment wird analysiert und optimale Konfiguration berechnet.</p>
</div>
</div>
)
}
return ( return (
<div className="max-w-4xl mx-auto p-6"> <div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-1">Neue Optimierung</h1> <h1 className="text-2xl font-bold text-gray-900 mb-1">Neue Optimierung</h1>
@@ -161,3 +185,11 @@ export default function NewOptimizationPage() {
</div> </div>
) )
} }
export default function NewOptimizationPage() {
return (
<Suspense fallback={<div className="max-w-4xl mx-auto p-6 text-gray-500">Laden...</div>}>
<NewOptimizationPageInner />
</Suspense>
)
}

View File

@@ -12,6 +12,7 @@ interface OptimizationSummary {
created_at: string created_at: string
zone_map: Record<string, { zone: 'FORBIDDEN' | 'RESTRICTED' | 'SAFE' }> zone_map: Record<string, { zone: 'FORBIDDEN' | 'RESTRICTED' | 'SAFE' }>
max_safe_config?: { safety_score: number; utility_score: number } max_safe_config?: { safety_score: number; utility_score: number }
assessment_id?: string
} }
function countZones(zoneMap: Record<string, { zone: string }>) { function countZones(zoneMap: Record<string, { zone: string }>) {
@@ -83,6 +84,7 @@ export default function ComplianceOptimizerPage() {
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Titel</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Titel</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Zonen</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Zonen</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Quelle</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Datum</th> <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Datum</th>
</tr> </tr>
</thead> </thead>
@@ -104,6 +106,15 @@ export default function ComplianceOptimizerPage() {
{zones.restricted > 0 && <span className="text-yellow-600 mr-2">{zones.restricted} eingeschraenkt</span>} {zones.restricted > 0 && <span className="text-yellow-600 mr-2">{zones.restricted} eingeschraenkt</span>}
<span className="text-green-600">{zones.safe} erlaubt</span> <span className="text-green-600">{zones.safe} erlaubt</span>
</td> </td>
<td className="px-4 py-3 text-sm">
{o.assessment_id ? (
<Link href={`/sdk/use-cases/${o.assessment_id}`} className="text-purple-600 hover:underline text-xs">
Assessment
</Link>
) : (
<span className="text-gray-400 text-xs">Manuell</span>
)}
</td>
<td className="px-4 py-3 text-sm text-gray-500"> <td className="px-4 py-3 text-sm text-gray-500">
{new Date(o.created_at).toLocaleDateString('de-DE')} {new Date(o.created_at).toLocaleDateString('de-DE')}
</td> </td>

View File

@@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation' import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard' import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard'
import { OptimizerUpsellCard } from '@/components/sdk/compliance-optimizer/OptimizerUpsellCard'
interface TriggeredRule { interface TriggeredRule {
code: string code: string
@@ -138,6 +139,18 @@ export default function AssessmentDetailPage() {
} }
} }
const [optimizing, setOptimizing] = useState(false)
const handleOptimize = async () => {
setOptimizing(true)
try {
const res = await fetch(`/api/sdk/v1/maximizer/optimize-from-assessment/${assessmentId}`, { method: 'POST' })
if (res.ok) {
const data = await res.json()
router.push(`/sdk/compliance-optimizer/${data.id}`)
}
} catch { /* silent */ } finally { setOptimizing(false) }
}
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@@ -234,6 +247,13 @@ export default function AssessmentDetailPage() {
> >
JSON JSON
</a> </a>
<button
onClick={handleOptimize}
disabled={optimizing}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
>
{optimizing ? 'Optimiere...' : 'Optimieren'}
</button>
<Link <Link
href={`/sdk/use-cases/new?edit=${assessmentId}`} href={`/sdk/use-cases/new?edit=${assessmentId}`}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors" className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
@@ -273,6 +293,13 @@ export default function AssessmentDetailPage() {
{/* Result */} {/* Result */}
<AssessmentResultCard result={resultForCard as Parameters<typeof AssessmentResultCard>[0]['result']} /> <AssessmentResultCard result={resultForCard as Parameters<typeof AssessmentResultCard>[0]['result']} />
{/* Compliance Optimizer Upsell */}
<OptimizerUpsellCard
feasibility={assessment.feasibility}
assessmentId={assessmentId}
riskScore={assessment.risk_score}
/>
{/* KI-Erklärung */} {/* KI-Erklärung */}
{assessment.explanation_text && ( {assessment.explanation_text && (
<div className="bg-purple-50 border border-purple-200 rounded-xl p-6"> <div className="bg-purple-50 border border-purple-200 rounded-xl p-6">

View File

@@ -0,0 +1,62 @@
'use client'
import Link from 'next/link'
interface OptimizerUpsellCardProps {
feasibility: string
assessmentId: string
riskScore?: number
}
export function OptimizerUpsellCard({ feasibility, assessmentId, riskScore }: OptimizerUpsellCardProps) {
const isRestricted = feasibility === 'CONDITIONAL' || feasibility === 'NO'
if (isRestricted) {
return (
<div className="bg-amber-50 border-2 border-amber-300 rounded-xl p-5">
<div className="flex items-start gap-3">
<span className="text-2xl">📊</span>
<div className="flex-1">
<h3 className="text-base font-semibold text-amber-900">
{feasibility === 'NO' ? 'Use Case aktuell nicht umsetzbar' : 'Use Case eingeschraenkt machbar'}
</h3>
<p className="text-sm text-amber-800 mt-1">
Der <strong>Compliance Optimizer</strong> zeigt Ihnen die optimale Konfiguration,
um den regulatorischen Spielraum maximal auszunutzen ohne Grenzen zu ueberschreiten.
</p>
{riskScore != null && riskScore >= 50 && (
<p className="text-xs text-amber-700 mt-1">
Risiko-Score {riskScore}/100 besonders hohes Optimierungspotenzial.
</p>
)}
<Link
href={`/sdk/compliance-optimizer/new?from_assessment=${assessmentId}`}
className="inline-flex items-center gap-1 mt-3 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
Jetzt optimieren
</Link>
</div>
</div>
</div>
)
}
return (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-blue-900">Regulatorischen Spielraum pruefen</h3>
<p className="text-xs text-blue-700 mt-0.5">
Pruefen Sie ob Sie den regulatorischen Spielraum noch besser nutzen koennen.
</p>
</div>
<Link
href={`/sdk/compliance-optimizer/new?from_assessment=${assessmentId}`}
className="text-sm text-blue-600 hover:underline whitespace-nowrap"
>
Optional optimieren
</Link>
</div>
</div>
)
}