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/roadmap_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

741 lines
19 KiB
Go

package handlers
import (
"bytes"
"io"
"net/http"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/breakpilot/ai-compliance-sdk/internal/roadmap"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// RoadmapHandlers handles roadmap-related HTTP requests
type RoadmapHandlers struct {
store *roadmap.Store
parser *roadmap.Parser
}
// NewRoadmapHandlers creates new roadmap handlers
func NewRoadmapHandlers(store *roadmap.Store) *RoadmapHandlers {
return &RoadmapHandlers{
store: store,
parser: roadmap.NewParser(),
}
}
// ============================================================================
// Roadmap CRUD
// ============================================================================
// CreateRoadmap creates a new roadmap
// POST /sdk/v1/roadmaps
func (h *RoadmapHandlers) CreateRoadmap(c *gin.Context) {
var req roadmap.CreateRoadmapRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
r := &roadmap.Roadmap{
TenantID: tenantID,
Title: req.Title,
Description: req.Description,
AssessmentID: req.AssessmentID,
PortfolioID: req.PortfolioID,
StartDate: req.StartDate,
TargetDate: req.TargetDate,
Status: "draft",
CreatedBy: userID,
}
if err := h.store.CreateRoadmap(c.Request.Context(), r); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, roadmap.CreateRoadmapResponse{Roadmap: *r})
}
// ListRoadmaps lists roadmaps for the tenant
// GET /sdk/v1/roadmaps
func (h *RoadmapHandlers) ListRoadmaps(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
filters := &roadmap.RoadmapFilters{
Status: c.Query("status"),
Limit: 50,
}
if assessmentID := c.Query("assessment_id"); assessmentID != "" {
if id, err := uuid.Parse(assessmentID); err == nil {
filters.AssessmentID = &id
}
}
roadmaps, err := h.store.ListRoadmaps(c.Request.Context(), tenantID, filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"roadmaps": roadmaps,
"total": len(roadmaps),
})
}
// GetRoadmap retrieves a roadmap by ID
// GET /sdk/v1/roadmaps/:id
func (h *RoadmapHandlers) GetRoadmap(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
r, err := h.store.GetRoadmap(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if r == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "roadmap not found"})
return
}
// Get items
items, err := h.store.ListItems(c.Request.Context(), id, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Get stats
stats, _ := h.store.GetRoadmapStats(c.Request.Context(), id)
c.JSON(http.StatusOK, gin.H{
"roadmap": r,
"items": items,
"stats": stats,
})
}
// UpdateRoadmap updates a roadmap
// PUT /sdk/v1/roadmaps/:id
func (h *RoadmapHandlers) UpdateRoadmap(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
r, err := h.store.GetRoadmap(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if r == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "roadmap not found"})
return
}
var req roadmap.CreateRoadmapRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
r.Title = req.Title
r.Description = req.Description
r.AssessmentID = req.AssessmentID
r.PortfolioID = req.PortfolioID
r.StartDate = req.StartDate
r.TargetDate = req.TargetDate
if err := h.store.UpdateRoadmap(c.Request.Context(), r); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"roadmap": r})
}
// DeleteRoadmap deletes a roadmap
// DELETE /sdk/v1/roadmaps/:id
func (h *RoadmapHandlers) DeleteRoadmap(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteRoadmap(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "roadmap deleted"})
}
// GetRoadmapStats returns statistics for a roadmap
// GET /sdk/v1/roadmaps/:id/stats
func (h *RoadmapHandlers) GetRoadmapStats(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
stats, err := h.store.GetRoadmapStats(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// ============================================================================
// RoadmapItem CRUD
// ============================================================================
// CreateItem creates a new roadmap item
// POST /sdk/v1/roadmaps/:id/items
func (h *RoadmapHandlers) CreateItem(c *gin.Context) {
roadmapID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"})
return
}
var input roadmap.RoadmapItemInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item := &roadmap.RoadmapItem{
RoadmapID: roadmapID,
Title: input.Title,
Description: input.Description,
Category: input.Category,
Priority: input.Priority,
Status: input.Status,
ControlID: input.ControlID,
RegulationRef: input.RegulationRef,
GapID: input.GapID,
EffortDays: input.EffortDays,
AssigneeName: input.AssigneeName,
Department: input.Department,
PlannedStart: input.PlannedStart,
PlannedEnd: input.PlannedEnd,
Notes: input.Notes,
}
if err := h.store.CreateItem(c.Request.Context(), item); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Update roadmap progress
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
c.JSON(http.StatusCreated, gin.H{"item": item})
}
// ListItems lists items for a roadmap
// GET /sdk/v1/roadmaps/:id/items
func (h *RoadmapHandlers) ListItems(c *gin.Context) {
roadmapID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid roadmap ID"})
return
}
filters := &roadmap.RoadmapItemFilters{
SearchQuery: c.Query("search"),
Limit: 100,
}
if status := c.Query("status"); status != "" {
filters.Status = roadmap.ItemStatus(status)
}
if priority := c.Query("priority"); priority != "" {
filters.Priority = roadmap.ItemPriority(priority)
}
if category := c.Query("category"); category != "" {
filters.Category = roadmap.ItemCategory(category)
}
if controlID := c.Query("control_id"); controlID != "" {
filters.ControlID = controlID
}
items, err := h.store.ListItems(c.Request.Context(), roadmapID, filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"items": items,
"total": len(items),
})
}
// GetItem retrieves a roadmap item
// GET /sdk/v1/roadmap-items/:id
func (h *RoadmapHandlers) GetItem(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
item, err := h.store.GetItem(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if item == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
return
}
c.JSON(http.StatusOK, gin.H{"item": item})
}
// UpdateItem updates a roadmap item
// PUT /sdk/v1/roadmap-items/:id
func (h *RoadmapHandlers) UpdateItem(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
item, err := h.store.GetItem(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if item == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
return
}
var input roadmap.RoadmapItemInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Update fields
item.Title = input.Title
item.Description = input.Description
if input.Category != "" {
item.Category = input.Category
}
if input.Priority != "" {
item.Priority = input.Priority
}
if input.Status != "" {
item.Status = input.Status
}
item.ControlID = input.ControlID
item.RegulationRef = input.RegulationRef
item.GapID = input.GapID
item.EffortDays = input.EffortDays
item.AssigneeName = input.AssigneeName
item.Department = input.Department
item.PlannedStart = input.PlannedStart
item.PlannedEnd = input.PlannedEnd
item.Notes = input.Notes
if err := h.store.UpdateItem(c.Request.Context(), item); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Update roadmap progress
h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID)
c.JSON(http.StatusOK, gin.H{"item": item})
}
// UpdateItemStatus updates just the status of a roadmap item
// PATCH /sdk/v1/roadmap-items/:id/status
func (h *RoadmapHandlers) UpdateItemStatus(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var req struct {
Status roadmap.ItemStatus `json:"status"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
item, err := h.store.GetItem(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if item == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
return
}
item.Status = req.Status
// Set actual dates
now := time.Now().UTC()
if req.Status == roadmap.ItemStatusInProgress && item.ActualStart == nil {
item.ActualStart = &now
}
if req.Status == roadmap.ItemStatusCompleted && item.ActualEnd == nil {
item.ActualEnd = &now
}
if err := h.store.UpdateItem(c.Request.Context(), item); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Update roadmap progress
h.store.UpdateRoadmapProgress(c.Request.Context(), item.RoadmapID)
c.JSON(http.StatusOK, gin.H{"item": item})
}
// DeleteItem deletes a roadmap item
// DELETE /sdk/v1/roadmap-items/:id
func (h *RoadmapHandlers) DeleteItem(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
item, err := h.store.GetItem(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if item == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "item not found"})
return
}
roadmapID := item.RoadmapID
if err := h.store.DeleteItem(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Update roadmap progress
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
c.JSON(http.StatusOK, gin.H{"message": "item deleted"})
}
// ============================================================================
// Import Workflow
// ============================================================================
// UploadImport handles file upload for import
// POST /sdk/v1/roadmaps/import/upload
func (h *RoadmapHandlers) UploadImport(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
// Get file from form
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "file is required"})
return
}
defer file.Close()
// Read file content
buf := bytes.Buffer{}
if _, err := io.Copy(&buf, file); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file"})
return
}
// Detect format
format := roadmap.ImportFormat("")
filename := header.Filename
contentType := header.Header.Get("Content-Type")
// Create import job
job := &roadmap.ImportJob{
TenantID: tenantID,
Filename: filename,
FileSize: header.Size,
ContentType: contentType,
Status: "pending",
CreatedBy: userID,
}
if err := h.store.CreateImportJob(c.Request.Context(), job); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Parse the file
job.Status = "parsing"
h.store.UpdateImportJob(c.Request.Context(), job)
result, err := h.parser.ParseFile(buf.Bytes(), filename, contentType)
if err != nil {
job.Status = "failed"
job.ErrorMessage = err.Error()
h.store.UpdateImportJob(c.Request.Context(), job)
c.JSON(http.StatusBadRequest, gin.H{
"error": "failed to parse file",
"detail": err.Error(),
})
return
}
// Update job with parsed data
job.Status = "parsed"
job.Format = format
job.TotalRows = result.TotalRows
job.ValidRows = result.ValidRows
job.InvalidRows = result.InvalidRows
job.ParsedItems = result.Items
h.store.UpdateImportJob(c.Request.Context(), job)
c.JSON(http.StatusOK, roadmap.ImportParseResponse{
JobID: job.ID,
Status: job.Status,
TotalRows: result.TotalRows,
ValidRows: result.ValidRows,
InvalidRows: result.InvalidRows,
Items: result.Items,
ColumnMap: buildColumnMap(result.Columns),
})
}
// GetImportJob returns the status of an import job
// GET /sdk/v1/roadmaps/import/:jobId
func (h *RoadmapHandlers) GetImportJob(c *gin.Context) {
jobID, err := uuid.Parse(c.Param("jobId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"})
return
}
job, err := h.store.GetImportJob(c.Request.Context(), jobID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if job == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"job": job,
"items": job.ParsedItems,
})
}
// ConfirmImport confirms and executes the import
// POST /sdk/v1/roadmaps/import/:jobId/confirm
func (h *RoadmapHandlers) ConfirmImport(c *gin.Context) {
jobID, err := uuid.Parse(c.Param("jobId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job ID"})
return
}
var req roadmap.ImportConfirmRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
job, err := h.store.GetImportJob(c.Request.Context(), jobID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if job == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "import job not found"})
return
}
if job.Status != "parsed" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "job is not ready for confirmation",
"status": job.Status,
})
return
}
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
// Create or use existing roadmap
var roadmapID uuid.UUID
if req.RoadmapID != nil {
roadmapID = *req.RoadmapID
// Verify roadmap exists
r, err := h.store.GetRoadmap(c.Request.Context(), roadmapID)
if err != nil || r == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "roadmap not found"})
return
}
} else {
// Create new roadmap
title := req.RoadmapTitle
if title == "" {
title = "Imported Roadmap - " + job.Filename
}
r := &roadmap.Roadmap{
TenantID: tenantID,
Title: title,
Status: "active",
CreatedBy: userID,
}
if err := h.store.CreateRoadmap(c.Request.Context(), r); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
roadmapID = r.ID
}
// Determine which rows to import
selectedRows := make(map[int]bool)
if len(req.SelectedRows) > 0 {
for _, row := range req.SelectedRows {
selectedRows[row] = true
}
}
// Convert parsed items to roadmap items
var items []roadmap.RoadmapItem
var importedCount, skippedCount int
for i, parsed := range job.ParsedItems {
// Skip invalid items
if !parsed.IsValid {
skippedCount++
continue
}
// Skip unselected rows if selection was specified
if len(selectedRows) > 0 && !selectedRows[parsed.RowNumber] {
skippedCount++
continue
}
item := roadmap.RoadmapItem{
RoadmapID: roadmapID,
Title: parsed.Data.Title,
Description: parsed.Data.Description,
Category: parsed.Data.Category,
Priority: parsed.Data.Priority,
Status: parsed.Data.Status,
ControlID: parsed.Data.ControlID,
RegulationRef: parsed.Data.RegulationRef,
GapID: parsed.Data.GapID,
EffortDays: parsed.Data.EffortDays,
AssigneeName: parsed.Data.AssigneeName,
Department: parsed.Data.Department,
PlannedStart: parsed.Data.PlannedStart,
PlannedEnd: parsed.Data.PlannedEnd,
Notes: parsed.Data.Notes,
SourceRow: parsed.RowNumber,
SourceFile: job.Filename,
SortOrder: i,
}
// Apply auto-mappings if requested
if req.ApplyMappings {
if parsed.MatchedControl != "" {
item.ControlID = parsed.MatchedControl
}
if parsed.MatchedRegulation != "" {
item.RegulationRef = parsed.MatchedRegulation
}
if parsed.MatchedGap != "" {
item.GapID = parsed.MatchedGap
}
}
// Set defaults
if item.Status == "" {
item.Status = roadmap.ItemStatusPlanned
}
if item.Priority == "" {
item.Priority = roadmap.ItemPriorityMedium
}
if item.Category == "" {
item.Category = roadmap.ItemCategoryTechnical
}
items = append(items, item)
importedCount++
}
// Bulk create items
if len(items) > 0 {
if err := h.store.BulkCreateItems(c.Request.Context(), items); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
// Update roadmap progress
h.store.UpdateRoadmapProgress(c.Request.Context(), roadmapID)
// Update job status
now := time.Now().UTC()
job.Status = "completed"
job.RoadmapID = &roadmapID
job.ImportedItems = importedCount
job.CompletedAt = &now
h.store.UpdateImportJob(c.Request.Context(), job)
c.JSON(http.StatusOK, roadmap.ImportConfirmResponse{
RoadmapID: roadmapID,
ImportedItems: importedCount,
SkippedItems: skippedCount,
Message: "Import completed successfully",
})
}
// ============================================================================
// Helper Functions
// ============================================================================
func buildColumnMap(columns []roadmap.DetectedColumn) map[string]string {
result := make(map[string]string)
for _, col := range columns {
if col.MappedTo != "" {
result[col.Header] = col.MappedTo
}
}
return result
}