feat: Use-Cases/UCCA Module auf 100% — Interface Fix, Search/Offset/Total, Explain/Export, Edit-Mode
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
Kritische Bug Fixes: - [id]/page.tsx: FullAssessment Interface repariert (nested result → flat fields) - resultForCard baut explizit aus flachen Assessment-Feldern (feasibility, risk_score etc.) - Use-Case-Text-Pfad: assessment.intake?.use_case_text statt assessment.use_case_text - rule_code/code Mapping beim Übergeben an AssessmentResultCard Backend (A2+A3): - store.go: AssessmentFilters um Search + Offset erweitert - ListAssessments: COUNT-Query (total), ILIKE-Search auf title, OFFSET-Pagination - ListAssessments Signatur: ([]Assessment, int, error) - Handler: search/offset aus Query-Params, total in Response - import "strconv" hinzugefügt Neue Features: - KI-Erklärung Button (POST /explain) mit lila Erklärungsbox - Export-Buttons Markdown + JSON (Download-Links) - Edit-Mode in new/page.tsx: useSearchParams(?edit=id), Form vorausfüllen - Bedingte PUT/POST Logik; nach Edit → Detail-Seite Redirect - Suspense-Wrapper für useSearchParams (Next.js 15 Requirement) Backend Edit: - store.go: UpdateAssessment() Methode (UPDATE-Query) - ucca_handlers.go: UpdateAssessment Handler (re-evaluiert Intake) - main.go: PUT /ucca/assessments/:id Route registriert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,44 +5,66 @@ 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'
|
||||||
|
|
||||||
|
interface TriggeredRule {
|
||||||
|
code: string
|
||||||
|
category: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
severity: string
|
||||||
|
score_delta: number
|
||||||
|
gdpr_ref: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequiredControl {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
severity: string
|
||||||
|
category: string
|
||||||
|
gdpr_ref: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PatternRecommendation {
|
||||||
|
pattern_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
rationale: string
|
||||||
|
priority: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ForbiddenPattern {
|
||||||
|
pattern_id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
interface FullAssessment {
|
interface FullAssessment {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
tenant_id: string
|
tenant_id: string
|
||||||
domain: string
|
domain: string
|
||||||
created_at: string
|
created_at: string
|
||||||
use_case_text?: string
|
intake?: {
|
||||||
intake?: Record<string, unknown>
|
use_case_text?: string
|
||||||
result?: {
|
[key: string]: unknown
|
||||||
feasibility: string
|
|
||||||
risk_level: string
|
|
||||||
risk_score: number
|
|
||||||
complexity: string
|
|
||||||
dsfa_recommended: boolean
|
|
||||||
art22_risk: boolean
|
|
||||||
training_allowed: string
|
|
||||||
summary: string
|
|
||||||
recommendation: string
|
|
||||||
alternative_approach?: string
|
|
||||||
triggered_rules?: Array<{
|
|
||||||
rule_code: string
|
|
||||||
title: string
|
|
||||||
severity: string
|
|
||||||
gdpr_ref: string
|
|
||||||
}>
|
|
||||||
required_controls?: Array<{
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
effort: string
|
|
||||||
}>
|
|
||||||
recommended_architecture?: Array<{
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
benefit: string
|
|
||||||
}>
|
|
||||||
}
|
}
|
||||||
|
// Flat result fields
|
||||||
|
feasibility: string
|
||||||
|
risk_level: string
|
||||||
|
risk_score: number
|
||||||
|
complexity: string
|
||||||
|
dsfa_recommended: boolean
|
||||||
|
art22_risk: boolean
|
||||||
|
training_allowed: string
|
||||||
|
triggered_rules?: TriggeredRule[]
|
||||||
|
required_controls?: RequiredControl[]
|
||||||
|
recommended_architecture?: PatternRecommendation[]
|
||||||
|
forbidden_patterns?: ForbiddenPattern[]
|
||||||
|
explanation_text?: string
|
||||||
|
explanation_model?: string
|
||||||
|
explanation_generated_at?: string
|
||||||
|
policy_version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AssessmentDetailPage() {
|
export default function AssessmentDetailPage() {
|
||||||
@@ -53,26 +75,20 @@ export default function AssessmentDetailPage() {
|
|||||||
const [assessment, setAssessment] = useState<FullAssessment | null>(null)
|
const [assessment, setAssessment] = useState<FullAssessment | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [explaining, setExplaining] = useState(false)
|
||||||
|
const [explanationError, setExplanationError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
async function loadAssessment() {
|
||||||
|
const response = await fetch(`/api/sdk/v1/ucca/assessments/${assessmentId}`)
|
||||||
|
if (!response.ok) throw new Error('Assessment nicht gefunden')
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
if (!assessmentId) return
|
||||||
try {
|
loadAssessment()
|
||||||
const response = await fetch(`/api/sdk/v1/ucca/assessments/${assessmentId}`)
|
.then(data => setAssessment(data))
|
||||||
if (!response.ok) {
|
.catch(() => {
|
||||||
throw new Error('Assessment nicht gefunden')
|
|
||||||
}
|
|
||||||
const data = await response.json()
|
|
||||||
setAssessment(data)
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Fehler beim Laden')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (assessmentId) {
|
|
||||||
// Try the direct endpoint first; if it fails, try the list endpoint and filter
|
|
||||||
load().catch(() => {
|
|
||||||
// Fallback: fetch from list
|
// Fallback: fetch from list
|
||||||
fetch('/api/sdk/v1/ucca/assessments')
|
fetch('/api/sdk/v1/ucca/assessments')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
@@ -81,12 +97,13 @@ export default function AssessmentDetailPage() {
|
|||||||
if (found) {
|
if (found) {
|
||||||
setAssessment(found)
|
setAssessment(found)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
} else {
|
||||||
|
setError('Assessment nicht gefunden')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => setError('Fehler beim Laden'))
|
||||||
.finally(() => setLoading(false))
|
|
||||||
})
|
})
|
||||||
}
|
.finally(() => setLoading(false))
|
||||||
}, [assessmentId])
|
}, [assessmentId])
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@@ -99,6 +116,26 @@ export default function AssessmentDetailPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleExplain = async () => {
|
||||||
|
setExplaining(true)
|
||||||
|
setExplanationError(null)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/sdk/v1/ucca/assessments/${assessmentId}/explain`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ language: 'de' }),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error('Fehler bei der Erklärung')
|
||||||
|
// Reload assessment to get updated explanation_text
|
||||||
|
const updated = await fetch(`/api/sdk/v1/ucca/assessments/${assessmentId}`)
|
||||||
|
setAssessment(await updated.json())
|
||||||
|
} catch (err) {
|
||||||
|
setExplanationError(err instanceof Error ? err.message : 'Fehler')
|
||||||
|
} finally {
|
||||||
|
setExplaining(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">
|
||||||
@@ -121,6 +158,38 @@ export default function AssessmentDetailPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build result object for AssessmentResultCard from flat assessment fields
|
||||||
|
const resultForCard = {
|
||||||
|
feasibility: assessment.feasibility,
|
||||||
|
risk_level: assessment.risk_level,
|
||||||
|
risk_score: assessment.risk_score,
|
||||||
|
complexity: assessment.complexity,
|
||||||
|
dsfa_recommended: assessment.dsfa_recommended,
|
||||||
|
art22_risk: assessment.art22_risk,
|
||||||
|
training_allowed: assessment.training_allowed,
|
||||||
|
// AssessmentResultCard expects rule_code; backend stores code — map here
|
||||||
|
triggered_rules: assessment.triggered_rules?.map(r => ({
|
||||||
|
rule_code: r.code,
|
||||||
|
title: r.title,
|
||||||
|
severity: r.severity,
|
||||||
|
gdpr_ref: r.gdpr_ref,
|
||||||
|
})),
|
||||||
|
required_controls: assessment.required_controls?.map(c => ({
|
||||||
|
id: c.id,
|
||||||
|
title: c.title,
|
||||||
|
description: c.description,
|
||||||
|
effort: c.category,
|
||||||
|
})),
|
||||||
|
recommended_architecture: assessment.recommended_architecture?.map(p => ({
|
||||||
|
id: p.pattern_id,
|
||||||
|
title: p.title,
|
||||||
|
description: p.description,
|
||||||
|
benefit: p.rationale,
|
||||||
|
})),
|
||||||
|
summary: '',
|
||||||
|
recommendation: '',
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
@@ -131,7 +200,7 @@ export default function AssessmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">{assessment.title || 'Assessment Detail'}</h1>
|
<h1 className="text-2xl font-bold text-gray-900">{assessment.title || 'Assessment Detail'}</h1>
|
||||||
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
|
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
|
||||||
@@ -139,7 +208,28 @@ export default function AssessmentDetailPage() {
|
|||||||
<span>Erstellt: {new Date(assessment.created_at).toLocaleDateString('de-DE')}</span>
|
<span>Erstellt: {new Date(assessment.created_at).toLocaleDateString('de-DE')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap justify-end">
|
||||||
|
<button
|
||||||
|
onClick={handleExplain}
|
||||||
|
disabled={explaining}
|
||||||
|
className="px-4 py-2 text-sm bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{explaining ? 'Generiere...' : '✨ KI-Erklärung'}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={`/api/sdk/v1/ucca/export/${assessmentId}?format=md`}
|
||||||
|
download
|
||||||
|
className="px-4 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
↓ Markdown
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`/api/sdk/v1/ucca/export/${assessmentId}?format=json`}
|
||||||
|
download
|
||||||
|
className="px-4 py-2 text-sm bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
↓ JSON
|
||||||
|
</a>
|
||||||
<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"
|
||||||
@@ -161,23 +251,37 @@ export default function AssessmentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Explanation error */}
|
||||||
|
{explanationError && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700 text-sm">
|
||||||
|
{explanationError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Use Case Text */}
|
{/* Use Case Text */}
|
||||||
{assessment.use_case_text && (
|
{assessment.intake?.use_case_text && (
|
||||||
<div className="bg-gray-50 rounded-xl border border-gray-200 p-6">
|
<div className="bg-gray-50 rounded-xl border border-gray-200 p-6">
|
||||||
<h3 className="text-sm font-medium text-gray-500 mb-2">Beschreibung des Anwendungsfalls</h3>
|
<h3 className="text-sm font-medium text-gray-500 mb-2">Beschreibung des Anwendungsfalls</h3>
|
||||||
<p className="text-gray-800 whitespace-pre-wrap">{assessment.use_case_text}</p>
|
<p className="text-gray-800 whitespace-pre-wrap">{assessment.intake.use_case_text as string}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Result */}
|
{/* Result */}
|
||||||
{assessment.result && (
|
<AssessmentResultCard result={resultForCard as Parameters<typeof AssessmentResultCard>[0]['result']} />
|
||||||
<AssessmentResultCard result={assessment.result} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* No Result */}
|
{/* KI-Erklärung */}
|
||||||
{!assessment.result && (
|
{assessment.explanation_text && (
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-6 text-center">
|
<div className="bg-purple-50 border border-purple-200 rounded-xl p-6">
|
||||||
<p className="text-yellow-700">Dieses Assessment hat noch kein Ergebnis.</p>
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<span className="text-lg">✨</span>
|
||||||
|
<h3 className="text-sm font-semibold text-purple-800">KI-Erklärung</h3>
|
||||||
|
{assessment.explanation_model && (
|
||||||
|
<span className="text-xs text-purple-500 ml-auto">via {assessment.explanation_model}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-purple-900 whitespace-pre-wrap text-sm leading-relaxed">
|
||||||
|
{assessment.explanation_text}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard'
|
import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard'
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -38,10 +38,15 @@ const DOMAINS = [
|
|||||||
// MAIN COMPONENT
|
// MAIN COMPONENT
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export default function NewUseCasePage() {
|
function NewUseCasePageInner() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
const editId = searchParams.get('edit')
|
||||||
|
const isEditMode = !!editId
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState(1)
|
const [currentStep, setCurrentStep] = useState(1)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [editLoading, setEditLoading] = useState(false)
|
||||||
const [result, setResult] = useState<unknown>(null)
|
const [result, setResult] = useState<unknown>(null)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -93,6 +98,52 @@ export default function NewUseCasePage() {
|
|||||||
setForm(prev => ({ ...prev, ...updates }))
|
setForm(prev => ({ ...prev, ...updates }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-fill form when in edit mode
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editId) return
|
||||||
|
setEditLoading(true)
|
||||||
|
fetch(`/api/sdk/v1/ucca/assessments/${editId}`)
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const intake = data.intake || {}
|
||||||
|
setForm({
|
||||||
|
title: data.title || '',
|
||||||
|
use_case_text: intake.use_case_text || '',
|
||||||
|
domain: data.domain || 'general',
|
||||||
|
personal_data: intake.data_types?.personal_data || false,
|
||||||
|
special_categories: intake.data_types?.article_9_data || false,
|
||||||
|
minors_data: intake.data_types?.minor_data || false,
|
||||||
|
health_data: intake.data_types?.health_data || false,
|
||||||
|
biometric_data: intake.data_types?.biometric_data || false,
|
||||||
|
financial_data: intake.data_types?.financial_data || false,
|
||||||
|
purpose_profiling: intake.purpose?.profiling || false,
|
||||||
|
purpose_automated_decision: intake.purpose?.automated_decision || intake.purpose?.decision_making || false,
|
||||||
|
purpose_marketing: intake.purpose?.marketing || false,
|
||||||
|
purpose_analytics: intake.purpose?.analytics || false,
|
||||||
|
purpose_service_delivery: intake.purpose?.service_delivery || intake.purpose?.customer_support || false,
|
||||||
|
automation: intake.automation || 'assistive',
|
||||||
|
hosting_provider: intake.hosting?.provider || 'self_hosted',
|
||||||
|
hosting_region: intake.hosting?.region || 'eu',
|
||||||
|
model_rag: intake.model_usage?.rag || false,
|
||||||
|
model_finetune: intake.model_usage?.finetune || false,
|
||||||
|
model_training: intake.model_usage?.training || false,
|
||||||
|
model_inference: intake.model_usage?.inference ?? true,
|
||||||
|
legal_basis: intake.legal_basis || 'consent',
|
||||||
|
international_transfer: intake.international_transfer?.enabled || false,
|
||||||
|
transfer_countries: intake.international_transfer?.countries || [],
|
||||||
|
transfer_mechanism: intake.international_transfer?.mechanism || 'none',
|
||||||
|
retention_days: intake.retention?.days || 90,
|
||||||
|
retention_purpose: intake.retention?.purpose || '',
|
||||||
|
has_dpa: intake.contracts?.has_dpa || false,
|
||||||
|
has_aia_documentation: intake.contracts?.has_aia_documentation || false,
|
||||||
|
has_risk_assessment: intake.contracts?.has_risk_assessment || false,
|
||||||
|
subprocessors: intake.contracts?.subprocessors || '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setEditLoading(false))
|
||||||
|
}, [editId])
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true)
|
setIsSubmitting(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
@@ -146,8 +197,13 @@ export default function NewUseCasePage() {
|
|||||||
store_raw_text: true,
|
store_raw_text: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/sdk/v1/ucca/assess', {
|
const url = isEditMode
|
||||||
method: 'POST',
|
? `/api/sdk/v1/ucca/assessments/${editId}`
|
||||||
|
: '/api/sdk/v1/ucca/assess'
|
||||||
|
const method = isEditMode ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(intake),
|
body: JSON.stringify(intake),
|
||||||
})
|
})
|
||||||
@@ -157,6 +213,11 @@ export default function NewUseCasePage() {
|
|||||||
throw new Error(errData?.error || `HTTP ${response.status}`)
|
throw new Error(errData?.error || `HTTP ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEditMode) {
|
||||||
|
router.push(`/sdk/use-cases/${editId}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setResult(data)
|
setResult(data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -201,12 +262,23 @@ export default function NewUseCasePage() {
|
|||||||
<div className="max-w-3xl mx-auto space-y-6">
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Neues Use Case Assessment</h1>
|
<h1 className="text-2xl font-bold text-gray-900">
|
||||||
|
{isEditMode ? 'Assessment bearbeiten' : 'Neues Use Case Assessment'}
|
||||||
|
</h1>
|
||||||
<p className="mt-1 text-gray-500">
|
<p className="mt-1 text-gray-500">
|
||||||
Beschreiben Sie Ihren KI-Anwendungsfall Schritt fuer Schritt
|
{isEditMode
|
||||||
|
? 'Angaben anpassen und Assessment neu bewerten'
|
||||||
|
: 'Beschreiben Sie Ihren KI-Anwendungsfall Schritt fuer Schritt'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Edit loading indicator */}
|
||||||
|
{editLoading && (
|
||||||
|
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 text-purple-700 text-sm">
|
||||||
|
Lade Assessment-Daten...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Step Indicator */}
|
{/* Step Indicator */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{WIZARD_STEPS.map((step, idx) => (
|
{WIZARD_STEPS.map((step, idx) => (
|
||||||
@@ -591,7 +663,7 @@ export default function NewUseCasePage() {
|
|||||||
Bewerte...
|
Bewerte...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
'Assessment starten'
|
isEditMode ? 'Speichern & neu bewerten' : 'Assessment starten'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -599,3 +671,11 @@ export default function NewUseCasePage() {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function NewUseCasePage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="flex items-center justify-center h-64 text-gray-500">Lade...</div>}>
|
||||||
|
<NewUseCasePageInner />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ func main() {
|
|||||||
// Assessment management
|
// Assessment management
|
||||||
uccaRoutes.GET("/assessments", uccaHandlers.ListAssessments)
|
uccaRoutes.GET("/assessments", uccaHandlers.ListAssessments)
|
||||||
uccaRoutes.GET("/assessments/:id", uccaHandlers.GetAssessment)
|
uccaRoutes.GET("/assessments/:id", uccaHandlers.GetAssessment)
|
||||||
|
uccaRoutes.PUT("/assessments/:id", uccaHandlers.UpdateAssessment)
|
||||||
uccaRoutes.DELETE("/assessments/:id", uccaHandlers.DeleteAssessment)
|
uccaRoutes.DELETE("/assessments/:id", uccaHandlers.DeleteAssessment)
|
||||||
|
|
||||||
// LLM explanation
|
// LLM explanation
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -213,15 +214,22 @@ func (h *UCCAHandlers) ListAssessments(c *gin.Context) {
|
|||||||
Feasibility: c.Query("feasibility"),
|
Feasibility: c.Query("feasibility"),
|
||||||
Domain: c.Query("domain"),
|
Domain: c.Query("domain"),
|
||||||
RiskLevel: c.Query("risk_level"),
|
RiskLevel: c.Query("risk_level"),
|
||||||
|
Search: c.Query("search"),
|
||||||
|
}
|
||||||
|
if limit, err := strconv.Atoi(c.DefaultQuery("limit", "0")); err == nil {
|
||||||
|
filters.Limit = limit
|
||||||
|
}
|
||||||
|
if offset, err := strconv.Atoi(c.DefaultQuery("offset", "0")); err == nil {
|
||||||
|
filters.Offset = offset
|
||||||
}
|
}
|
||||||
|
|
||||||
assessments, err := h.store.ListAssessments(c.Request.Context(), tenantID, filters)
|
assessments, total, err := h.store.ListAssessments(c.Request.Context(), tenantID, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"assessments": assessments})
|
c.JSON(http.StatusOK, gin.H{"assessments": assessments, "total": total})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -269,6 +277,78 @@ func (h *UCCAHandlers) DeleteAssessment(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// PUT /sdk/v1/ucca/assessments/:id - Update an existing assessment
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// UpdateAssessment re-evaluates and updates an existing assessment
|
||||||
|
func (h *UCCAHandlers) UpdateAssessment(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var intake ucca.UseCaseIntake
|
||||||
|
if err := c.ShouldBindJSON(&intake); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-run evaluation with updated intake
|
||||||
|
var result *ucca.AssessmentResult
|
||||||
|
var policyVersion string
|
||||||
|
if h.policyEngine != nil {
|
||||||
|
result = h.policyEngine.Evaluate(&intake)
|
||||||
|
policyVersion = h.policyEngine.GetPolicyVersion()
|
||||||
|
} else {
|
||||||
|
result = h.legacyRuleEngine.Evaluate(&intake)
|
||||||
|
policyVersion = "1.0.0-legacy"
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.Sum256([]byte(intake.UseCaseText))
|
||||||
|
hashStr := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
|
updated := &ucca.Assessment{
|
||||||
|
Title: intake.Title,
|
||||||
|
PolicyVersion: policyVersion,
|
||||||
|
Intake: intake,
|
||||||
|
UseCaseTextStored: intake.StoreRawText,
|
||||||
|
UseCaseTextHash: hashStr,
|
||||||
|
Feasibility: result.Feasibility,
|
||||||
|
RiskLevel: result.RiskLevel,
|
||||||
|
Complexity: result.Complexity,
|
||||||
|
RiskScore: result.RiskScore,
|
||||||
|
TriggeredRules: result.TriggeredRules,
|
||||||
|
RequiredControls: result.RequiredControls,
|
||||||
|
RecommendedArchitecture: result.RecommendedArchitecture,
|
||||||
|
ForbiddenPatterns: result.ForbiddenPatterns,
|
||||||
|
ExampleMatches: result.ExampleMatches,
|
||||||
|
DSFARecommended: result.DSFARecommended,
|
||||||
|
Art22Risk: result.Art22Risk,
|
||||||
|
TrainingAllowed: result.TrainingAllowed,
|
||||||
|
Domain: intake.Domain,
|
||||||
|
}
|
||||||
|
if !intake.StoreRawText {
|
||||||
|
updated.Intake.UseCaseText = ""
|
||||||
|
}
|
||||||
|
if updated.Title == "" {
|
||||||
|
updated.Title = fmt.Sprintf("Assessment vom %s", time.Now().Format("02.01.2006 15:04"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.store.UpdateAssessment(c.Request.Context(), id, updated); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assessment, err := h.store.GetAssessment(c.Request.Context(), id)
|
||||||
|
if err != nil || assessment == nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "assessment not found after update"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, assessment)
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// GET /sdk/v1/ucca/patterns - Get pattern catalog
|
// GET /sdk/v1/ucca/patterns - Get pattern catalog
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ucca
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -137,7 +138,40 @@ func (s *Store) GetAssessment(ctx context.Context, id uuid.UUID) (*Assessment, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListAssessments lists assessments for a tenant with optional filters
|
// ListAssessments lists assessments for a tenant with optional filters
|
||||||
func (s *Store) ListAssessments(ctx context.Context, tenantID uuid.UUID, filters *AssessmentFilters) ([]Assessment, error) {
|
func (s *Store) ListAssessments(ctx context.Context, tenantID uuid.UUID, filters *AssessmentFilters) ([]Assessment, int, error) {
|
||||||
|
baseWhere := " WHERE tenant_id = $1"
|
||||||
|
args := []interface{}{tenantID}
|
||||||
|
argIdx := 2
|
||||||
|
|
||||||
|
// Build WHERE clause from filters
|
||||||
|
if filters != nil {
|
||||||
|
if filters.Feasibility != "" {
|
||||||
|
baseWhere += " AND feasibility = $" + itoa(argIdx)
|
||||||
|
args = append(args, filters.Feasibility)
|
||||||
|
argIdx++
|
||||||
|
}
|
||||||
|
if filters.Domain != "" {
|
||||||
|
baseWhere += " AND domain = $" + itoa(argIdx)
|
||||||
|
args = append(args, filters.Domain)
|
||||||
|
argIdx++
|
||||||
|
}
|
||||||
|
if filters.RiskLevel != "" {
|
||||||
|
baseWhere += " AND risk_level = $" + itoa(argIdx)
|
||||||
|
args = append(args, filters.RiskLevel)
|
||||||
|
argIdx++
|
||||||
|
}
|
||||||
|
if filters.Search != "" {
|
||||||
|
baseWhere += " AND title ILIKE $" + strconv.Itoa(argIdx)
|
||||||
|
args = append(args, "%"+filters.Search+"%")
|
||||||
|
argIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// COUNT query (same WHERE, no ORDER/LIMIT/OFFSET)
|
||||||
|
var total int
|
||||||
|
s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM ucca_assessments"+baseWhere, args...).Scan(&total)
|
||||||
|
|
||||||
|
// Data query
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
SELECT
|
||||||
id, tenant_id, namespace_id, title, policy_version, status,
|
id, tenant_id, namespace_id, title, policy_version, status,
|
||||||
@@ -149,41 +183,25 @@ func (s *Store) ListAssessments(ctx context.Context, tenantID uuid.UUID, filters
|
|||||||
corpus_version_id, corpus_version,
|
corpus_version_id, corpus_version,
|
||||||
explanation_text, explanation_generated_at, explanation_model,
|
explanation_text, explanation_generated_at, explanation_model,
|
||||||
domain, created_at, updated_at, created_by
|
domain, created_at, updated_at, created_by
|
||||||
FROM ucca_assessments WHERE tenant_id = $1`
|
FROM ucca_assessments` + baseWhere + " ORDER BY created_at DESC"
|
||||||
|
|
||||||
args := []interface{}{tenantID}
|
// Apply LIMIT
|
||||||
argIdx := 2
|
if filters != nil && filters.Limit > 0 {
|
||||||
|
query += " LIMIT $" + strconv.Itoa(argIdx)
|
||||||
// Apply filters
|
args = append(args, filters.Limit)
|
||||||
if filters != nil {
|
argIdx++
|
||||||
if filters.Feasibility != "" {
|
|
||||||
query += " AND feasibility = $" + itoa(argIdx)
|
|
||||||
args = append(args, filters.Feasibility)
|
|
||||||
argIdx++
|
|
||||||
}
|
|
||||||
if filters.Domain != "" {
|
|
||||||
query += " AND domain = $" + itoa(argIdx)
|
|
||||||
args = append(args, filters.Domain)
|
|
||||||
argIdx++
|
|
||||||
}
|
|
||||||
if filters.RiskLevel != "" {
|
|
||||||
query += " AND risk_level = $" + itoa(argIdx)
|
|
||||||
args = append(args, filters.RiskLevel)
|
|
||||||
argIdx++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query += " ORDER BY created_at DESC"
|
// Apply OFFSET
|
||||||
|
if filters != nil && filters.Offset > 0 {
|
||||||
// Apply limit
|
query += " OFFSET $" + strconv.Itoa(argIdx)
|
||||||
if filters != nil && filters.Limit > 0 {
|
args = append(args, filters.Offset)
|
||||||
query += " LIMIT $" + itoa(argIdx)
|
argIdx++
|
||||||
args = append(args, filters.Limit)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := s.pool.Query(ctx, query, args...)
|
rows, err := s.pool.Query(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
@@ -205,7 +223,7 @@ func (s *Store) ListAssessments(ctx context.Context, tenantID uuid.UUID, filters
|
|||||||
&domain, &a.CreatedAt, &a.UpdatedAt, &a.CreatedBy,
|
&domain, &a.CreatedAt, &a.UpdatedAt, &a.CreatedBy,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal JSONB fields
|
// Unmarshal JSONB fields
|
||||||
@@ -226,7 +244,7 @@ func (s *Store) ListAssessments(ctx context.Context, tenantID uuid.UUID, filters
|
|||||||
assessments = append(assessments, a)
|
assessments = append(assessments, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
return assessments, nil
|
return assessments, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAssessment deletes an assessment by ID
|
// DeleteAssessment deletes an assessment by ID
|
||||||
@@ -235,6 +253,34 @@ func (s *Store) DeleteAssessment(ctx context.Context, id uuid.UUID) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAssessment updates an existing assessment's intake and re-evaluated results
|
||||||
|
func (s *Store) UpdateAssessment(ctx context.Context, id uuid.UUID, a *Assessment) error {
|
||||||
|
intake, _ := json.Marshal(a.Intake)
|
||||||
|
triggeredRules, _ := json.Marshal(a.TriggeredRules)
|
||||||
|
requiredControls, _ := json.Marshal(a.RequiredControls)
|
||||||
|
recommendedArchitecture, _ := json.Marshal(a.RecommendedArchitecture)
|
||||||
|
forbiddenPatterns, _ := json.Marshal(a.ForbiddenPatterns)
|
||||||
|
exampleMatches, _ := json.Marshal(a.ExampleMatches)
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
_, err := s.pool.Exec(ctx, `
|
||||||
|
UPDATE ucca_assessments SET
|
||||||
|
title = $2, intake = $3, use_case_text_stored = $4, use_case_text_hash = $5,
|
||||||
|
feasibility = $6, risk_level = $7, complexity = $8, risk_score = $9,
|
||||||
|
triggered_rules = $10, required_controls = $11, recommended_architecture = $12,
|
||||||
|
forbidden_patterns = $13, example_matches = $14,
|
||||||
|
dsfa_recommended = $15, art22_risk = $16, training_allowed = $17,
|
||||||
|
domain = $18, policy_version = $19, updated_at = $20
|
||||||
|
WHERE id = $1
|
||||||
|
`, id, a.Title, intake, a.UseCaseTextStored, a.UseCaseTextHash,
|
||||||
|
string(a.Feasibility), string(a.RiskLevel), string(a.Complexity), a.RiskScore,
|
||||||
|
triggeredRules, requiredControls, recommendedArchitecture,
|
||||||
|
forbiddenPatterns, exampleMatches,
|
||||||
|
a.DSFARecommended, a.Art22Risk, string(a.TrainingAllowed),
|
||||||
|
string(a.Domain), a.PolicyVersion, now)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateExplanation updates the LLM explanation for an assessment
|
// UpdateExplanation updates the LLM explanation for an assessment
|
||||||
func (s *Store) UpdateExplanation(ctx context.Context, id uuid.UUID, explanation string, model string) error {
|
func (s *Store) UpdateExplanation(ctx context.Context, id uuid.UUID, explanation string, model string) error {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
@@ -307,7 +353,9 @@ type AssessmentFilters struct {
|
|||||||
Feasibility string
|
Feasibility string
|
||||||
Domain string
|
Domain string
|
||||||
RiskLevel string
|
RiskLevel string
|
||||||
|
Search string // ILIKE on title
|
||||||
Limit int
|
Limit int
|
||||||
|
Offset int // OFFSET for pagination
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user