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>
This commit is contained in:
421
ai-compliance-sdk/internal/api/handlers/escalation_handlers.go
Normal file
421
ai-compliance-sdk/internal/api/handlers/escalation_handlers.go
Normal file
@@ -0,0 +1,421 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user