feat(iace): VDA-Format FMEA Excel Export
Build + Deploy / build-admin-compliance (push) Successful in 1m48s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 44s
Build + Deploy / build-developer-portal (push) Successful in 11s
Build + Deploy / build-tts (push) Successful in 11s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 13s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m36s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 41s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 14s
Build + Deploy / trigger-orca (push) Successful in 2m15s
Build + Deploy / build-admin-compliance (push) Successful in 1m48s
Build + Deploy / build-backend-compliance (push) Successful in 11s
Build + Deploy / build-ai-sdk (push) Successful in 44s
Build + Deploy / build-developer-portal (push) Successful in 11s
Build + Deploy / build-tts (push) Successful in 11s
Build + Deploy / build-document-crawler (push) Successful in 12s
Build + Deploy / build-dsms-gateway (push) Successful in 10s
Build + Deploy / build-dsms-node (push) Successful in 13s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 14s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m36s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 41s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 14s
Build + Deploy / trigger-orca (push) Successful in 2m15s
- GET /projects/:id/fmea/export → xlsx im VDA-Formblatt - Spalten: Nr, Komponente, Typ, Fehlerart, Fehlerfolge, S, O, D, RPZ, AP, Massnahme - AP-Zellen farbig: H=rot, M=gelb, L=gruen - Dependency: github.com/xuri/excelize/v2 (BSD-3-Clause) - Frontend: "VDA Excel exportieren" Button auf FMEA-Seite Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user