iace_handler.go (2706 LOC) split into 9 files: - iace_handler.go: struct, constructor, shared helpers (~156 LOC) - iace_handler_projects.go: project CRUD + InitFromProfile (~310 LOC) - iace_handler_components.go: components + classification (~387 LOC) - iace_handler_hazards.go: hazard library, CRUD, risk assessment (~469 LOC) - iace_handler_mitigations.go: mitigations, evidence, verification plans (~293 LOC) - iace_handler_techfile.go: CE tech file generation/export (~452 LOC) - iace_handler_monitoring.go: monitoring events + audit trail (~134 LOC) - iace_handler_refdata.go: ISO 12100 ref data, patterns, suggestions (~465 LOC) - iace_handler_rag.go: RAG library search + section enrichment (~142 LOC) training_handlers.go (1864 LOC) split into 9 files: - training_handlers.go: struct + constructor (~23 LOC) - training_handlers_modules.go: module CRUD (~226 LOC) - training_handlers_matrix.go: CTM matrix endpoints (~95 LOC) - training_handlers_assignments.go: assignment lifecycle (~243 LOC) - training_handlers_quiz.go: quiz submit/grade/attempts (~185 LOC) - training_handlers_content.go: LLM content/audio/video generation (~274 LOC) - training_handlers_media.go: media, streaming, interactive video (~325 LOC) - training_handlers_blocks.go: block configs + canonical controls (~280 LOC) - training_handlers_stats.go: deadlines, escalation, audit, certificates (~290 LOC) All files remain in package handlers. Zero behavior changes. All exported function names preserved. All files under 500 LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
453 lines
14 KiB
Go
453 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// CE Technical File
|
|
// ============================================================================
|
|
|
|
// GenerateTechFile handles POST /projects/:id/tech-file/generate
|
|
// Generates technical file sections for a project.
|
|
// TODO: Integrate LLM for intelligent content generation based on project data.
|
|
func (h *IACEHandler) GenerateTechFile(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
// Define the standard CE technical file sections to generate
|
|
sectionDefinitions := []struct {
|
|
SectionType string
|
|
Title string
|
|
}{
|
|
{"general_description", "General Description of the Machinery"},
|
|
{"risk_assessment_report", "Risk Assessment Report"},
|
|
{"hazard_log_combined", "Combined Hazard Log"},
|
|
{"essential_requirements", "Essential Health and Safety Requirements"},
|
|
{"design_specifications", "Design Specifications and Drawings"},
|
|
{"test_reports", "Test Reports and Verification Results"},
|
|
{"standards_applied", "Applied Harmonised Standards"},
|
|
{"declaration_of_conformity", "EU Declaration of Conformity"},
|
|
}
|
|
|
|
// Check if project has AI components for additional sections
|
|
components, _ := h.store.ListComponents(c.Request.Context(), projectID)
|
|
hasAI := false
|
|
for _, comp := range components {
|
|
if comp.ComponentType == iace.ComponentTypeAIModel {
|
|
hasAI = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasAI {
|
|
sectionDefinitions = append(sectionDefinitions,
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_intended_purpose", "AI System Intended Purpose"},
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_model_description", "AI Model Description and Training Data"},
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_risk_management", "AI Risk Management System"},
|
|
struct {
|
|
SectionType string
|
|
Title string
|
|
}{"ai_human_oversight", "AI Human Oversight Measures"},
|
|
)
|
|
}
|
|
|
|
// Generate each section with LLM-based content
|
|
var sections []iace.TechFileSection
|
|
existingSections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
existingMap := make(map[string]bool)
|
|
for _, s := range existingSections {
|
|
existingMap[s.SectionType] = true
|
|
}
|
|
|
|
for _, def := range sectionDefinitions {
|
|
// Skip sections that already exist
|
|
if existingMap[def.SectionType] {
|
|
continue
|
|
}
|
|
|
|
// Generate content via LLM (falls back to structured placeholder if LLM unavailable)
|
|
content, _ := h.techFileGen.GenerateSection(c.Request.Context(), projectID, def.SectionType)
|
|
if content == "" {
|
|
content = fmt.Sprintf("[Sektion: %s — Inhalt wird generiert]", def.Title)
|
|
}
|
|
|
|
section, err := h.store.CreateTechFileSection(
|
|
c.Request.Context(), projectID, def.SectionType, def.Title, content,
|
|
)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
sections = append(sections, *section)
|
|
}
|
|
|
|
// Update project status
|
|
h.store.UpdateProjectStatus(c.Request.Context(), projectID, iace.ProjectStatusTechFile)
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "tech_file", projectID,
|
|
iace.AuditActionCreate, userID.String(), nil, nil,
|
|
)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"sections_created": len(sections),
|
|
"sections": sections,
|
|
})
|
|
}
|
|
|
|
// GenerateSingleSection handles POST /projects/:id/tech-file/:section/generate
|
|
// Generates or regenerates a single tech file section using LLM.
|
|
func (h *IACEHandler) GenerateSingleSection(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
sectionType := c.Param("section")
|
|
if sectionType == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"})
|
|
return
|
|
}
|
|
|
|
// Generate content via LLM
|
|
content, err := h.techFileGen.GenerateSection(c.Request.Context(), projectID, sectionType)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("generation failed: %v", err)})
|
|
return
|
|
}
|
|
|
|
// Find existing section and update, or create new
|
|
sections, _ := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
var sectionID uuid.UUID
|
|
found := false
|
|
for _, s := range sections {
|
|
if s.SectionType == sectionType {
|
|
sectionID = s.ID
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if found {
|
|
if err := h.store.UpdateTechFileSection(c.Request.Context(), sectionID, content); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
} else {
|
|
title := sectionType // fallback
|
|
sectionTitles := map[string]string{
|
|
"general_description": "General Description of the Machinery",
|
|
"risk_assessment_report": "Risk Assessment Report",
|
|
"hazard_log_combined": "Combined Hazard Log",
|
|
"essential_requirements": "Essential Health and Safety Requirements",
|
|
"design_specifications": "Design Specifications and Drawings",
|
|
"test_reports": "Test Reports and Verification Results",
|
|
"standards_applied": "Applied Harmonised Standards",
|
|
"declaration_of_conformity": "EU Declaration of Conformity",
|
|
"component_list": "Component List",
|
|
"classification_report": "Regulatory Classification Report",
|
|
"mitigation_report": "Mitigation Measures Report",
|
|
"verification_report": "Verification Report",
|
|
"evidence_index": "Evidence Index",
|
|
"instructions_for_use": "Instructions for Use",
|
|
"monitoring_plan": "Post-Market Monitoring Plan",
|
|
"ai_intended_purpose": "AI System Intended Purpose",
|
|
"ai_model_description": "AI Model Description and Training Data",
|
|
"ai_risk_management": "AI Risk Management System",
|
|
"ai_human_oversight": "AI Human Oversight Measures",
|
|
}
|
|
if t, ok := sectionTitles[sectionType]; ok {
|
|
title = t
|
|
}
|
|
|
|
_, err := h.store.CreateTechFileSection(c.Request.Context(), projectID, sectionType, title, content)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "tech_file_section", projectID,
|
|
iace.AuditActionCreate, userID.String(), nil, nil,
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "section generated",
|
|
"section_type": sectionType,
|
|
"content": content,
|
|
})
|
|
}
|
|
|
|
// ListTechFileSections handles GET /projects/:id/tech-file
|
|
// Lists all technical file sections for a project.
|
|
func (h *IACEHandler) ListTechFileSections(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
if sections == nil {
|
|
sections = []iace.TechFileSection{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"sections": sections,
|
|
"total": len(sections),
|
|
})
|
|
}
|
|
|
|
// UpdateTechFileSection handles PUT /projects/:id/tech-file/:section
|
|
// Updates the content of a technical file section (identified by section_type).
|
|
func (h *IACEHandler) UpdateTechFileSection(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
sectionType := c.Param("section")
|
|
if sectionType == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Content string `json:"content" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Find the section by project ID and section type
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var sectionID uuid.UUID
|
|
found := false
|
|
for _, s := range sections {
|
|
if s.SectionType == sectionType {
|
|
sectionID = s.ID
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)})
|
|
return
|
|
}
|
|
|
|
if err := h.store.UpdateTechFileSection(c.Request.Context(), sectionID, req.Content); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
userID := rbac.GetUserID(c)
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "tech_file_section", sectionID,
|
|
iace.AuditActionUpdate, userID.String(), nil, nil,
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "tech file section updated"})
|
|
}
|
|
|
|
// ApproveTechFileSection handles POST /projects/:id/tech-file/:section/approve
|
|
// Marks a technical file section as approved.
|
|
func (h *IACEHandler) ApproveTechFileSection(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
sectionType := c.Param("section")
|
|
if sectionType == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "section type required"})
|
|
return
|
|
}
|
|
|
|
// Find the section by project ID and section type
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var sectionID uuid.UUID
|
|
found := false
|
|
for _, s := range sections {
|
|
if s.SectionType == sectionType {
|
|
sectionID = s.ID
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("tech file section '%s' not found", sectionType)})
|
|
return
|
|
}
|
|
|
|
userID := rbac.GetUserID(c)
|
|
|
|
if err := h.store.ApproveTechFileSection(c.Request.Context(), sectionID, userID.String()); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Audit trail
|
|
h.store.AddAuditEntry(
|
|
c.Request.Context(), projectID, "tech_file_section", sectionID,
|
|
iace.AuditActionApprove, userID.String(), nil, nil,
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "tech file section approved"})
|
|
}
|
|
|
|
// ExportTechFile handles GET /projects/:id/tech-file/export?format=pdf|xlsx|docx|md|json
|
|
// Exports all tech file sections in the requested format.
|
|
func (h *IACEHandler) ExportTechFile(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
sections, err := h.store.ListTechFileSections(c.Request.Context(), projectID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Load hazards, assessments, mitigations, classifications for export
|
|
hazards, _ := h.store.ListHazards(c.Request.Context(), projectID)
|
|
var allAssessments []iace.RiskAssessment
|
|
var allMitigations []iace.Mitigation
|
|
for _, hazard := range hazards {
|
|
assessments, _ := h.store.ListAssessments(c.Request.Context(), hazard.ID)
|
|
allAssessments = append(allAssessments, assessments...)
|
|
mitigations, _ := h.store.ListMitigations(c.Request.Context(), hazard.ID)
|
|
allMitigations = append(allMitigations, mitigations...)
|
|
}
|
|
classifications, _ := h.store.GetClassifications(c.Request.Context(), projectID)
|
|
|
|
format := c.DefaultQuery("format", "json")
|
|
safeName := strings.ReplaceAll(project.MachineName, " ", "_")
|
|
|
|
switch format {
|
|
case "pdf":
|
|
data, err := h.exporter.ExportPDF(project, sections, hazards, allAssessments, allMitigations, classifications)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("PDF export failed: %v", err)})
|
|
return
|
|
}
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.pdf"`, safeName))
|
|
c.Data(http.StatusOK, "application/pdf", data)
|
|
|
|
case "xlsx":
|
|
data, err := h.exporter.ExportExcel(project, sections, hazards, allAssessments, allMitigations)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel export failed: %v", err)})
|
|
return
|
|
}
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.xlsx"`, safeName))
|
|
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", data)
|
|
|
|
case "docx":
|
|
data, err := h.exporter.ExportDOCX(project, sections)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("DOCX export failed: %v", err)})
|
|
return
|
|
}
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.docx"`, safeName))
|
|
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.wordprocessingml.document", data)
|
|
|
|
case "md":
|
|
data, err := h.exporter.ExportMarkdown(project, sections)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Markdown export failed: %v", err)})
|
|
return
|
|
}
|
|
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="CE-Akte-%s.md"`, safeName))
|
|
c.Data(http.StatusOK, "text/markdown", data)
|
|
|
|
default:
|
|
// JSON export (original behavior)
|
|
allApproved := true
|
|
for _, s := range sections {
|
|
if s.Status != iace.TechFileSectionStatusApproved {
|
|
allApproved = false
|
|
break
|
|
}
|
|
}
|
|
riskSummary, _ := h.store.GetRiskSummary(c.Request.Context(), projectID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"project": project,
|
|
"sections": sections,
|
|
"classifications": classifications,
|
|
"risk_summary": riskSummary,
|
|
"all_approved": allApproved,
|
|
"export_format": "json",
|
|
})
|
|
}
|
|
}
|