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

- 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:
Benjamin Admin
2026-05-12 09:45:18 +02:00
parent 078f936449
commit 6ce5b4bf41
6 changed files with 257 additions and 9 deletions
@@ -46,6 +46,20 @@ export default function FMEAPage() {
</p>
</div>
{/* Export Button */}
<div className="flex justify-end">
<a
href={`/api/sdk/v1/iace/projects/${projectId}/fmea/export`}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 text-sm font-medium transition-colors"
download
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
VDA Excel exportieren
</a>
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-3">
<StatCard label="Gesamt" value={stats.total} color="gray" />
+9 -9
View File
@@ -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
)
+18
View File
@@ -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=
@@ -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)
}
+1
View File
@@ -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
}