From dc07580adcb0cde77a51c625651ab3f55775ed2e Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Wed, 13 May 2026 23:54:54 +0300 Subject: [PATCH] Add AER decode, event counter, and sparkline to component detail modal - decodeAERStatus: parses aer_status hex from kernel error strings and maps PCIe AER register bits to human-readable names with correctable/ uncorrectable classification (e.g. "Receiver Error, Replay Timer Timeout (correctable)") - renderSparkline: 100px inline SVG showing non-OK events over time, bars positioned proportionally to timestamp; evenly spaced when timestamps coincide - renderComponentDetail: shows event count badge and sparkline in the component header row; decoded AER line appears below the raw error summary Co-Authored-By: Claude Sonnet 4.6 --- audit/internal/webui/pages.go | 139 +++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 2 deletions(-) diff --git a/audit/internal/webui/pages.go b/audit/internal/webui/pages.go index a759a74..470d4ec 100644 --- a/audit/internal/webui/pages.go +++ b/audit/internal/webui/pages.go @@ -5,7 +5,9 @@ import ( "fmt" "html" "path/filepath" + "regexp" "sort" + "strconv" "strings" "bee/audit/internal/app" @@ -1031,6 +1033,114 @@ func rowIssueHTML(issue string) string { return html.EscapeString(issue) } +var aerStatusRe = regexp.MustCompile(`aer_status:\s*0x([0-9a-fA-F]{1,8})`) + +// decodeAERStatus parses an AER status hex value from a kernel error detail string +// and returns a human-readable list of set bit names with correctable/uncorrectable label, +// or "" if no AER status is found. +func decodeAERStatus(detail string) string { + m := aerStatusRe.FindStringSubmatch(detail) + if m == nil { + return "" + } + v64, err := strconv.ParseUint(m[1], 16, 32) + if err != nil { + return "" + } + val := uint32(v64) + + type bitDef struct { + bit uint32 + name string + } + corrBits := []bitDef{ + {0, "Receiver Error"}, {6, "Replay Timer Timeout"}, {7, "Advisory Non-Fatal"}, + {8, "Corrected Internal Error"}, {9, "Header Log Overflow"}, + {13, "Replay Num Rollover"}, {14, "Bad DLLP"}, {15, "Bad TLP"}, + } + uncorrBits := []bitDef{ + {4, "Data Link Protocol Error"}, {5, "Surprise Down Error"}, + {12, "Poisoned TLP Received"}, {13, "Flow Control Protocol Error"}, + {14, "Completion Timeout"}, {15, "Completer Abort"}, {16, "Unexpected Completion"}, + {17, "Receiver Overflow"}, {18, "Malformed TLP"}, {19, "ECRC Error"}, + {20, "Unsupported Request Error"}, {21, "ACS Violation"}, {22, "Uncorrectable Internal Error"}, + } + var corrNames, uncorrNames []string + for _, b := range corrBits { + if val&(1<= len(uncorrNames) && len(corrNames) > 0 { + return strings.Join(corrNames, ", ") + " (correctable)" + } + if len(uncorrNames) > 0 { + return strings.Join(uncorrNames, ", ") + " (uncorrectable)" + } + return fmt.Sprintf("unknown bits: 0x%08x", val) +} + +// renderSparkline returns a small inline SVG showing non-OK events over time. +// Events are positioned proportionally along the time axis; if all share the same +// timestamp they are spaced evenly. Width is always 100px. +func renderSparkline(history []app.ComponentStatusEntry) string { + const ( + svgW = 100 + svgH = 20 + barW = 3 + barH = 14 + ) + var events []app.ComponentStatusEntry + for _, e := range history { + if e.Status != "OK" { + events = append(events, e) + } + } + if len(events) == 0 { + return "" + } + n := len(events) + barColor := func(status string) string { + if status == "Critical" { + return "#c0392b" + } + return "#d97706" + } + yTop := (svgH - barH) / 2 + + var bars strings.Builder + if n == 1 { + x := (svgW - barW) / 2 + fmt.Fprintf(&bars, ``, + x, yTop, barW, barH, barColor(events[0].Status)) + } else { + minT := events[0].At + maxT := events[n-1].At + dur := maxT.Sub(minT).Seconds() + for i, e := range events { + var x int + if dur <= 0 { + step := svgW / n + x = i*step + (step-barW)/2 + } else { + frac := e.At.Sub(minT).Seconds() / dur + x = int(frac * float64(svgW-barW)) + } + fmt.Fprintf(&bars, ``, + x, yTop, barW, barH, barColor(e.Status)) + } + } + return fmt.Sprintf( + ``+ + `%s`, + svgW, svgH, svgW, svgH, bars.String()) +} + // renderComponentDetail renders a modal content fragment for one component type. // Called by handleAPIComponentDetail and displayed inside #component-detail-dialog. func renderComponentDetail(title string, records []app.ComponentStatusRecord) string { @@ -1053,16 +1163,41 @@ func renderComponentDetail(title string, records []app.ComponentStatusRecord) st for _, rec := range records { letter, cls := chipLetterClass(rec.Status) + + // Count non-OK events across the full history for the badge + sparkline. + warnCount := 0 + for _, e := range rec.History { + if e.Status != "OK" { + warnCount++ + } + } + fmt.Fprintf(&b, `
`) - fmt.Fprintf(&b, `
`) + fmt.Fprintf(&b, `
`) fmt.Fprintf(&b, `%s`, cls, letter) fmt.Fprintf(&b, `%s`, html.EscapeString(rec.ComponentKey)) if !rec.LastCheckedAt.IsZero() { fmt.Fprintf(&b, `checked %s`, rec.LastCheckedAt.Format("2006-01-02 15:04:05")) } + if warnCount > 0 { + noun := "events" + if warnCount == 1 { + noun = "event" + } + fmt.Fprintf(&b, + `%d %s`, + warnCount, noun) + b.WriteString(renderSparkline(rec.History)) + } b.WriteString(`
`) + if rec.ErrorSummary != "" { - fmt.Fprintf(&b, `
%s
`, html.EscapeString(rec.ErrorSummary)) + fmt.Fprintf(&b, `
%s
`, html.EscapeString(rec.ErrorSummary)) + if decoded := decodeAERStatus(rec.ErrorSummary); decoded != "" { + fmt.Fprintf(&b, + `
AER: %s
`, + html.EscapeString(decoded)) + } } // History table — newest first, cap at 20 entries.