This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/api/handlers/escalation_handlers.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

422 lines
13 KiB
Go

package handlers
import (
"net/http"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// EscalationHandlers handles escalation-related API endpoints.
type EscalationHandlers struct {
store *ucca.EscalationStore
assessmentStore *ucca.Store
trigger *ucca.EscalationTrigger
}
// NewEscalationHandlers creates new escalation handlers.
func NewEscalationHandlers(store *ucca.EscalationStore, assessmentStore *ucca.Store) *EscalationHandlers {
return &EscalationHandlers{
store: store,
assessmentStore: assessmentStore,
trigger: ucca.DefaultEscalationTrigger(),
}
}
// ============================================================================
// GET /sdk/v1/ucca/escalations - List escalations
// ============================================================================
// ListEscalations returns escalations for a tenant with optional filters.
func (h *EscalationHandlers) ListEscalations(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
status := c.Query("status")
level := c.Query("level")
var assignedTo *uuid.UUID
if assignedToStr := c.Query("assigned_to"); assignedToStr != "" {
if id, err := uuid.Parse(assignedToStr); err == nil {
assignedTo = &id
}
}
// If user is a reviewer, filter to their assignments by default
userID := rbac.GetUserID(c)
if c.Query("my_reviews") == "true" && userID != uuid.Nil {
assignedTo = &userID
}
escalations, err := h.store.ListEscalations(c.Request.Context(), tenantID, status, level, assignedTo)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"escalations": escalations})
}
// ============================================================================
// GET /sdk/v1/ucca/escalations/:id - Get single escalation
// ============================================================================
// GetEscalation returns a single escalation by ID.
func (h *EscalationHandlers) GetEscalation(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
escalation, err := h.store.GetEscalation(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if escalation == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
// Get history
history, _ := h.store.GetEscalationHistory(c.Request.Context(), id)
c.JSON(http.StatusOK, gin.H{
"escalation": escalation,
"history": history,
})
}
// ============================================================================
// POST /sdk/v1/ucca/escalations - Create escalation (manual)
// ============================================================================
// CreateEscalation creates a manual escalation for an assessment.
func (h *EscalationHandlers) CreateEscalation(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var req ucca.CreateEscalationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Get the assessment
assessment, err := h.assessmentStore.GetAssessment(c.Request.Context(), req.AssessmentID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if assessment == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "assessment not found"})
return
}
// Determine escalation level
result := &ucca.AssessmentResult{
Feasibility: assessment.Feasibility,
RiskLevel: assessment.RiskLevel,
RiskScore: assessment.RiskScore,
TriggeredRules: assessment.TriggeredRules,
DSFARecommended: assessment.DSFARecommended,
Art22Risk: assessment.Art22Risk,
}
level, reason := h.trigger.DetermineEscalationLevel(result)
// Calculate due date based on SLA
responseHours, _ := ucca.GetDefaultSLA(level)
var dueDate *time.Time
if responseHours > 0 {
due := time.Now().UTC().Add(time.Duration(responseHours) * time.Hour)
dueDate = &due
}
// Create escalation
escalation := &ucca.Escalation{
TenantID: tenantID,
AssessmentID: req.AssessmentID,
EscalationLevel: level,
EscalationReason: reason,
Status: ucca.EscalationStatusPending,
DueDate: dueDate,
}
// For E0, auto-approve
if level == ucca.EscalationLevelE0 {
escalation.Status = ucca.EscalationStatusApproved
approveDecision := ucca.EscalationDecisionApprove
escalation.Decision = &approveDecision
now := time.Now().UTC()
escalation.DecisionAt = &now
autoNotes := "Automatische Freigabe (E0)"
escalation.DecisionNotes = &autoNotes
}
if err := h.store.CreateEscalation(c.Request.Context(), escalation); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Add history entry
h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
EscalationID: escalation.ID,
Action: "created",
NewStatus: string(escalation.Status),
NewLevel: string(escalation.EscalationLevel),
ActorID: userID,
Notes: reason,
})
// For E1/E2/E3, try to auto-assign
if level != ucca.EscalationLevelE0 {
role := ucca.GetRoleForLevel(level)
reviewer, err := h.store.GetNextAvailableReviewer(c.Request.Context(), tenantID, role)
if err == nil && reviewer != nil {
h.store.AssignEscalation(c.Request.Context(), escalation.ID, reviewer.UserID, role)
h.store.IncrementReviewerCount(c.Request.Context(), reviewer.UserID)
h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
EscalationID: escalation.ID,
Action: "auto_assigned",
OldStatus: string(ucca.EscalationStatusPending),
NewStatus: string(ucca.EscalationStatusAssigned),
ActorID: userID,
Notes: "Automatisch zugewiesen an: " + reviewer.UserName,
})
}
}
c.JSON(http.StatusCreated, escalation)
}
// ============================================================================
// POST /sdk/v1/ucca/escalations/:id/assign - Assign escalation
// ============================================================================
// AssignEscalation assigns an escalation to a reviewer.
func (h *EscalationHandlers) AssignEscalation(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
userID := rbac.GetUserID(c)
var req ucca.AssignEscalationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
escalation, err := h.store.GetEscalation(c.Request.Context(), id)
if err != nil || escalation == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "escalation not found"})
return
}
role := ucca.GetRoleForLevel(escalation.EscalationLevel)
if err := h.store.AssignEscalation(c.Request.Context(), id, req.AssignedTo, role); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.store.IncrementReviewerCount(c.Request.Context(), req.AssignedTo)
h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
EscalationID: id,
Action: "assigned",
OldStatus: string(escalation.Status),
NewStatus: string(ucca.EscalationStatusAssigned),
ActorID: userID,
})
c.JSON(http.StatusOK, gin.H{"message": "assigned"})
}
// ============================================================================
// POST /sdk/v1/ucca/escalations/:id/review - Start review
// ============================================================================
// StartReview marks an escalation as being reviewed.
func (h *EscalationHandlers) StartReview(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
userID := rbac.GetUserID(c)
if userID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "user ID required"})
return
}
escalation, err := h.store.GetEscalation(c.Request.Context(), id)
if err != nil || escalation == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "escalation not found"})
return
}
if err := h.store.StartReview(c.Request.Context(), id, userID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
EscalationID: id,
Action: "review_started",
OldStatus: string(escalation.Status),
NewStatus: string(ucca.EscalationStatusInReview),
ActorID: userID,
})
c.JSON(http.StatusOK, gin.H{"message": "review started"})
}
// ============================================================================
// POST /sdk/v1/ucca/escalations/:id/decide - Make decision
// ============================================================================
// DecideEscalation makes a decision on an escalation.
func (h *EscalationHandlers) DecideEscalation(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
userID := rbac.GetUserID(c)
if userID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "user ID required"})
return
}
var req ucca.DecideEscalationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
escalation, err := h.store.GetEscalation(c.Request.Context(), id)
if err != nil || escalation == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "escalation not found"})
return
}
if err := h.store.DecideEscalation(c.Request.Context(), id, req.Decision, req.DecisionNotes, req.Conditions); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Decrement reviewer count
if escalation.AssignedTo != nil {
h.store.DecrementReviewerCount(c.Request.Context(), *escalation.AssignedTo)
}
newStatus := "decided"
switch req.Decision {
case ucca.EscalationDecisionApprove:
newStatus = string(ucca.EscalationStatusApproved)
case ucca.EscalationDecisionReject:
newStatus = string(ucca.EscalationStatusRejected)
case ucca.EscalationDecisionModify:
newStatus = string(ucca.EscalationStatusReturned)
case ucca.EscalationDecisionEscalate:
newStatus = "escalated"
}
h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{
EscalationID: id,
Action: "decision_made",
OldStatus: string(escalation.Status),
NewStatus: newStatus,
ActorID: userID,
Notes: req.DecisionNotes,
})
c.JSON(http.StatusOK, gin.H{"message": "decision recorded", "status": newStatus})
}
// ============================================================================
// GET /sdk/v1/ucca/escalations/stats - Get statistics
// ============================================================================
// GetEscalationStats returns escalation statistics.
func (h *EscalationHandlers) GetEscalationStats(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
stats, err := h.store.GetEscalationStats(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// ============================================================================
// DSB Pool Management
// ============================================================================
// ListDSBPool returns the DSB review pool for a tenant.
func (h *EscalationHandlers) ListDSBPool(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
role := c.Query("role")
members, err := h.store.GetDSBPoolMembers(c.Request.Context(), tenantID, role)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"members": members})
}
// AddDSBPoolMember adds a member to the DSB pool.
func (h *EscalationHandlers) AddDSBPoolMember(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var member ucca.DSBPoolMember
if err := c.ShouldBindJSON(&member); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
member.TenantID = tenantID
member.IsActive = true
if member.MaxConcurrentReviews == 0 {
member.MaxConcurrentReviews = 10
}
if err := h.store.AddDSBPoolMember(c.Request.Context(), &member); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, member)
}