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", }) } }