diff --git a/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx index 6e3daac..fb4c28d 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/fmea/page.tsx @@ -46,6 +46,20 @@ export default function FMEAPage() {

+ {/* Export Button */} +
+ + + + + VDA Excel exportieren + +
+ {/* Stats */}
diff --git a/ai-compliance-sdk/go.mod b/ai-compliance-sdk/go.mod index 35dd3a8..fa55ab4 100644 --- a/ai-compliance-sdk/go.mod +++ b/ai-compliance-sdk/go.mod @@ -10,7 +10,7 @@ require ( github.com/jackc/pgx/v5 v5.5.3 github.com/joho/godotenv v1.5.1 github.com/jung-kurt/gofpdf v1.16.2 - github.com/xuri/excelize/v2 v2.9.1 + github.com/xuri/excelize/v2 v2.10.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -35,19 +35,19 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/richardlehane/mscfb v1.0.4 // indirect - github.com/richardlehane/msoleps v1.0.4 // indirect + github.com/richardlehane/mscfb v1.0.6 // indirect + github.com/richardlehane/msoleps v1.0.6 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/tiendc/go-deepcopy v1.7.1 // indirect + github.com/tiendc/go-deepcopy v1.7.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect github.com/xuri/efp v0.0.1 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect golang.org/x/arch v0.18.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/ai-compliance-sdk/go.sum b/ai-compliance-sdk/go.sum index 44211a1..4d190d9 100644 --- a/ai-compliance-sdk/go.sum +++ b/ai-compliance-sdk/go.sum @@ -76,9 +76,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8= +github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= +github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= @@ -95,6 +99,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= +github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -103,25 +109,37 @@ github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw= github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s= +github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0= +github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_fmea.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_fmea.go new file mode 100644 index 0000000..22b4b0b --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_fmea.go @@ -0,0 +1,81 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// ExportFMEA handles GET /projects/:id/fmea/export +// Returns an xlsx file in VDA FMEA format. +func (h *IACEHandler) ExportFMEA(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + ctx := c.Request.Context() + project, err := h.store.GetProject(ctx, projectID) + if err != nil || project == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + // Load components + components, _ := h.store.ListComponents(ctx, projectID) + + // Load all failure modes + allFMs := iace.GetFailureModeLibrary() + + // Build FMEA rows: each component × matching FMs + var rows []iace.FMEAExportRow + for _, comp := range components { + compType := string(comp.ComponentType) + var compFMs []iace.FailureModeEntry + for _, fm := range allFMs { + if fm.ComponentType == compType { + compFMs = append(compFMs, fm) + } + } + if len(compFMs) == 0 { + // Fallback: mechanical FMs + for _, fm := range allFMs { + if fm.ComponentType == "mechanical" && len(compFMs) < 3 { + compFMs = append(compFMs, fm) + } + } + } + + for _, fm := range compFMs { + s, o, d := fm.DefaultSeverity, fm.DefaultOccurrence, fm.DefaultDetection + rows = append(rows, iace.FMEAExportRow{ + ComponentName: comp.Name, + ComponentType: compType, + FailureMode: fm.NameDE, + FailureEffect: fm.Effect, + FailureCause: fm.DetectionHint, + Severity: s, + Occurrence: o, + Detection: d, + RPZ: s * o * d, + AP: iace.CalculateAP(s, o, d), + Measure: "", + DetectionHint: fm.DetectionHint, + }) + } + } + + xlsxBytes, err := iace.GenerateFMEAExcel(project.MachineName, rows) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Excel generation failed: %v", err)}) + return + } + + filename := fmt.Sprintf("FMEA-%s.xlsx", project.MachineName) + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", xlsxBytes) +} diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go index ba6c83b..931f4d7 100644 --- a/ai-compliance-sdk/internal/app/routes.go +++ b/ai-compliance-sdk/internal/app/routes.go @@ -393,6 +393,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { iaceRoutes.POST("/projects/:id/match-patterns", h.MatchPatterns) iaceRoutes.POST("/projects/:id/parse-narrative", h.ParseNarrative) iaceRoutes.POST("/projects/:id/delta-analysis", h.DeltaAnalysis) + iaceRoutes.GET("/projects/:id/fmea/export", h.ExportFMEA) iaceRoutes.POST("/projects/:id/apply-patterns", h.ApplyPatternResults) iaceRoutes.POST("/projects/:id/hazards/:hid/suggest-measures", h.SuggestMeasuresForHazard) iaceRoutes.POST("/projects/:id/mitigations/:mid/suggest-evidence", h.SuggestEvidenceForMitigation) diff --git a/ai-compliance-sdk/internal/iace/fmea_export.go b/ai-compliance-sdk/internal/iace/fmea_export.go new file mode 100644 index 0000000..dc7c8e8 --- /dev/null +++ b/ai-compliance-sdk/internal/iace/fmea_export.go @@ -0,0 +1,134 @@ +package iace + +import ( + "fmt" + + "github.com/xuri/excelize/v2" +) + +// FMEAExportRow represents one row in the VDA FMEA worksheet. +type FMEAExportRow struct { + ComponentName string `json:"component_name"` + ComponentType string `json:"component_type"` + FailureMode string `json:"failure_mode"` + FailureEffect string `json:"failure_effect"` + FailureCause string `json:"failure_cause"` + Severity int `json:"severity"` + Occurrence int `json:"occurrence"` + Detection int `json:"detection"` + RPZ int `json:"rpz"` + AP string `json:"ap"` + Measure string `json:"measure"` + DetectionHint string `json:"detection_hint"` +} + +// GenerateFMEAExcel creates a VDA-format FMEA worksheet as xlsx bytes. +func GenerateFMEAExcel(projectName string, rows []FMEAExportRow) ([]byte, error) { + f := excelize.NewFile() + sheet := "FMEA-Worksheet" + f.SetSheetName("Sheet1", sheet) + + // Column widths + widths := map[string]float64{ + "A": 6, "B": 25, "C": 14, "D": 28, "E": 30, "F": 28, + "G": 6, "H": 6, "I": 6, "J": 8, "K": 6, "L": 30, "M": 25, + } + for col, w := range widths { + _ = f.SetColWidth(sheet, col, col, w) + } + + // Header style + headerStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Size: 10, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center", WrapText: true}, + Border: []excelize.Border{ + {Type: "left", Color: "000000", Style: 1}, {Type: "right", Color: "000000", Style: 1}, + {Type: "top", Color: "000000", Style: 1}, {Type: "bottom", Color: "000000", Style: 1}, + }, + }) + + // Title row + f.SetCellValue(sheet, "A1", fmt.Sprintf("FMEA-Worksheet — %s", projectName)) + titleStyle, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Size: 14}, + }) + f.SetCellStyle(sheet, "A1", "A1", titleStyle) + f.MergeCell(sheet, "A1", "M1") + + // Sub-header + f.SetCellValue(sheet, "A2", "AIAG-VDA FMEA Format | AP = Action Priority (H/M/L)") + f.MergeCell(sheet, "A2", "M2") + + // Column headers (row 4) + headers := []string{"Nr.", "Komponente", "Typ", "Fehlerart", "Fehlerfolge", "Fehlerursache", + "S", "O", "D", "RPZ", "AP", "Empfohlene Massnahme", "Erkennung"} + for i, h := range headers { + cell := fmt.Sprintf("%s4", string(rune('A'+i))) + f.SetCellValue(sheet, cell, h) + f.SetCellStyle(sheet, cell, cell, headerStyle) + } + + // Data rows + dataStyle, _ := f.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{WrapText: true, Vertical: "top"}, + Border: []excelize.Border{ + {Type: "left", Color: "D0D0D0", Style: 1}, {Type: "right", Color: "D0D0D0", Style: 1}, + {Type: "bottom", Color: "D0D0D0", Style: 1}, + }, + }) + apHigh, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FF0000"}}, + Alignment: &excelize.Alignment{Horizontal: "center"}, + }) + apMed, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FFD700"}}, + Alignment: &excelize.Alignment{Horizontal: "center"}, + }) + apLow, _ := f.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"00B050"}}, + Alignment: &excelize.Alignment{Horizontal: "center"}, + }) + + for i, row := range rows { + r := i + 5 // data starts at row 5 + f.SetCellValue(sheet, fmt.Sprintf("A%d", r), i+1) + f.SetCellValue(sheet, fmt.Sprintf("B%d", r), row.ComponentName) + f.SetCellValue(sheet, fmt.Sprintf("C%d", r), row.ComponentType) + f.SetCellValue(sheet, fmt.Sprintf("D%d", r), row.FailureMode) + f.SetCellValue(sheet, fmt.Sprintf("E%d", r), row.FailureEffect) + f.SetCellValue(sheet, fmt.Sprintf("F%d", r), row.FailureCause) + f.SetCellValue(sheet, fmt.Sprintf("G%d", r), row.Severity) + f.SetCellValue(sheet, fmt.Sprintf("H%d", r), row.Occurrence) + f.SetCellValue(sheet, fmt.Sprintf("I%d", r), row.Detection) + f.SetCellValue(sheet, fmt.Sprintf("J%d", r), row.RPZ) + f.SetCellValue(sheet, fmt.Sprintf("K%d", r), row.AP) + f.SetCellValue(sheet, fmt.Sprintf("L%d", r), row.Measure) + f.SetCellValue(sheet, fmt.Sprintf("M%d", r), row.DetectionHint) + + // Style data cells + for c := 0; c < 13; c++ { + cell := fmt.Sprintf("%s%d", string(rune('A'+c)), r) + f.SetCellStyle(sheet, cell, cell, dataStyle) + } + // AP color + apCell := fmt.Sprintf("K%d", r) + switch row.AP { + case "H": + f.SetCellStyle(sheet, apCell, apCell, apHigh) + case "M": + f.SetCellStyle(sheet, apCell, apCell, apMed) + case "L": + f.SetCellStyle(sheet, apCell, apCell, apLow) + } + } + + buf, err := f.WriteToBuffer() + if err != nil { + return nil, fmt.Errorf("excel write: %w", err) + } + return buf.Bytes(), nil +}