Files
bee/audit/internal/webui/page_topo_diagram.go
T
Mikhail ChusavitinandClaude Sonnet 5 319f08ac4b feat(topo): per-socket bar layout with disks under their controller
Rework the /topo diagram from side-by-side stacked cards into one tall
vertical bar per CPU socket with everything attached to it branching off
sideways (socket 0 left/branches right, socket 1 right/branches left).

Disks are now parented under the storage controller they physically hang
off (SATA/AHCI, SAS HBA, RAID) — itself a NUMA-affine PCIe device under one
socket — instead of a synthetic catch-all node. The disk->controller link
is read from a new storage-controllers.txt techdump
(platform.StorageControllerMapScript, a /sys/block walk); disks with no
resolvable controller fall back to an "Other" bar. No board/root node.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYjrG6bVmeG1Z2Wmc8kg3o
2026-09-03 12:15:49 +03:00

1078 lines
36 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package webui
import (
"encoding/json"
"fmt"
"html"
"path/filepath"
"regexp"
"strconv"
"strings"
"bee/audit/internal/app"
"bee/audit/internal/platform"
"bee/audit/internal/schema"
)
func renderTopoMainDiagram(hw schema.HardwareSnapshot, exportDir string) string {
// A GPU hardware fault that needs a physical reboot (Xid 79 "fallen off
// the bus", Xid 154 "Node Reboot Required" — see gpuNeedsPhysicalReboot)
// isn't visible in dev.Status: the collector only sets that from PCIe
// link-speed checks, not from SAT/kmsg results. Without this, a GPU that
// dropped off the bus mid-test still renders green here even though the
// Hardware Summary card is showing a critical banner for it.
gpuHardwareFault := false
if db, err := app.OpenComponentStatusDB(filepath.Join(exportDir, "component-status.json")); err == nil {
if _, needsReboot := gpuNeedsPhysicalReboot(matchedRecords(db.All(), nil, []string{"pcie:gpu:"})); needsReboot {
gpuHardwareFault = true
}
}
socketIdx := buildSocketIndex(hw.CPUs)
numCols := len(hw.CPUs)
if numCols == 0 {
numCols = 1
}
unknownCol := numCols // extra trailing column for unmatched devices
// Group PCIe devices (GPU/NIC/RAID only — matches the mockup's node
// types) into columns by NUMA node, falling back to the "unknown" bucket.
type placedDevice struct {
dev schema.HardwarePCIeDevice
kind string // "gpu", "nic", "raid"
col int
bdf string
}
var placed []placedDevice
for _, dev := range hw.PCIeDevices {
kind := pcieDeviceKind(dev)
if kind == "" {
continue
}
col := unknownCol
if dev.NUMANode != nil {
if ci, ok := socketIdx[*dev.NUMANode]; ok {
col = ci
}
}
bdf := ""
if dev.Slot != nil {
bdf = normalizeTopoBDF(*dev.Slot)
} else if dev.BDF != nil {
bdf = normalizeTopoBDF(*dev.BDF)
}
placed = append(placed, placedDevice{dev: dev, kind: kind, col: col, bdf: bdf})
}
// GPU index<->BDF map + pairwise NVLink adjacency, read from the
// persisted techdump captured during the last audit cycle, best-effort:
// if the dump is missing (older audit, no NVIDIA GPUs), this is simply
// skipped. Used only to detect the cross-NUMA-bonded-pair anomaly below;
// the pairwise links themselves are drawn in the separate NVLink
// Topology card, since grouping same-kind/same-column devices into one
// stacked card here leaves no single per-GPU anchor point to draw a
// pairwise connector to or from.
bdfToIndex, _ := readNVIDIAIndexByBDF(exportDir)
var pairs []gpuPairLink
if topoMatrix, err := readGPUTopologyMatrix(exportDir); err == nil {
pairs = parseGPUPairAdjacency(topoMatrix)
}
gpuNUMAByIndex := map[int]*int{}
gpuBDFByIndex := map[int]string{}
for _, p := range placed {
if p.kind != "gpu" || p.bdf == "" {
continue
}
if idx, ok := bdfToIndex[p.bdf]; ok {
gpuNUMAByIndex[idx] = p.dev.NUMANode
gpuBDFByIndex[idx] = p.bdf
}
}
// A bonded pair spanning two different NUMA nodes is treated as an
// anomaly (not a neutral fact) per project decision: a bonded pair is
// expected to sit on one NUMA node, so a cross-NUMA bond escalates both
// GPUs' effective severity to at least Warning, regardless of their own
// reported SAT status.
crossNUMAWarnBDF := map[string]bool{}
for _, pair := range pairs {
numaA, okA := gpuNUMAByIndex[pair.GPUA]
numaB, okB := gpuNUMAByIndex[pair.GPUB]
if !okA || !okB || numaA == nil || numaB == nil || *numaA == *numaB {
continue
}
crossNUMAWarnBDF[gpuBDFByIndex[pair.GPUA]] = true
crossNUMAWarnBDF[gpuBDFByIndex[pair.GPUB]] = true
}
kindOrder := []string{"gpu", "nic", "raid"}
kindLabel := map[string]string{"gpu": "GPU", "nic": "NIC", "raid": "RAID"}
// Attach memory DIMMs to their CPU column too, the same way GPU/NIC/RAID
// PCIe devices are attached via NUMANode — memory has no NUMANode field
// in the schema, so this reads the DIMM's own Locator/Bank Locator
// strings instead (see dimmRawNode). DIMMs that can't be confidently
// attached fall back to the unattached "Memory" row below the diagram,
// same as before this existed.
memBankNodes := map[string]int{}
if raw, err := readTopoTechDump(exportDir, "dmidecode-type17.txt"); err == nil {
memBankNodes = parseDIMMBankLocatorNodes(raw)
}
memCol := make([]int, len(hw.Memory))
memMatched := make([]bool, len(hw.Memory))
var memRawNodes []int
for i, m := range hw.Memory {
if node, ok := dimmRawNode(m, memBankNodes); ok {
memCol[i] = node
memMatched[i] = true
memRawNodes = append(memRawNodes, node)
}
}
memColIdx := buildMemoryColumnIndex(memRawNodes)
for i := range hw.Memory {
if !memMatched[i] {
continue
}
col := memColIdx[memCol[i]]
if col >= numCols {
memMatched[i] = false
continue
}
memCol[i] = col
}
// Map each disk to the PCI function of the controller it hangs off, read
// from the persisted storage-controllers.txt techdump
// (platform.StorageControllerMapScript). Lets a disk be drawn as a branch
// of its real storage controller (SATA/AHCI, SAS HBA, RAID) — which is
// itself a NUMA-affine PCIe device under one CPU — instead of floating in
// a synthetic catch-all node.
ctrlByHCTL := map[string]string{}
if raw, err := readTopoTechDump(exportDir, "storage-controllers.txt"); err == nil {
ctrlByHCTL = parseStorageControllerMap(raw)
}
// NUMA node of each PCIe function, for joining a controller BDF to a CPU
// column (the controller itself is usually classed "SATA controller" /
// "Serial Attached SCSI controller" and so isn't in `placed`).
numaByBDF := map[string]int{}
classByBDF := map[string]string{}
modelByBDF := map[string]string{}
for _, dev := range hw.PCIeDevices {
bdf := ""
if dev.Slot != nil {
bdf = normalizeTopoBDF(*dev.Slot)
} else if dev.BDF != nil {
bdf = normalizeTopoBDF(*dev.BDF)
}
if bdf == "" {
continue
}
if dev.NUMANode != nil {
numaByBDF[bdf] = *dev.NUMANode
}
if dev.DeviceClass != nil {
classByBDF[bdf] = *dev.DeviceClass
}
if dev.Model != nil {
modelByBDF[bdf] = *dev.Model
}
}
// diskCtrlGroups[cpuCol][ctrlBDF] = disks on that controller (cpuCol == -1
// for controllers with no resolvable NUMA/CPU — rendered under "Other").
type ctrlGroup struct {
bdf string
disks []schema.HardwareStorage
}
diskCtrlGroups := map[int][]*ctrlGroup{}
var looseDisks []schema.HardwareStorage // no HCTL / no controller match at all
ctrlSeen := map[string]*ctrlGroup{}
for _, d := range hw.Storage {
hctl := ""
if d.Slot != nil {
hctl = strings.TrimSpace(*d.Slot)
}
ctrl := ""
if hctl != "" {
ctrl = ctrlByHCTL[hctl]
}
if ctrl == "" {
looseDisks = append(looseDisks, d)
continue
}
col := -1
if n, ok := numaByBDF[ctrl]; ok {
if ci, ok := socketIdx[n]; ok && ci < numCols {
col = ci
}
}
g := ctrlSeen[ctrl]
if g == nil {
g = &ctrlGroup{bdf: ctrl}
ctrlSeen[ctrl] = g
diskCtrlGroups[col] = append(diskCtrlGroups[col], g)
}
g.disks = append(g.disks, d)
}
// ── Layout ──────────────────────────────────────────────────────────────
// Each CPU socket is a tall vertical bar; everything attached to it
// branches off sideways as a vertical stack of boxes. Socket 0 sits on the
// left with its branches growing rightward, socket 1 on the right growing
// leftward (further sockets alternate sides, one row per pair). Disks hang
// off their storage-controller box as a further branch. An "Other" bar
// collects devices/disks with no resolvable socket. The figure grows
// downward, not sideways.
const (
topoBarW = 118
topoColGap = 46
topoBranchW = 208
topoSubW = 172
topoBlockGap = 30
topoMinBarH = 92
topoMidGap = 72
)
type topoBranch struct {
info topoCardInfo
edge string
subs []topoCardInfo // disk groups under a storage-controller branch
}
type topoBlock struct {
head topoCardInfo
branches []topoBranch
}
// storageControllerBranch builds one branch for a controller and its disks.
storageControllerBranch := func(g *ctrlGroup) topoBranch {
label := "Storage ctrl"
if c := strings.TrimSpace(classByBDF[g.bdf]); c != "" {
label = strings.TrimSpace(strings.NewReplacer("Controller", "", "controller", "").Replace(c))
if label == "" {
label = "Storage"
}
label += " ctrl"
}
sub := g.bdf
if m := strings.TrimSpace(modelByBDF[g.bdf]); m != "" && !strings.HasPrefix(m, "Device ") {
sub = m
}
var tally topoStatusTally
for _, d := range g.disks {
tally.add(classifyTopoSeverity(d.Status))
}
fill, stroke, text := topoSeverityColors(tally.worst())
return topoBranch{
info: topoCardInfo{
label: label, sublabel: sub, count: 1,
statusLine: fmt.Sprintf("%d disk(s)", len(g.disks)),
fillVar: fill, strokeVar: stroke, textVar: text,
detailType: "storage",
},
edge: "var(--ok-fg)",
subs: buildStorageGroupCards(g.disks),
}
}
// deviceBranches gathers the GPU/NIC/RAID branches for a given column
// (col == unknownCol for the "Other" bar).
deviceBranches := func(col int) []topoBranch {
var out []topoBranch
for _, kind := range kindOrder {
var group []placedDevice
for _, p := range placed {
if p.col == col && p.kind == kind {
group = append(group, p)
}
}
if len(group) == 0 {
continue
}
var tally topoStatusTally
model := ""
edgeColor := "var(--ok-fg)"
for i, p := range group {
sev := classifyTopoSeverity(p.dev.Status)
if kind == "gpu" && crossNUMAWarnBDF[p.bdf] && sev < 2 {
sev = 2
}
if kind == "gpu" && gpuHardwareFault && sev < 3 {
sev = 3
}
tally.add(sev)
if i == 0 && p.dev.Model != nil {
model = *p.dev.Model
}
if topoEdgeColorVar(p.dev) == "var(--warn-fg)" {
edgeColor = "var(--warn-fg)"
}
}
fill, stroke, text := topoSeverityColors(tally.worst())
out = append(out, topoBranch{
info: topoCardInfo{
label: kindLabel[kind], sublabel: model, count: len(group),
statusLine: tally.line(),
fillVar: fill, strokeVar: stroke, textVar: text,
detailType: kind,
},
edge: edgeColor,
})
}
return out
}
var blocks []topoBlock
for col := 0; col < numCols && col < len(hw.CPUs); col++ {
cpu := hw.CPUs[col]
model := ""
if cpu.Cores != nil && cpu.Threads != nil {
model = fmt.Sprintf("%dC / %dT", *cpu.Cores, *cpu.Threads)
} else if cpu.Model != nil {
model = cleanCPUModel(*cpu.Model)
}
socket := col
if cpu.Socket != nil {
socket = *cpu.Socket
}
var tally topoStatusTally
tally.add(classifyTopoSeverity(cpu.Status))
fill, stroke, text := topoSeverityColors(tally.worst())
blk := topoBlock{head: topoCardInfo{
label: fmt.Sprintf("CPU %d", socket), sublabel: model, count: 1,
statusLine: tally.line(),
fillVar: fill, strokeVar: stroke, textVar: text,
detailType: "cpu",
}}
// Memory first — wired straight to the socket's memory controller.
var memGroup []schema.HardwareMemory
for i, m := range hw.Memory {
if memMatched[i] && memCol[i] == col {
memGroup = append(memGroup, m)
}
}
if len(memGroup) > 0 {
var mt topoStatusTally
sizeGB := 0
for _, m := range memGroup {
mt.add(classifyTopoSeverity(m.Status))
if m.SizeMB != nil {
sizeGB += *m.SizeMB / 1024
}
}
mfill, mstroke, mtext := topoSeverityColors(mt.worst())
sublabel := ""
if sizeGB > 0 {
sublabel = fmt.Sprintf("%d GB total", sizeGB)
}
blk.branches = append(blk.branches, topoBranch{
info: topoCardInfo{
label: "Memory", sublabel: sublabel, count: len(memGroup),
statusLine: mt.line(),
fillVar: mfill, strokeVar: mstroke, textVar: mtext,
detailType: "memory",
},
edge: "var(--ok-fg)",
})
}
blk.branches = append(blk.branches, deviceBranches(col)...)
for _, g := range diskCtrlGroups[col] {
blk.branches = append(blk.branches, storageControllerBranch(g))
}
blocks = append(blocks, blk)
}
// "Other" bar: unmatched PCIe devices, unmatched storage controllers, and
// disks with no controller/HCTL at all.
var otherBranches []topoBranch
otherBranches = append(otherBranches, deviceBranches(unknownCol)...)
for _, g := range diskCtrlGroups[-1] {
otherBranches = append(otherBranches, storageControllerBranch(g))
}
if len(looseDisks) > 0 {
for _, ci := range buildStorageGroupCards(looseDisks) {
otherBranches = append(otherBranches, topoBranch{info: ci, edge: "var(--ok-fg)"})
}
}
hasOther := len(otherBranches) > 0
if hasOther {
fill, stroke, text := topoSeverityColors(0)
blocks = append(blocks, topoBlock{
head: topoCardInfo{
label: "Other", sublabel: "no socket affinity", count: 1,
fillVar: fill, strokeVar: stroke, textVar: text,
},
branches: otherBranches,
})
}
var boxes []topoBox
var pcieEdges []topoEdge
// side: 0 = left (branches grow right), 1 = right (branches grow left).
// CPU sockets alternate; the "Other" block is always left.
blockSide := func(i int) int {
if hasOther && i == len(blocks)-1 {
return 0
}
return i % 2
}
sideHasSubs := [2]bool{}
hasRight := false
for i, blk := range blocks {
s := blockSide(i)
if s == 1 {
hasRight = true
}
for _, br := range blk.branches {
if len(br.subs) > 0 {
sideHasSubs[s] = true
}
}
}
reach := func(s int) int {
r := topoBarW + topoColGap + topoBranchW
if sideHasSubs[s] {
r += topoColGap + topoSubW
}
return r
}
leftReach := reach(0)
svgWidth := 24 + leftReach + 24
if hasRight {
svgWidth = 24 + leftReach + topoMidGap + reach(1) + 24
}
leftBarX := 24
leftBranchX := leftBarX + topoBarW + topoColGap
leftSubX := leftBranchX + topoBranchW + topoColGap
rightBarX := svgWidth - 24 - topoBarW
rightBranchX := rightBarX - topoColGap - topoBranchW
rightSubX := rightBranchX - topoColGap - topoSubW
geom := func(s int) (barX, barLinkX, branchX, branchLinkX, subX int) {
if s == 0 {
return leftBarX, leftBarX + topoBarW, leftBranchX, leftBranchX + topoBranchW, leftSubX
}
return rightBarX, rightBarX, rightBranchX, rightBranchX, rightSubX
}
// No root/board node — board identity lives in the Firmware row below.
// Each socket bar is an independent column with its own branch stack.
// layoutBlock places one socket bar + its branch/sub boxes and returns the
// bar's bottom Y.
layoutBlock := func(blk topoBlock, s, barTop int) int {
barX, barLinkX, branchX, branchLinkX, subX := geom(s)
by := barTop
for _, br := range blk.branches {
midY := by + topoBoxHeight/2
boxes = append(boxes, topoBox{x: branchX, y: by, w: topoBranchW, h: topoBoxHeight, topoCardInfo: br.info})
pcieEdges = append(pcieEdges, topoEdge{x1: barLinkX, y1: midY, x2: branchLinkX, y2: midY, color: br.edge})
advance := topoBoxHeight + topoDeviceGap + topoStackLayers(br.info.count)*topoStackStep
if len(br.subs) > 0 {
sy := by
for _, sc := range br.subs {
smid := sy + topoBoxHeight/2
sLink := subX
if s == 1 {
sLink = subX + topoSubW
}
boxes = append(boxes, topoBox{x: subX, y: sy, w: topoSubW, h: topoBoxHeight, topoCardInfo: sc})
pcieEdges = append(pcieEdges, topoEdge{x1: branchLinkX, y1: midY, x2: sLink, y2: smid, color: "var(--ok-fg)"})
sy += topoBoxHeight + topoDeviceGap + topoStackLayers(sc.count)*topoStackStep
}
if sy-by > advance {
advance = sy - by
}
}
by += advance
}
barBottom := by - topoDeviceGap
if barBottom < barTop+topoMinBarH {
barBottom = barTop + topoMinBarH
}
boxes = append(boxes, topoBox{x: barX, y: barTop, w: topoBarW, h: barBottom - barTop, topoCardInfo: blk.head})
return barBottom
}
rowTop := topoTopMargin
i := 0
for i < len(blocks) {
s := blockSide(i)
rowBottom := layoutBlock(blocks[i], s, rowTop)
next := i + 1
if next < len(blocks) && blockSide(next) == 1 && s == 0 {
rb := layoutBlock(blocks[next], 1, rowTop)
if rb > rowBottom {
rowBottom = rb
}
next++
}
rowTop = rowBottom + topoBlockGap
i = next
}
svgHeight := topoTopMargin + topoMinBarH
for _, box := range boxes {
bottom := box.y + box.h + topoStackLayers(box.count)*topoStackStep
if bottom > svgHeight {
svgHeight = bottom
}
}
svgHeight += 24
var b strings.Builder
// Wrapped in its own horizontally-scrolling container (matching the
// overflow-x:auto convention used for wide tables elsewhere in webui)
// rather than max-width:100% — squashing a node/edge diagram to fit a
// narrow viewport makes labels and badges illegible, whereas scrolling
// keeps the diagram readable at its natural size on any screen width.
b.WriteString(`<div style="overflow-x:auto">`)
fmt.Fprintf(&b, `<svg width="%d" height="%d" viewBox="0 0 %d %d">`+"\n", svgWidth, svgHeight, svgWidth, svgHeight)
for _, e := range pcieEdges {
fmt.Fprintf(&b, `<line x1="%d" y1="%d" x2="%d" y2="%d" style="stroke:%s;stroke-width:2"/>`+"\n", e.x1, e.y1, e.x2, e.y2, e.color)
}
for _, box := range boxes {
writeTopoBoxSVG(&b, box)
}
b.WriteString(`</svg></div>`)
// Firmware (BMC/BIOS/...) and PSUs have no PCIe/CPU affinity to anchor
// them to a column, and there can be an arbitrary number of any of them
// — so unlike the diagram above, they're plain flex-wrap HTML below the
// SVG rather than absolutely-positioned SVG boxes. A fixed-size SVG
// canvas has no way to wrap overflow onto a new row, which is exactly
// what caused these to pile up and overlap once a board had more
// PSUs/firmware records than fit in one fixed-width row.
//
// Memory DIMMs that were matched to a CPU column above already got a
// box in the SVG diagram; only DIMMs that couldn't be attached to a
// column (see memMatched above) fall back to this row.
var unmatchedMem []schema.HardwareMemory
for i, m := range hw.Memory {
if !memMatched[i] {
unmatchedMem = append(unmatchedMem, m)
}
}
if len(unmatchedMem) > 0 {
var tally topoStatusTally
for _, m := range unmatchedMem {
tally.add(classifyTopoSeverity(m.Status))
}
fill, stroke, text := topoSeverityColors(tally.worst())
sizeGB := 0
for _, m := range unmatchedMem {
if m.SizeMB != nil {
sizeGB += *m.SizeMB / 1024
}
}
sublabel := ""
if sizeGB > 0 {
sublabel = fmt.Sprintf("%d GB total", sizeGB)
}
b.WriteString(renderTopoFlexRow("Memory", []topoCardInfo{{
label: "Memory", sublabel: sublabel, count: len(unmatchedMem),
statusLine: tally.line(),
fillVar: fill, strokeVar: stroke, textVar: text,
detailType: "memory",
}}))
}
var firmwareItems []topoCardInfo
for _, rec := range hw.Firmware {
// Firmware records carry no per-item status in the schema (they are
// identity, not health, facts), so each stays a neutral, uncolored
// card rather than forcing a fake "Unknown" status line.
fillVar, strokeVar, textVar := topoSeverityColors(0)
firmwareItems = append(firmwareItems, topoCardInfo{
label: rec.DeviceName, sublabel: "fw " + rec.Version, count: 1,
fillVar: fillVar, strokeVar: strokeVar, textVar: textVar,
})
}
b.WriteString(renderTopoFlexRow("Firmware", firmwareItems))
if len(hw.PowerSupplies) > 0 {
var tally topoStatusTally
watt := 0
for _, psu := range hw.PowerSupplies {
tally.add(classifyTopoSeverity(psu.Status))
if psu.WattageW != nil {
watt = *psu.WattageW
}
}
fill, stroke, text := topoSeverityColors(tally.worst())
sublabel := ""
if watt > 0 {
sublabel = fmt.Sprintf("%dW each", watt)
}
b.WriteString(renderTopoFlexRow("Power Supplies", []topoCardInfo{{
label: "Power Supplies", sublabel: sublabel, count: len(hw.PowerSupplies),
statusLine: tally.line(),
fillVar: fill, strokeVar: stroke, textVar: text,
detailType: "psu",
}}))
}
return topoCard("Topology", b.String())
}
// renderTopoFlexRow renders a labeled, wrapping row of component cards.
// Returns "" if items is empty (e.g. no PSU data in this audit).
func renderTopoFlexRow(title string, items []topoCardInfo) string {
if len(items) == 0 {
return ""
}
var b strings.Builder
fmt.Fprintf(&b, `<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin:16px 0 6px">%s</div>`,
html.EscapeString(title))
b.WriteString(`<div style="display:flex;flex-wrap:wrap;gap:10px">`)
for _, item := range items {
onclick := ""
cursor := "default"
if item.detailType != "" {
onclick = fmt.Sprintf(` onclick="openComponentDetail('%s')"`, item.detailType)
cursor = "pointer"
}
stackLayers := topoStackLayers(item.count)
// Extra right/bottom padding on the wrapper reserves room for the
// backing layers of the stack effect so they aren't clipped by the
// flex container.
fmt.Fprintf(&b, `<div style="position:relative;padding-right:%dpx;padding-bottom:%dpx">`,
stackLayers*topoStackStep, stackLayers*topoStackStep)
for i := stackLayers; i >= 1; i-- {
off := i * topoStackStep
fmt.Fprintf(&b, `<div style="position:absolute;top:%dpx;left:%dpx;right:0;bottom:0;border-radius:6px;background:%s;border:1px solid %s;opacity:.55"></div>`,
off, off, item.fillVar, item.strokeVar)
}
fmt.Fprintf(&b, `<div%s style="position:relative;cursor:%s;min-width:160px;padding:10px 12px;border-radius:6px;background:%s;border:1px solid %s;color:%s">`,
onclick, cursor, item.fillVar, item.strokeVar, item.textVar)
label := item.label
if item.count > 1 {
label = fmt.Sprintf("%s ×%d", item.label, item.count)
}
fmt.Fprintf(&b, `<div style="font-size:13px;font-weight:700">%s</div>`, html.EscapeString(label))
if item.sublabel != "" {
fmt.Fprintf(&b, `<div style="font-size:11px;opacity:.85">%s</div>`, html.EscapeString(item.sublabel))
}
if item.statusLine != "" {
fmt.Fprintf(&b, `<div style="font-size:11px;font-weight:600;margin-top:4px">%s</div>`, html.EscapeString(item.statusLine))
}
b.WriteString(`</div></div>`)
}
b.WriteString(`</div>`)
return b.String()
}
// buildStorageGroupCards groups disks by media type ("SSD"/"HDD"/"NVMe", or
// "Disk" when Type is absent) into one card per type: label "SSD ×2", a
// "<model> · <capacity> total" sublabel, and the worst-of status line for
// that group. Order follows first appearance in hw.Storage. Every card is
// clickable through to the shared "storage" component-detail modal.
func buildStorageGroupCards(disks []schema.HardwareStorage) []topoCardInfo {
if len(disks) == 0 {
return nil
}
type diskGroup struct {
label string
tally topoStatusTally
count int
sizeGB int
}
var order []string
groups := map[string]*diskGroup{}
for _, d := range disks {
label := "Disk"
if d.Type != nil && strings.TrimSpace(*d.Type) != "" {
label = strings.TrimSpace(*d.Type)
}
g := groups[label]
if g == nil {
g = &diskGroup{label: label}
groups[label] = g
order = append(order, label)
}
g.count++
g.tally.add(classifyTopoSeverity(d.Status))
if d.SizeGB != nil {
g.sizeGB += *d.SizeGB
}
}
var cards []topoCardInfo
for _, label := range order {
g := groups[label]
fill, stroke, text := topoSeverityColors(g.tally.worst())
// Capacity only — the model string is too long for an SVG node label
// and is one click away in the storage detail modal anyway.
sublabel := ""
if g.sizeGB > 0 {
if g.sizeGB >= 1024 {
sublabel = fmt.Sprintf("%.1f TB total", float64(g.sizeGB)/1024)
} else {
sublabel = fmt.Sprintf("%d GB total", g.sizeGB)
}
}
cards = append(cards, topoCardInfo{
label: g.label, sublabel: sublabel, count: g.count,
statusLine: g.tally.line(),
fillVar: fill, strokeVar: stroke, textVar: text,
detailType: "storage",
})
}
return cards
}
// parseStorageControllerMap parses storage-controllers.txt
// (platform.StorageControllerMapScript output: one
// "<name> hctl=<h:c:t:l> ctrl=<dddd:bb:dd.f>" line per disk) into a map from
// SCSI HCTL address (which matches schema.HardwareStorage.Slot) to the
// normalized PCI BDF of the controller the disk hangs off. Disks with an
// empty hctl (NVMe) are skipped — they have no HCTL Slot to join on.
func parseStorageControllerMap(raw string) map[string]string {
out := map[string]string{}
for _, line := range strings.Split(raw, "\n") {
var hctl, ctrl string
for _, f := range strings.Fields(line) {
if v, ok := strings.CutPrefix(f, "hctl="); ok {
hctl = v
}
if v, ok := strings.CutPrefix(f, "ctrl="); ok {
ctrl = normalizeTopoBDF(v)
}
}
if hctl != "" && ctrl != "" {
out[hctl] = ctrl
}
}
return out
}
// cleanCPUModel trims the marketing noise ("(R)", "(TM)", "CPU", "Processor")
// out of a dmidecode CPU model string so it fits the narrow socket bar.
func cleanCPUModel(s string) string {
s = strings.NewReplacer(
"(R)", "", "(r)", "", "(TM)", "", "(tm)", "",
" CPU", "", " Processor", "", " processor", "",
).Replace(s)
return strings.Join(strings.Fields(s), " ")
}
func writeTopoBoxSVG(b *strings.Builder, box topoBox) {
onclick := ""
cursor := "default"
if box.detailType != "" {
onclick = fmt.Sprintf(` onclick="openComponentDetail('%s')"`, box.detailType)
cursor = "pointer"
}
fmt.Fprintf(b, `<g%s style="cursor:%s">`, onclick, cursor)
// Stack-of-cards effect: faint offset rects behind the front card when
// this box represents more than one physical component (e.g. 4 GPUs in
// one NUMA column), so a group reads as "a deck of N" rather than a
// single item. Peeks toward the bottom-right, into space already
// reserved between this box and the next one in the column.
for i := topoStackLayers(box.count); i >= 1; i-- {
off := i * topoStackStep
fmt.Fprintf(b, `<rect x="%d" y="%d" width="%d" height="%d" rx="6" ry="6" style="fill:%s;stroke:%s;opacity:.55"/>`+"\n",
box.x+off, box.y+off, box.w, box.h, box.fillVar, box.strokeVar)
}
fmt.Fprintf(b, `<rect x="%d" y="%d" width="%d" height="%d" rx="6" ry="6" style="fill:%s;stroke:%s"/>`+"\n",
box.x, box.y, box.w, box.h, box.fillVar, box.strokeVar)
label := box.label
if box.count > 1 {
label = fmt.Sprintf("%s ×%d", box.label, box.count)
}
fmt.Fprintf(b, `<text x="%d" y="%d" style="fill:%s;font-size:13px;font-weight:700">%s</text>`+"\n",
box.x+10, box.y+20, box.textVar, html.EscapeString(label))
if box.sublabel != "" {
fmt.Fprintf(b, `<text x="%d" y="%d" style="fill:%s;font-size:11px;opacity:.85">%s</text>`+"\n",
box.x+10, box.y+36, box.textVar, html.EscapeString(truncateTopoLabel(box.sublabel, 26)))
}
if box.statusLine != "" {
fmt.Fprintf(b, `<text x="%d" y="%d" style="fill:%s;font-size:10px;font-weight:600">%s</text>`+"\n",
box.x+10, box.y+box.h-10, box.textVar, html.EscapeString(box.statusLine))
}
b.WriteString(`</g>` + "\n")
}
func truncateTopoLabel(s string, max int) string {
if len(s) <= max {
return s
}
if max <= 1 {
return s[:max]
}
return s[:max-1] + "…"
}
// ---------------------------------------------------------------------------
// Separate NVLink topology card (read from techdump, not written to any
// ingest contract)
// ---------------------------------------------------------------------------
type topoNVLinkPort struct {
Index int
Active bool
SpeedGBs *float64
ReplayErrors int64
RecoveryErrors int64
CRCErrors int64
}
var (
topoNVLinkGPUHeaderRe = regexp.MustCompile(`^GPU (\d+):`)
topoNVLinkSpeedLineRe = regexp.MustCompile(`^Link (\d+):\s*([\d.]+)\s*GB/s`)
topoNVLinkInactiveRe = regexp.MustCompile(`^Link (\d+):\s*<inactive>`)
)
func readTopoNVLinkStatus(exportDir string) (map[int][]topoNVLinkPort, error) {
raw, err := readTopoTechDump(exportDir, "nvidia-smi-nvlink-status.txt")
if err != nil {
return nil, err
}
return parseTopoNVLinkStatus(raw), nil
}
func parseTopoNVLinkStatus(raw string) map[int][]topoNVLinkPort {
result := map[int][]topoNVLinkPort{}
currentGPU := -1
for _, line := range strings.Split(raw, "\n") {
trimmed := strings.TrimSpace(line)
if m := topoNVLinkGPUHeaderRe.FindStringSubmatch(trimmed); m != nil {
currentGPU, _ = strconv.Atoi(m[1])
continue
}
if currentGPU < 0 {
continue
}
if m := topoNVLinkInactiveRe.FindStringSubmatch(trimmed); m != nil {
idx, _ := strconv.Atoi(m[1])
result[currentGPU] = append(result[currentGPU], topoNVLinkPort{Index: idx, Active: false})
continue
}
if m := topoNVLinkSpeedLineRe.FindStringSubmatch(trimmed); m != nil {
idx, _ := strconv.Atoi(m[1])
port := topoNVLinkPort{Index: idx, Active: true}
if speed, err := strconv.ParseFloat(m[2], 64); err == nil {
port.SpeedGBs = &speed
}
result[currentGPU] = append(result[currentGPU], port)
}
}
return result
}
func readTopoNVLinkErrors(exportDir string) (map[int]map[int][3]int64, error) {
raw, err := readTopoTechDump(exportDir, "nvidia-smi-nvlink-errors.txt")
if err != nil {
return nil, err
}
return parseTopoNVLinkErrors(raw), nil
}
// parseTopoNVLinkErrors returns, per GPU then link index, [replay, recovery, crc].
func parseTopoNVLinkErrors(raw string) map[int]map[int][3]int64 {
return platform.ParseNvidiaNVLinkErrors(raw)
}
// renderTopoNVLinkCard renders the separate NVLink topology card. Returns ""
// if there are fewer than 2 NVIDIA GPUs, or the nvidia-smi nvlink techdump
// wasn't captured (older audit, or nvidia-smi unavailable on that run).
func renderTopoNVLinkCard(hw schema.HardwareSnapshot, exportDir string) string {
gpuCount := 0
for _, dev := range hw.PCIeDevices {
if dev.DeviceClass != nil && isGPUDeviceClass(*dev.DeviceClass) {
gpuCount++
}
}
if gpuCount < 2 {
return ""
}
status, err := readTopoNVLinkStatus(exportDir)
if err != nil || len(status) == 0 {
return topoCard("NVLink Topology", `<span class="badge badge-unknown">nvidia-smi nvlink data unavailable</span>`)
}
errors, _ := readTopoNVLinkErrors(exportDir)
topoMatrix, _ := readGPUTopologyMatrix(exportDir)
pairs := parseGPUPairAdjacency(topoMatrix)
var bodyB strings.Builder
if gpuCount <= 4 && len(pairs) > 0 {
// Small GPU count: per-pair box+line with per-link detail.
for _, pair := range pairs {
activeCount, total, hasError := 0, 0, false
for _, port := range status[pair.GPUA] {
total++
if port.Active {
activeCount++
}
}
for _, counters := range errors[pair.GPUA] {
if counters[0] != 0 || counters[1] != 0 || counters[2] != 0 {
hasError = true
}
}
color := "var(--ok-fg)"
switch {
case hasError:
color = "var(--crit-fg)"
case total > 0 && activeCount < total:
color = "var(--warn-fg)"
}
fmt.Fprintf(&bodyB, `<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px">`+
`<div style="padding:8px 12px;border:1px solid var(--border);border-radius:6px">GPU %d</div>`+
`<div style="flex:1;height:2px;background:%s"></div>`+
`<div style="padding:8px 12px;border:1px solid var(--border);border-radius:6px">GPU %d</div>`+
`<div style="font-size:12px;color:var(--muted)">%d/%d links active%s</div>`+
`</div>`,
pair.GPUA, color, pair.GPUB, activeCount, total, errNoteSuffix(hasError))
}
} else if len(pairs) > 0 {
// Larger GPU counts (NVSwitch fabric): aggregate pair table instead of
// an unreadable all-to-all graph.
bodyB.WriteString(`<table><thead><tr><th>GPU A</th><th>GPU B</th><th>NVLinks</th></tr></thead><tbody>`)
for _, pair := range pairs {
fmt.Fprintf(&bodyB, `<tr><td>GPU %d</td><td>GPU %d</td><td>%d</td></tr>`, pair.GPUA, pair.GPUB, pair.NVLinks)
}
bodyB.WriteString(`</tbody></table>`)
} else {
bodyB.WriteString(`<span class="badge badge-unknown">No NVLink-bonded GPU pairs found</span>`)
}
return topoCard("NVLink Topology", bodyB.String())
}
func errNoteSuffix(hasError bool) string {
if hasError {
return " — errors detected"
}
return ""
}
// ---------------------------------------------------------------------------
// Inventory fallback for the component-detail modal
//
// handleAPIComponentDetail normally sources records from app.ComponentStatusDB,
// which only gains entries once something has actually written a status
// observation (SAT run, watchdog tick, ...). On a freshly booted host that
// hasn't run SAT yet, StatusDB can be entirely empty for a component type even
// though the /topo card for it already shows "N OK" — that card reads
// schema.HardwareComponentStatus.Status straight from the audit snapshot.
// inventoryFallbackRecords bridges that gap by building synthetic records
// from the same snapshot/classifiers the topology card uses, so the two
// views never disagree about how many devices exist or their status.
// ---------------------------------------------------------------------------
// topoSeverityStatus renders classifyTopoSeverity's rank back into the status
// string vocabulary renderComponentDetail/chipLetterClass expect ("OK",
// "Warning", "Critical", "Unknown") — kept in lockstep with classifyTopoSeverity
// so a device the topo card counts as "OK" is never shown here as "Unknown".
func topoSeverityStatus(status *string) string {
switch classifyTopoSeverity(status) {
case 3:
return "Critical"
case 2:
return "Warning"
case 1:
return "OK"
default:
return "Unknown"
}
}
// pcieDeviceKind classifies a PCIe device the same way renderTopoMainDiagram
// does, returning "" for devices that aren't GPU/NIC/RAID.
func pcieDeviceKind(dev schema.HardwarePCIeDevice) string {
switch {
case dev.DeviceClass != nil && isGPUDeviceClass(*dev.DeviceClass):
return "gpu"
case isNICDeviceClassDev(dev):
return "nic"
case dev.DeviceClass != nil && isRAIDControllerClass(*dev.DeviceClass):
return "raid"
default:
return ""
}
}
// pcieDeviceKey builds a stable, human-readable component key for a PCIe
// device: "<kind>:<bdf>" when a slot/BDF is known, else "<kind>:<index>".
func pcieDeviceKey(kind string, index int, dev schema.HardwarePCIeDevice) string {
bdf := ""
if dev.Slot != nil {
bdf = normalizeTopoBDF(*dev.Slot)
} else if dev.BDF != nil {
bdf = normalizeTopoBDF(*dev.BDF)
}
if bdf != "" {
return kind + ":" + bdf
}
return fmt.Sprintf("%s:%d", kind, index)
}
// inventoryFallbackRecords builds ComponentStatusRecord entries straight from
// the audit inventory (bee-audit.json) for the given component type, used
// when ComponentStatusDB has no matching records yet. Records carry only
// ComponentKey/Status — no LastCheckedAt/History — so renderComponentDetail
// renders them without a "checked at" timestamp or sparkline.
func inventoryFallbackRecords(compType string, opts HandlerOptions) []app.ComponentStatusRecord {
data, err := loadSnapshot(opts.AuditPath)
if err != nil {
return nil
}
var ingest schema.HardwareIngestRequest
if err := json.Unmarshal(data, &ingest); err != nil {
return nil
}
hw := ingest.Hardware
var records []app.ComponentStatusRecord
switch compType {
case "cpu":
for i, cpu := range hw.CPUs {
key := fmt.Sprintf("cpu:%d", i)
if cpu.Socket != nil {
key = fmt.Sprintf("cpu:socket%d", *cpu.Socket)
}
records = append(records, app.ComponentStatusRecord{ComponentKey: key, Status: topoSeverityStatus(cpu.Status)})
}
case "memory":
for i, m := range hw.Memory {
key := fmt.Sprintf("memory:%d", i)
if m.Slot != nil && strings.TrimSpace(*m.Slot) != "" {
key = "memory:" + strings.TrimSpace(*m.Slot)
}
records = append(records, app.ComponentStatusRecord{ComponentKey: key, Status: topoSeverityStatus(m.Status)})
}
case "storage":
for i, s := range hw.Storage {
key := fmt.Sprintf("storage:%d", i)
if s.Slot != nil && strings.TrimSpace(*s.Slot) != "" {
key = "storage:" + strings.TrimSpace(*s.Slot)
}
records = append(records, app.ComponentStatusRecord{ComponentKey: key, Status: topoSeverityStatus(s.Status)})
}
case "psu":
for i, p := range hw.PowerSupplies {
key := fmt.Sprintf("psu:%d", i)
if p.Slot != nil && strings.TrimSpace(*p.Slot) != "" {
key = "psu:" + strings.TrimSpace(*p.Slot)
}
records = append(records, app.ComponentStatusRecord{ComponentKey: key, Status: topoSeverityStatus(p.Status)})
}
case "gpu", "nic", "raid":
for i, dev := range hw.PCIeDevices {
if pcieDeviceKind(dev) != compType {
continue
}
key := pcieDeviceKey(compType, i, dev)
records = append(records, app.ComponentStatusRecord{ComponentKey: key, Status: topoSeverityStatus(dev.Status)})
}
}
return records
}