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>
466 lines
16 KiB
Go
466 lines
16 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// ISO 12100 Endpoints
|
|
// ============================================================================
|
|
|
|
// ListLifecyclePhases handles GET /lifecycle-phases
|
|
// Returns the 12 machine lifecycle phases with DE/EN labels.
|
|
func (h *IACEHandler) ListLifecyclePhases(c *gin.Context) {
|
|
phases, err := h.store.ListLifecyclePhases(c.Request.Context())
|
|
if err != nil {
|
|
// Fallback: return hardcoded 25 phases if DB table not yet migrated
|
|
phases = []iace.LifecyclePhaseInfo{
|
|
{ID: "transport", LabelDE: "Transport", LabelEN: "Transport", Sort: 1},
|
|
{ID: "storage", LabelDE: "Lagerung", LabelEN: "Storage", Sort: 2},
|
|
{ID: "assembly", LabelDE: "Montage", LabelEN: "Assembly", Sort: 3},
|
|
{ID: "installation", LabelDE: "Installation", LabelEN: "Installation", Sort: 4},
|
|
{ID: "commissioning", LabelDE: "Inbetriebnahme", LabelEN: "Commissioning", Sort: 5},
|
|
{ID: "parameterization", LabelDE: "Parametrierung", LabelEN: "Parameterization", Sort: 6},
|
|
{ID: "setup", LabelDE: "Einrichten / Setup", LabelEN: "Setup", Sort: 7},
|
|
{ID: "normal_operation", LabelDE: "Normalbetrieb", LabelEN: "Normal Operation", Sort: 8},
|
|
{ID: "automatic_operation", LabelDE: "Automatikbetrieb", LabelEN: "Automatic Operation", Sort: 9},
|
|
{ID: "manual_operation", LabelDE: "Handbetrieb", LabelEN: "Manual Operation", Sort: 10},
|
|
{ID: "teach_mode", LabelDE: "Teach-Modus", LabelEN: "Teach Mode", Sort: 11},
|
|
{ID: "production_start", LabelDE: "Produktionsstart", LabelEN: "Production Start", Sort: 12},
|
|
{ID: "production_stop", LabelDE: "Produktionsstopp", LabelEN: "Production Stop", Sort: 13},
|
|
{ID: "process_monitoring", LabelDE: "Prozessueberwachung", LabelEN: "Process Monitoring", Sort: 14},
|
|
{ID: "cleaning", LabelDE: "Reinigung", LabelEN: "Cleaning", Sort: 15},
|
|
{ID: "maintenance", LabelDE: "Wartung", LabelEN: "Maintenance", Sort: 16},
|
|
{ID: "inspection", LabelDE: "Inspektion", LabelEN: "Inspection", Sort: 17},
|
|
{ID: "calibration", LabelDE: "Kalibrierung", LabelEN: "Calibration", Sort: 18},
|
|
{ID: "fault_clearing", LabelDE: "Stoerungsbeseitigung", LabelEN: "Fault Clearing", Sort: 19},
|
|
{ID: "repair", LabelDE: "Reparatur", LabelEN: "Repair", Sort: 20},
|
|
{ID: "changeover", LabelDE: "Umruestung", LabelEN: "Changeover", Sort: 21},
|
|
{ID: "software_update", LabelDE: "Software-Update", LabelEN: "Software Update", Sort: 22},
|
|
{ID: "remote_maintenance", LabelDE: "Fernwartung", LabelEN: "Remote Maintenance", Sort: 23},
|
|
{ID: "decommissioning", LabelDE: "Ausserbetriebnahme", LabelEN: "Decommissioning", Sort: 24},
|
|
{ID: "disposal", LabelDE: "Demontage / Entsorgung", LabelEN: "Dismantling / Disposal", Sort: 25},
|
|
}
|
|
}
|
|
|
|
if phases == nil {
|
|
phases = []iace.LifecyclePhaseInfo{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"lifecycle_phases": phases,
|
|
"total": len(phases),
|
|
})
|
|
}
|
|
|
|
// ListProtectiveMeasures handles GET /protective-measures-library
|
|
// Returns the protective measures library, optionally filtered by ?reduction_type and ?hazard_category.
|
|
func (h *IACEHandler) ListProtectiveMeasures(c *gin.Context) {
|
|
reductionType := c.Query("reduction_type")
|
|
hazardCategory := c.Query("hazard_category")
|
|
|
|
all := iace.GetProtectiveMeasureLibrary()
|
|
|
|
var filtered []iace.ProtectiveMeasureEntry
|
|
for _, entry := range all {
|
|
if reductionType != "" && entry.ReductionType != reductionType {
|
|
continue
|
|
}
|
|
if hazardCategory != "" && entry.HazardCategory != hazardCategory && entry.HazardCategory != "general" && entry.HazardCategory != "" {
|
|
continue
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
|
|
if filtered == nil {
|
|
filtered = []iace.ProtectiveMeasureEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"protective_measures": filtered,
|
|
"total": len(filtered),
|
|
})
|
|
}
|
|
|
|
// ValidateMitigationHierarchy handles POST /projects/:id/validate-mitigation-hierarchy
|
|
// Validates if the proposed mitigation type follows the 3-step hierarchy principle.
|
|
func (h *IACEHandler) ValidateMitigationHierarchy(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
var req iace.ValidateMitigationHierarchyRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get existing mitigations for the hazard
|
|
mitigations, err := h.store.ListMitigations(c.Request.Context(), req.HazardID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
_ = projectID // projectID used for authorization context
|
|
|
|
warnings := h.engine.ValidateProtectiveMeasureHierarchy(req.ReductionType, mitigations)
|
|
|
|
c.JSON(http.StatusOK, iace.ValidateMitigationHierarchyResponse{
|
|
Valid: len(warnings) == 0,
|
|
Warnings: warnings,
|
|
})
|
|
}
|
|
|
|
// ListRoles handles GET /roles
|
|
// Returns the 20 affected person roles reference data.
|
|
func (h *IACEHandler) ListRoles(c *gin.Context) {
|
|
roles, err := h.store.ListRoles(c.Request.Context())
|
|
if err != nil {
|
|
// Fallback: return hardcoded roles if DB table not yet migrated
|
|
roles = []iace.RoleInfo{
|
|
{ID: "operator", LabelDE: "Maschinenbediener", LabelEN: "Machine Operator", Sort: 1},
|
|
{ID: "setter", LabelDE: "Einrichter", LabelEN: "Setter", Sort: 2},
|
|
{ID: "maintenance_tech", LabelDE: "Wartungstechniker", LabelEN: "Maintenance Technician", Sort: 3},
|
|
{ID: "service_tech", LabelDE: "Servicetechniker", LabelEN: "Service Technician", Sort: 4},
|
|
{ID: "cleaning_staff", LabelDE: "Reinigungspersonal", LabelEN: "Cleaning Staff", Sort: 5},
|
|
{ID: "production_manager", LabelDE: "Produktionsleiter", LabelEN: "Production Manager", Sort: 6},
|
|
{ID: "safety_officer", LabelDE: "Sicherheitsbeauftragter", LabelEN: "Safety Officer", Sort: 7},
|
|
{ID: "electrician", LabelDE: "Elektriker", LabelEN: "Electrician", Sort: 8},
|
|
{ID: "software_engineer", LabelDE: "Softwareingenieur", LabelEN: "Software Engineer", Sort: 9},
|
|
{ID: "maintenance_manager", LabelDE: "Instandhaltungsleiter", LabelEN: "Maintenance Manager", Sort: 10},
|
|
{ID: "plant_operator", LabelDE: "Anlagenfahrer", LabelEN: "Plant Operator", Sort: 11},
|
|
{ID: "qa_inspector", LabelDE: "Qualitaetssicherung", LabelEN: "Quality Assurance", Sort: 12},
|
|
{ID: "logistics_staff", LabelDE: "Logistikpersonal", LabelEN: "Logistics Staff", Sort: 13},
|
|
{ID: "subcontractor", LabelDE: "Fremdfirma / Subunternehmer", LabelEN: "Subcontractor", Sort: 14},
|
|
{ID: "visitor", LabelDE: "Besucher", LabelEN: "Visitor", Sort: 15},
|
|
{ID: "auditor", LabelDE: "Auditor", LabelEN: "Auditor", Sort: 16},
|
|
{ID: "it_admin", LabelDE: "IT-Administrator", LabelEN: "IT Administrator", Sort: 17},
|
|
{ID: "remote_service", LabelDE: "Fernwartungsdienst", LabelEN: "Remote Service", Sort: 18},
|
|
{ID: "plant_owner", LabelDE: "Betreiber", LabelEN: "Plant Owner / Operator", Sort: 19},
|
|
{ID: "emergency_responder", LabelDE: "Notfallpersonal", LabelEN: "Emergency Responder", Sort: 20},
|
|
}
|
|
}
|
|
|
|
if roles == nil {
|
|
roles = []iace.RoleInfo{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"roles": roles,
|
|
"total": len(roles),
|
|
})
|
|
}
|
|
|
|
// ListEvidenceTypes handles GET /evidence-types
|
|
// Returns the 50 evidence/verification types reference data.
|
|
func (h *IACEHandler) ListEvidenceTypes(c *gin.Context) {
|
|
types, err := h.store.ListEvidenceTypes(c.Request.Context())
|
|
if err != nil {
|
|
// Fallback: return empty if not migrated
|
|
types = []iace.EvidenceTypeInfo{}
|
|
}
|
|
|
|
if types == nil {
|
|
types = []iace.EvidenceTypeInfo{}
|
|
}
|
|
|
|
category := c.Query("category")
|
|
if category != "" {
|
|
var filtered []iace.EvidenceTypeInfo
|
|
for _, t := range types {
|
|
if t.Category == category {
|
|
filtered = append(filtered, t)
|
|
}
|
|
}
|
|
if filtered == nil {
|
|
filtered = []iace.EvidenceTypeInfo{}
|
|
}
|
|
types = filtered
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"evidence_types": types,
|
|
"total": len(types),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Component Library & Energy Sources (Phase 1)
|
|
// ============================================================================
|
|
|
|
// ListComponentLibrary handles GET /component-library
|
|
// Returns the built-in component library with optional category filter.
|
|
func (h *IACEHandler) ListComponentLibrary(c *gin.Context) {
|
|
category := c.Query("category")
|
|
|
|
all := iace.GetComponentLibrary()
|
|
var filtered []iace.ComponentLibraryEntry
|
|
for _, entry := range all {
|
|
if category != "" && entry.Category != category {
|
|
continue
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
|
|
if filtered == nil {
|
|
filtered = []iace.ComponentLibraryEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"components": filtered,
|
|
"total": len(filtered),
|
|
})
|
|
}
|
|
|
|
// ListEnergySources handles GET /energy-sources
|
|
// Returns the built-in energy source library.
|
|
func (h *IACEHandler) ListEnergySources(c *gin.Context) {
|
|
sources := iace.GetEnergySources()
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"energy_sources": sources,
|
|
"total": len(sources),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tag Taxonomy (Phase 2)
|
|
// ============================================================================
|
|
|
|
// ListTags handles GET /tags
|
|
// Returns the tag taxonomy with optional domain filter.
|
|
func (h *IACEHandler) ListTags(c *gin.Context) {
|
|
domain := c.Query("domain")
|
|
|
|
all := iace.GetTagTaxonomy()
|
|
var filtered []iace.TagEntry
|
|
for _, entry := range all {
|
|
if domain != "" && entry.Domain != domain {
|
|
continue
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
|
|
if filtered == nil {
|
|
filtered = []iace.TagEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"tags": filtered,
|
|
"total": len(filtered),
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Hazard Patterns & Pattern Engine (Phase 3+4)
|
|
// ============================================================================
|
|
|
|
// ListHazardPatterns handles GET /hazard-patterns
|
|
// Returns all built-in hazard patterns.
|
|
func (h *IACEHandler) ListHazardPatterns(c *gin.Context) {
|
|
patterns := iace.GetBuiltinHazardPatterns()
|
|
patterns = append(patterns, iace.GetExtendedHazardPatterns()...)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"patterns": patterns,
|
|
"total": len(patterns),
|
|
})
|
|
}
|
|
|
|
// MatchPatterns handles POST /projects/:id/match-patterns
|
|
// Runs the pattern engine against the project's components and energy sources.
|
|
func (h *IACEHandler) MatchPatterns(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
// Verify project exists
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil || project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
var input iace.MatchInput
|
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
engine := iace.NewPatternEngine()
|
|
result := engine.Match(input)
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// ApplyPatternResults handles POST /projects/:id/apply-patterns
|
|
// Accepts matched patterns and creates concrete hazards, mitigations, and
|
|
// verification plans in the project.
|
|
func (h *IACEHandler) ApplyPatternResults(c *gin.Context) {
|
|
projectID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
return
|
|
}
|
|
|
|
tenantID, err := getTenantID(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
project, err := h.store.GetProject(c.Request.Context(), projectID)
|
|
if err != nil || project == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "project not found"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
AcceptedHazards []iace.CreateHazardRequest `json:"accepted_hazards"`
|
|
AcceptedMeasures []iace.CreateMitigationRequest `json:"accepted_measures"`
|
|
AcceptedEvidence []iace.CreateVerificationPlanRequest `json:"accepted_evidence"`
|
|
SourcePatternIDs []string `json:"source_pattern_ids"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
var createdHazards int
|
|
var createdMeasures int
|
|
var createdEvidence int
|
|
|
|
// Create hazards
|
|
for _, hazardReq := range req.AcceptedHazards {
|
|
hazardReq.ProjectID = projectID
|
|
_, err := h.store.CreateHazard(ctx, hazardReq)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
createdHazards++
|
|
}
|
|
|
|
// Create mitigations
|
|
for _, mitigReq := range req.AcceptedMeasures {
|
|
_, err := h.store.CreateMitigation(ctx, mitigReq)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
createdMeasures++
|
|
}
|
|
|
|
// Create verification plans
|
|
for _, evidReq := range req.AcceptedEvidence {
|
|
evidReq.ProjectID = projectID
|
|
_, err := h.store.CreateVerificationPlan(ctx, evidReq)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
createdEvidence++
|
|
}
|
|
|
|
// Audit trail
|
|
h.store.AddAuditEntry(ctx, projectID, "pattern_matching", projectID,
|
|
iace.AuditActionCreate, tenantID.String(),
|
|
nil,
|
|
mustMarshalJSON(map[string]interface{}{
|
|
"source_patterns": req.SourcePatternIDs,
|
|
"created_hazards": createdHazards,
|
|
"created_measures": createdMeasures,
|
|
"created_evidence": createdEvidence,
|
|
}),
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"created_hazards": createdHazards,
|
|
"created_measures": createdMeasures,
|
|
"created_evidence": createdEvidence,
|
|
"message": "Pattern results applied successfully",
|
|
})
|
|
}
|
|
|
|
// SuggestMeasuresForHazard handles POST /projects/:id/hazards/:hid/suggest-measures
|
|
// Suggests measures for a specific hazard based on its tags and category.
|
|
func (h *IACEHandler) SuggestMeasuresForHazard(c *gin.Context) {
|
|
hazardID, err := uuid.Parse(c.Param("hid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"})
|
|
return
|
|
}
|
|
|
|
hazard, err := h.store.GetHazard(c.Request.Context(), hazardID)
|
|
if err != nil || hazard == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"})
|
|
return
|
|
}
|
|
|
|
// Find measures matching the hazard category
|
|
all := iace.GetProtectiveMeasureLibrary()
|
|
var suggested []iace.ProtectiveMeasureEntry
|
|
for _, m := range all {
|
|
if m.HazardCategory == hazard.Category || m.HazardCategory == "general" {
|
|
suggested = append(suggested, m)
|
|
}
|
|
}
|
|
|
|
if suggested == nil {
|
|
suggested = []iace.ProtectiveMeasureEntry{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"hazard_id": hazardID.String(),
|
|
"hazard_category": hazard.Category,
|
|
"suggested_measures": suggested,
|
|
"total": len(suggested),
|
|
})
|
|
}
|
|
|
|
// SuggestEvidenceForMitigation handles POST /projects/:id/mitigations/:mid/suggest-evidence
|
|
// Suggests evidence types for a specific mitigation.
|
|
func (h *IACEHandler) SuggestEvidenceForMitigation(c *gin.Context) {
|
|
mitigationID, err := uuid.Parse(c.Param("mid"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"})
|
|
return
|
|
}
|
|
|
|
mitigation, err := h.store.GetMitigation(c.Request.Context(), mitigationID)
|
|
if err != nil || mitigation == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not found"})
|
|
return
|
|
}
|
|
|
|
// Map reduction type to relevant evidence tags
|
|
var relevantTags []string
|
|
switch mitigation.ReductionType {
|
|
case iace.ReductionTypeDesign:
|
|
relevantTags = []string{"design_evidence", "analysis_evidence"}
|
|
case iace.ReductionTypeProtective:
|
|
relevantTags = []string{"test_evidence", "inspection_evidence"}
|
|
case iace.ReductionTypeInformation:
|
|
relevantTags = []string{"training_evidence", "operational_evidence"}
|
|
}
|
|
|
|
resolver := iace.NewTagResolver()
|
|
suggested := resolver.FindEvidenceByTags(relevantTags)
|
|
|
|
if suggested == nil {
|
|
suggested = []iace.EvidenceTypeInfo{}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"mitigation_id": mitigationID.String(),
|
|
"reduction_type": string(mitigation.ReductionType),
|
|
"suggested_evidence": suggested,
|
|
"total": len(suggested),
|
|
})
|
|
}
|