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()
}
@@ -212,42 +212,13 @@ func (h *IACEHandler) InitializeProject(c *gin.Context) {
// Join all applicable lifecycles as comma-separated string
lifecycleStr := strings.Join(mp.ApplicableLifecycles, ",")
// Append pattern-defined clarification questions to the
// description so they're visible in the UI without DB-
// schema changes. The engine does NOT invent commentary;
// it only hangs the standard ISO/EN clarification points
// onto the hazard so the operator knows what to verify
// with the Anlagenbauer.
// Phase 2: clarification questions are no longer embedded
// in the hazard description they live in the dedicated
// /clarifications API and the UI loads them on demand.
// The hazard description stays clean and focused on the
// scenario itself. Only the aggregated norm-references
// block is appended below for an at-a-glance audit trail.
desc := mp.ScenarioDE
clarBuckets := mp.ClarificationQuestionsDE
// Manufacturer-specific clarifications: if the narrative
// mentions a known manufacturer (FANUC/KUKA/Siemens/...),
// append its feature-specific questions to the matching
// hazard categories. Markennennung ist nominative use
// (§ 23 MarkenG), Fakten ueber Safety-Features sind nicht
// urheberrechtlich geschuetzt.
for _, mf := range iace.LookupManufacturerFeaturesInText(narrativeText) {
applies := len(mf.AppliesToHazardCats) == 0
for _, hc := range mf.AppliesToHazardCats {
if hc == cat {
applies = true
break
}
}
if !applies {
continue
}
prefix := mf.Manufacturer + " (" + mf.FeatureName + "): "
for _, q := range mf.Clarifications {
clarBuckets = append(clarBuckets, prefix+q)
}
}
if len(clarBuckets) > 0 {
desc += "\n\nMit Anlagenbauer zu klaeren:"
for _, q := range clarBuckets {
desc += "\n- " + q
}
}
hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{
ProjectID: projectID,