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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<<b.bit) != 0 {
|
||||
corrNames = append(corrNames, b.name)
|
||||
}
|
||||
}
|
||||
for _, b := range uncorrBits {
|
||||
if val&(1<<b.bit) != 0 {
|
||||
uncorrNames = append(uncorrNames, b.name)
|
||||
}
|
||||
}
|
||||
if len(corrNames) >= 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, `<rect x="%d" y="%d" width="%d" height="%d" fill="%s" rx="1"/>`,
|
||||
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, `<rect x="%d" y="%d" width="%d" height="%d" fill="%s" rx="1"/>`,
|
||||
x, yTop, barW, barH, barColor(e.Status))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
`<svg width="%d" height="%d" style="display:inline-block;vertical-align:middle;margin-left:6px;flex-shrink:0" xmlns="http://www.w3.org/2000/svg">`+
|
||||
`<rect x="0" y="0" width="%d" height="%d" fill="var(--surface-alt,#ebebeb)" rx="3"/>%s</svg>`,
|
||||
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, `<div style="margin-bottom:20px">`)
|
||||
fmt.Fprintf(&b, `<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">`)
|
||||
fmt.Fprintf(&b, `<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">`)
|
||||
fmt.Fprintf(&b, `<span class="chip %s">%s</span>`, cls, letter)
|
||||
fmt.Fprintf(&b, `<span style="font-weight:700;font-size:13px">%s</span>`, html.EscapeString(rec.ComponentKey))
|
||||
if !rec.LastCheckedAt.IsZero() {
|
||||
fmt.Fprintf(&b, `<span style="color:var(--muted);font-size:12px">checked %s</span>`, rec.LastCheckedAt.Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
if warnCount > 0 {
|
||||
noun := "events"
|
||||
if warnCount == 1 {
|
||||
noun = "event"
|
||||
}
|
||||
fmt.Fprintf(&b,
|
||||
`<span style="font-size:11px;background:var(--warn-bg,#fffbeb);color:var(--warn-fg,#92400e);border:1px solid var(--warn-border,#fde68a);border-radius:10px;padding:1px 7px;white-space:nowrap">%d %s</span>`,
|
||||
warnCount, noun)
|
||||
b.WriteString(renderSparkline(rec.History))
|
||||
}
|
||||
b.WriteString(`</div>`)
|
||||
|
||||
if rec.ErrorSummary != "" {
|
||||
fmt.Fprintf(&b, `<div style="font-size:12px;margin-bottom:8px;color:var(--muted)">%s</div>`, html.EscapeString(rec.ErrorSummary))
|
||||
fmt.Fprintf(&b, `<div style="font-size:12px;margin-bottom:4px;color:var(--muted)">%s</div>`, html.EscapeString(rec.ErrorSummary))
|
||||
if decoded := decodeAERStatus(rec.ErrorSummary); decoded != "" {
|
||||
fmt.Fprintf(&b,
|
||||
`<div style="font-size:12px;margin-bottom:8px;color:var(--muted)"><span style="background:var(--surface-alt,#f5f5f5);border-radius:4px;padding:1px 6px;font-family:monospace">AER: %s</span></div>`,
|
||||
html.EscapeString(decoded))
|
||||
}
|
||||
}
|
||||
|
||||
// History table — newest first, cap at 20 entries.
|
||||
|
||||
Reference in New Issue
Block a user