94233b7c66
Three coupled pieces of work, all landing the same PoC:
1. Backend gap-review endpoint (Task #7)
- internal/api/handlers/iace_handler_gap_review.go:
POST /projects/:id/llm-gap-review
feeds Limits-Form + current hazards + current mitigations to
the configured LLM (Qwen / Claude / OpenAI via ProviderRegistry),
parses a JSON suggestion list, filter+stamps confidence, falls
back to a static checklist when LLM is unavailable.
- Adopt step is NOT in this endpoint by design — the user clicks
Adopt in the frontend which calls the existing CreateHazard /
CreateMitigation handlers so provenance flows through the normal
audit trail.
2. Frontend modal + button (Task #8)
- app/sdk/iace/[projectId]/hazards/_components/LLMGapReviewModal.tsx:
reusable modal that POSTs the gap-review endpoint, renders
suggestions with Adopt/Reject UX, shows confidence + norm refs,
source-stamp llm_gap_review vs fallback_static.
- hazards/page.tsx: indigo "KI-Gap-Review" button next to the
existing "Eigene Gefaehrdung" button + modal mount.
3. Tech-File sources appendix (Task #29 — Stufe 4)
- internal/iace/document_export_sources.go: new pdfSourcesAppendix
method appended to ExportPDF. Groups cited norms by license rule
(R1 OSHA/EU-Recht / R3 BreakPilot patterns / R3 DIN-EN-ISO
identifier-only) and emits the legally required statement that
pauschal Impressum-Hinweise nicht ausreichen.
- extractCitedNorms() scans hazard/mitigation text for EN/ISO/IEC/
DIN identifiers in a narrow grammar so prose isn't turned into
spurious citations.
Bonus refactor:
- internal/app/routes.go reached the 500-LOC hard cap when the new
llm-gap-review route was added. Extracted registerIACERoutes into
routes_iace.go (136 LOC). Same wiring, no behaviour change.
Three of the four Attribution-Renderer stages (1, 2, 4) now produce
real output. Stufe 3 ships as <SourceBadge> + <LicenseModuleBanner>
already (commits dfac940 + b9e3eea earlier in this branch).
The PoC is intentionally conservative: every LLM-Suggestion stays
unverbindlich until a human clicks Adopt, and Adopt goes through the
existing normal CreateHazard/CreateMitigation flow (not yet wired in
this commit — separate iteration). The endpoint, modal and provenance
chain are in place for the next iteration to wire Adopt → write path.
161 lines
4.5 KiB
Go
161 lines
4.5 KiB
Go
package iace
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
// ExportFormat represents a supported document export format
|
|
type ExportFormat string
|
|
|
|
const (
|
|
ExportFormatPDF ExportFormat = "pdf"
|
|
ExportFormatXLSX ExportFormat = "xlsx"
|
|
ExportFormatDOCX ExportFormat = "docx"
|
|
ExportFormatMD ExportFormat = "md"
|
|
ExportFormatJSON ExportFormat = "json"
|
|
)
|
|
|
|
// DocumentExporter handles exporting CE technical file data into various formats
|
|
type DocumentExporter struct{}
|
|
|
|
// NewDocumentExporter creates a new DocumentExporter instance
|
|
func NewDocumentExporter() *DocumentExporter {
|
|
return &DocumentExporter{}
|
|
}
|
|
|
|
// ============================================================================
|
|
// PDF Export
|
|
// ============================================================================
|
|
|
|
// ExportPDF generates a PDF document containing the full CE technical file
|
|
func (e *DocumentExporter) ExportPDF(
|
|
project *Project,
|
|
sections []TechFileSection,
|
|
hazards []Hazard,
|
|
assessments []RiskAssessment,
|
|
mitigations []Mitigation,
|
|
classifications []RegulatoryClassification,
|
|
) ([]byte, error) {
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project must not be nil")
|
|
}
|
|
|
|
pdf := gofpdf.New("P", "mm", "A4", "")
|
|
|
|
// --- Cover Page ---
|
|
pdf.AddPage()
|
|
e.pdfCoverPage(pdf, project)
|
|
|
|
// --- Methodology ("Erklaerteil") ---
|
|
pdf.AddPage()
|
|
e.pdfMethodologySection(pdf)
|
|
|
|
// --- Table of Contents ---
|
|
pdf.AddPage()
|
|
e.pdfTableOfContents(pdf, sections)
|
|
|
|
// --- Sections ---
|
|
for _, section := range sections {
|
|
pdf.AddPage()
|
|
e.pdfSection(pdf, section)
|
|
}
|
|
|
|
// --- Hazard Log ---
|
|
pdf.AddPage()
|
|
e.pdfHazardLog(pdf, hazards, assessments)
|
|
|
|
// --- Risk Matrix Summary ---
|
|
e.pdfRiskMatrixSummary(pdf, assessments)
|
|
|
|
// --- Mitigations Table ---
|
|
pdf.AddPage()
|
|
e.pdfMitigationsTable(pdf, mitigations)
|
|
|
|
// --- Regulatory Classifications ---
|
|
if len(classifications) > 0 {
|
|
pdf.AddPage()
|
|
e.pdfClassifications(pdf, classifications)
|
|
}
|
|
|
|
// --- Quellen & Lizenzen (Stufe 4 Attribution-Renderer, Task #29) ---
|
|
pdf.AddPage()
|
|
e.pdfSourcesAppendix(pdf, hazards, mitigations)
|
|
|
|
// --- Footer on every page ---
|
|
pdf.SetFooterFunc(func() {
|
|
pdf.SetY(-15)
|
|
pdf.SetFont("Helvetica", "I", 8)
|
|
pdf.SetTextColor(128, 128, 128)
|
|
pdf.CellFormat(0, 5,
|
|
fmt.Sprintf("CE-Akte %s | Generiert am %s | BreakPilot AI Compliance SDK",
|
|
project.MachineName, time.Now().Format("02.01.2006 15:04")),
|
|
"", 0, "C", false, 0, "")
|
|
})
|
|
|
|
var buf bytes.Buffer
|
|
if err := pdf.Output(&buf); err != nil {
|
|
return nil, fmt.Errorf("failed to generate PDF: %w", err)
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// Markdown Export
|
|
// ============================================================================
|
|
|
|
// ExportMarkdown generates a Markdown document of the CE technical file sections
|
|
func (e *DocumentExporter) ExportMarkdown(
|
|
project *Project,
|
|
sections []TechFileSection,
|
|
) ([]byte, error) {
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project must not be nil")
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
buf.WriteString(fmt.Sprintf("# CE-Akte: %s\n\n", project.MachineName))
|
|
|
|
buf.WriteString("| Eigenschaft | Wert |\n")
|
|
buf.WriteString("|-------------|------|\n")
|
|
buf.WriteString(fmt.Sprintf("| Hersteller | %s |\n", project.Manufacturer))
|
|
buf.WriteString(fmt.Sprintf("| Maschinentyp | %s |\n", project.MachineType))
|
|
if project.CEMarkingTarget != "" {
|
|
buf.WriteString(fmt.Sprintf("| CE-Kennzeichnungsziel | %s |\n", project.CEMarkingTarget))
|
|
}
|
|
buf.WriteString(fmt.Sprintf("| Status | %s |\n", project.Status))
|
|
buf.WriteString(fmt.Sprintf("| Datum | %s |\n", time.Now().Format("02.01.2006")))
|
|
buf.WriteString("\n")
|
|
|
|
if project.Description != "" {
|
|
buf.WriteString(fmt.Sprintf("> %s\n\n", project.Description))
|
|
}
|
|
|
|
buf.WriteString("---\n\n")
|
|
buf.WriteString(fmt.Sprintf("## %s\n\n", RiskAssessmentMethodologySectionTitle))
|
|
buf.WriteString(RiskAssessmentMethodologyDE)
|
|
buf.WriteString("\n\n---\n\n")
|
|
|
|
for _, section := range sections {
|
|
buf.WriteString(fmt.Sprintf("## %s\n\n", section.Title))
|
|
buf.WriteString(fmt.Sprintf("*Typ: %s | Status: %s | Version: %d*\n\n",
|
|
section.SectionType, string(section.Status), section.Version))
|
|
if section.Content != "" {
|
|
buf.WriteString(section.Content)
|
|
buf.WriteString("\n\n")
|
|
} else {
|
|
buf.WriteString("*(Kein Inhalt vorhanden)*\n\n")
|
|
}
|
|
}
|
|
|
|
buf.WriteString("---\n\n")
|
|
buf.WriteString(fmt.Sprintf("*Generiert am %s mit BreakPilot AI Compliance SDK*\n",
|
|
time.Now().Format("02.01.2006 15:04")))
|
|
|
|
return buf.Bytes(), nil
|
|
}
|