feat(iace): Klaerungen Phase 2 — Sidebar-Counter + CSV-Export + Hazard-Banner

Three pieces complete the Klaerungen UX:

1. Sidebar-Counter: layout.tsx polls /clarifications and shows a
   colored open-count badge on the "Klaerungen" nav item. Refreshes
   whenever the user changes route.

2. CSV-Export: new backend endpoint
   GET /sdk/v1/iace/projects/:id/clarifications.csv produces a UTF-8-
   BOM-prefixed semicolon-separated CSV (Excel-friendly) with ID,
   Quelle, Kategorie, Frage, Status, Antwort, Begruendung, Bearbeiter,
   answered_at, anzahl Gefaehrdungen, Gefaehrdungs-Namen, Norm-Refs.
   Frontend Klaerungen-Seite bekommt einen "CSV-Export"-Button.

3. Hazard-Banner statt Fragentext im Benchmark-Detail: the previous
   bulleted clarification list was duplicated across 48 hazards for a
   single FANUC question. Phase 2 replaces it with a compact status
   badge — "N offene Klaerung(en) — Klaerungen-Seite oeffnen" (orange)
   or "Alle N Klaerungen beantwortet" (green) with a direct link.

Backend cleanup: iace_handler_init.go no longer appends the "Mit
Anlagenbauer zu klaeren" block to Hazard.Description. The description
stays focused on the scenario; clarifications live in the dedicated
endpoint and answers persist across re-inits via project.metadata.
The aggregated "Referenzierte Normen" line on the hazard is kept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-17 01:25:36 +02:00
parent 525038359a
commit f19a75d83d
6 changed files with 205 additions and 62 deletions
@@ -1,9 +1,12 @@
package handlers
import (
"encoding/csv"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/iace"
@@ -216,3 +219,64 @@ func (h *IACEHandler) AnswerClarification(c *gin.Context) {
"answer": answers[cid],
})
}
// ExportClarificationsCSV handles GET /projects/:id/clarifications.csv.
// Returns the aggregated clarifications as a CSV for handover to the
// Anlagenbauer — one row per question with all referenced hazards and
// the current answer state.
func (h *IACEHandler) ExportClarificationsCSV(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
}
hazards, _ := h.store.ListHazards(ctx, projectID)
answers, _ := readClarificationAnswers(project.Metadata)
narrative := extractNarrativeFromMetadata(project.Metadata)
hazardToPatterns := h.reconstructHazardPatterns(narrative, project.MachineType, hazards)
manufHits := iace.LookupManufacturerFeaturesInText(narrative)
clarifications := iace.BuildProjectClarifications(hazards, hazardToPatterns, manufHits, answers)
sort.Slice(clarifications, func(i, j int) bool {
if clarifications[i].Status != clarifications[j].Status {
return clarifications[i].Status == "open"
}
return clarifications[i].Source < clarifications[j].Source
})
filename := fmt.Sprintf("klaerungen_%s_%s.csv", project.MachineName, time.Now().Format("2006-01-02"))
filename = strings.ReplaceAll(filename, " ", "_")
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="`+filename+`"`)
// Excel-Erkennung: UTF-8 BOM voranstellen
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
w := csv.NewWriter(c.Writer)
w.Comma = ';'
_ = w.Write([]string{
"ID", "Quelle", "Kategorie", "Frage", "Status", "Antwort", "Begruendung",
"Bearbeiter", "Beantwortet_am", "Anzahl_Gefaehrdungen", "Gefaehrdungen", "Norm_Referenzen",
})
for _, cl := range clarifications {
_ = w.Write([]string{
cl.ID,
cl.Source,
cl.Category,
cl.Question,
cl.Status,
cl.Answer,
cl.Reasoning,
cl.AnsweredBy,
cl.AnsweredAt,
fmt.Sprintf("%d", len(cl.AffectedHazardIDs)),
strings.Join(cl.AffectedHazardNames, " | "),
strings.Join(cl.NormReferences, " | "),
})
}
w.Flush()
}