From 312c2c9b6067fa38e4fdb660dd28321724fb9120 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 3 Mar 2026 15:13:42 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Use-Cases/UCCA=20Module=20auf=20100%=20?= =?UTF-8?q?=E2=80=94=20Interface=20Fix,=20Search/Offset/Total,=20Explain/E?= =?UTF-8?q?xport,=20Edit-Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../app/(sdk)/sdk/use-cases/[id]/page.tsx | 230 +++++++++++++----- .../app/(sdk)/sdk/use-cases/new/page.tsx | 96 +++++++- ai-compliance-sdk/cmd/server/main.go | 1 + .../internal/api/handlers/ucca_handlers.go | 84 ++++++- ai-compliance-sdk/internal/ucca/store.go | 110 ++++++--- 5 files changed, 417 insertions(+), 104 deletions(-) diff --git a/admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx b/admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx index 54486de..91aebdc 100644 --- a/admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/use-cases/[id]/page.tsx @@ -5,44 +5,66 @@ import { useParams, useRouter } from 'next/navigation' import Link from 'next/link' 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 { id: string title: string tenant_id: string domain: string created_at: string - use_case_text?: string - intake?: Record - result?: { - 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 - }> + intake?: { + use_case_text?: string + [key: string]: unknown } + // 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() { @@ -53,26 +75,20 @@ export default function AssessmentDetailPage() { const [assessment, setAssessment] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [explaining, setExplaining] = useState(false) + const [explanationError, setExplanationError] = useState(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(() => { - async function load() { - try { - const response = await fetch(`/api/sdk/v1/ucca/assessments/${assessmentId}`) - if (!response.ok) { - 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(() => { + if (!assessmentId) return + loadAssessment() + .then(data => setAssessment(data)) + .catch(() => { // Fallback: fetch from list fetch('/api/sdk/v1/ucca/assessments') .then(r => r.json()) @@ -81,12 +97,13 @@ export default function AssessmentDetailPage() { if (found) { setAssessment(found) setError(null) + } else { + setError('Assessment nicht gefunden') } }) - .catch(() => {}) - .finally(() => setLoading(false)) + .catch(() => setError('Fehler beim Laden')) }) - } + .finally(() => setLoading(false)) }, [assessmentId]) 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) { return (
@@ -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 (
{/* Breadcrumb */} @@ -131,7 +200,7 @@ export default function AssessmentDetailPage() {
{/* Header */} -
+

{assessment.title || 'Assessment Detail'}

@@ -139,7 +208,28 @@ export default function AssessmentDetailPage() { Erstellt: {new Date(assessment.created_at).toLocaleDateString('de-DE')}
-
+
+ + + ↓ Markdown + + + ↓ JSON +
+ {/* Explanation error */} + {explanationError && ( +
+ {explanationError} +
+ )} + {/* Use Case Text */} - {assessment.use_case_text && ( + {assessment.intake?.use_case_text && (

Beschreibung des Anwendungsfalls

-

{assessment.use_case_text}

+

{assessment.intake.use_case_text as string}

)} {/* Result */} - {assessment.result && ( - - )} + [0]['result']} /> - {/* No Result */} - {!assessment.result && ( -
-

Dieses Assessment hat noch kein Ergebnis.

+ {/* KI-Erklärung */} + {assessment.explanation_text && ( +
+
+ +

KI-Erklärung

+ {assessment.explanation_model && ( + via {assessment.explanation_model} + )} +
+

+ {assessment.explanation_text} +

)}
diff --git a/admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx b/admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx index 3b83670..978d06e 100644 --- a/admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx +++ b/admin-compliance/app/(sdk)/sdk/use-cases/new/page.tsx @@ -1,7 +1,7 @@ 'use client' -import React, { useState } from 'react' -import { useRouter } from 'next/navigation' +import React, { useState, useEffect, Suspense } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' import { AssessmentResultCard } from '@/components/sdk/use-case-assessment/AssessmentResultCard' // ============================================================================= @@ -38,10 +38,15 @@ const DOMAINS = [ // MAIN COMPONENT // ============================================================================= -export default function NewUseCasePage() { +function NewUseCasePageInner() { const router = useRouter() + const searchParams = useSearchParams() + const editId = searchParams.get('edit') + const isEditMode = !!editId + const [currentStep, setCurrentStep] = useState(1) const [isSubmitting, setIsSubmitting] = useState(false) + const [editLoading, setEditLoading] = useState(false) const [result, setResult] = useState(null) const [error, setError] = useState(null) @@ -93,6 +98,52 @@ export default function NewUseCasePage() { 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 () => { setIsSubmitting(true) setError(null) @@ -146,8 +197,13 @@ export default function NewUseCasePage() { store_raw_text: true, } - const response = await fetch('/api/sdk/v1/ucca/assess', { - method: 'POST', + const url = isEditMode + ? `/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' }, body: JSON.stringify(intake), }) @@ -157,6 +213,11 @@ export default function NewUseCasePage() { throw new Error(errData?.error || `HTTP ${response.status}`) } + if (isEditMode) { + router.push(`/sdk/use-cases/${editId}`) + return + } + const data = await response.json() setResult(data) } catch (err) { @@ -201,12 +262,23 @@ export default function NewUseCasePage() {
{/* Header */}
-

Neues Use Case Assessment

+

+ {isEditMode ? 'Assessment bearbeiten' : 'Neues Use Case Assessment'} +

- Beschreiben Sie Ihren KI-Anwendungsfall Schritt fuer Schritt + {isEditMode + ? 'Angaben anpassen und Assessment neu bewerten' + : 'Beschreiben Sie Ihren KI-Anwendungsfall Schritt fuer Schritt'}

+ {/* Edit loading indicator */} + {editLoading && ( +
+ Lade Assessment-Daten... +
+ )} + {/* Step Indicator */}
{WIZARD_STEPS.map((step, idx) => ( @@ -591,7 +663,7 @@ export default function NewUseCasePage() { Bewerte... ) : ( - 'Assessment starten' + isEditMode ? 'Speichern & neu bewerten' : 'Assessment starten' )} )} @@ -599,3 +671,11 @@ export default function NewUseCasePage() {
) } + +export default function NewUseCasePage() { + return ( + Lade...
}> + + + ) +} diff --git a/ai-compliance-sdk/cmd/server/main.go b/ai-compliance-sdk/cmd/server/main.go index 42fbafc..29968bd 100644 --- a/ai-compliance-sdk/cmd/server/main.go +++ b/ai-compliance-sdk/cmd/server/main.go @@ -311,6 +311,7 @@ func main() { // Assessment management uccaRoutes.GET("/assessments", uccaHandlers.ListAssessments) uccaRoutes.GET("/assessments/:id", uccaHandlers.GetAssessment) + uccaRoutes.PUT("/assessments/:id", uccaHandlers.UpdateAssessment) uccaRoutes.DELETE("/assessments/:id", uccaHandlers.DeleteAssessment) // LLM explanation diff --git a/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go b/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go index 15a0331..97acb26 100644 --- a/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go +++ b/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "net/http" + "strconv" "strings" "time" @@ -213,15 +214,22 @@ func (h *UCCAHandlers) ListAssessments(c *gin.Context) { Feasibility: c.Query("feasibility"), Domain: c.Query("domain"), 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 { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 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"}) } +// ============================================================================ +// 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 // ============================================================================ diff --git a/ai-compliance-sdk/internal/ucca/store.go b/ai-compliance-sdk/internal/ucca/store.go index a9862d6..f261e9f 100644 --- a/ai-compliance-sdk/internal/ucca/store.go +++ b/ai-compliance-sdk/internal/ucca/store.go @@ -3,6 +3,7 @@ package ucca import ( "context" "encoding/json" + "strconv" "time" "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 -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 := ` SELECT 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, explanation_text, explanation_generated_at, explanation_model, 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} - argIdx := 2 - - // Apply filters - if filters != nil { - 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++ - } + // Apply LIMIT + if filters != nil && filters.Limit > 0 { + query += " LIMIT $" + strconv.Itoa(argIdx) + args = append(args, filters.Limit) + argIdx++ } - query += " ORDER BY created_at DESC" - - // Apply limit - if filters != nil && filters.Limit > 0 { - query += " LIMIT $" + itoa(argIdx) - args = append(args, filters.Limit) + // Apply OFFSET + if filters != nil && filters.Offset > 0 { + query += " OFFSET $" + strconv.Itoa(argIdx) + args = append(args, filters.Offset) + argIdx++ } rows, err := s.pool.Query(ctx, query, args...) if err != nil { - return nil, err + return nil, 0, err } 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, ) if err != nil { - return nil, err + return nil, 0, err } // Unmarshal JSONB fields @@ -226,7 +244,7 @@ func (s *Store) ListAssessments(ctx context.Context, tenantID uuid.UUID, filters assessments = append(assessments, a) } - return assessments, nil + return assessments, total, nil } // DeleteAssessment deletes an assessment by ID @@ -235,6 +253,34 @@ func (s *Store) DeleteAssessment(ctx context.Context, id uuid.UUID) error { 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 func (s *Store) UpdateExplanation(ctx context.Context, id uuid.UUID, explanation string, model string) error { now := time.Now().UTC() @@ -307,7 +353,9 @@ type AssessmentFilters struct { Feasibility string Domain string RiskLevel string + Search string // ILIKE on title Limit int + Offset int // OFFSET for pagination } // ============================================================================