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:
@@ -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
|
||||
// ============================================================================
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user