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(`
`)
fmt.Fprintf(&b, `
`)
// 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))
hasPSU := len(hw.PowerSupplies) > 0
if hasPSU {
b.WriteString(renderTopoPSURow(hw.PowerSupplies, platform.ObservedPSUMaxW()))
}
// Cooling fans — one small clickable square per fan. Square SIZE encodes
// the fan's ceiling RPM (its class); the coloured FILL rising from the
// bottom encodes live duty cycle (current / ceiling).
fans := dedupeFansByName(hw.Sensors)
if len(fans) > 0 {
current := map[string]float64{}
for _, f := range fans {
if f.RPM != nil {
current[strings.TrimSpace(f.Name)] = float64(*f.RPM)
}
}
b.WriteString(renderTopoFanRow(fans, platform.ResolveFanMaxRPM(current), platform.ObservedFanMaxRPM()))
}
if hasPSU || len(fans) > 0 {
b.WriteString(topoLiveScript())
}
return topoCard("Topology", b.String())
}
// renderTopoPSURow renders the POWER SUPPLIES row: one card per PSU, coloured
// by that PSU's own status (a failed unit goes red on its own). The card shows
// input voltage and draw, and a load fill rising from the bottom — same idea
// as the fan duty-cycle fill. The load scale is the nameplate rating when the
// BMC reports it; otherwise it is the observed peak draw (observedMaxW, keyed
// by ordinal — the "autotune" recorded during any full-load run), and the
// figure is marked as an estimate.
func renderTopoPSURow(psus []schema.HardwarePowerSupply, observedMaxW map[string]float64) string {
var b strings.Builder
b.WriteString(topoRowHeading("Power Supplies"))
b.WriteString(`
`)
for i, p := range psus {
sev := classifyTopoSeverity(p.Status)
_, stroke, text := topoSeverityColors(sev)
label := fmt.Sprintf("PSU %d", i)
if p.Slot != nil && strings.TrimSpace(*p.Slot) != "" {
label = strings.TrimSpace(*p.Slot)
}
draw, haveDraw := psuDrawW(p)
rating := 0
if p.WattageW != nil && *p.WattageW > 0 {
rating = *p.WattageW
}
// Load scale: true rating if known, else the observed peak draw — but
// only once that peak sits meaningfully above the current draw. Until
// a real full-load run has bumped it, the "peak" is just idle draw and
// a load % off it would be nonsense, so fall back to plain watts.
scaleMax := float64(rating)
scaleEstimate := false
if scaleMax <= 0 {
if m := observedMaxW[strconv.Itoa(i)]; m > 0 && (!haveDraw || m >= draw*1.25) {
scaleMax = m
scaleEstimate = true
}
}
var parts []string
if p.InputVoltage != nil && *p.InputVoltage > 0 {
parts = append(parts, fmt.Sprintf("%.0f V", *p.InputVoltage))
}
switch {
case haveDraw && scaleMax > 0 && scaleEstimate:
parts = append(parts, fmt.Sprintf("%.0f / ~%.0f W · ~%.0f%% load", draw, scaleMax, draw/scaleMax*100))
case haveDraw && scaleMax > 0:
parts = append(parts, fmt.Sprintf("%.0f / %d W · %.0f%% load", draw, rating, draw/scaleMax*100))
case haveDraw:
parts = append(parts, fmt.Sprintf("%.0f W", draw))
case rating > 0:
parts = append(parts, fmt.Sprintf("%d W rated", rating))
}
detail := strings.Join(parts, " · ")
statusWord := ""
if sev >= 2 {
statusWord = topoSeverityStatus(p.Status)
}
fillH := 0.0
if haveDraw && scaleMax > 0 {
fillH = draw / scaleMax * 100
if fillH < 0 {
fillH = 0
}
if fillH > 100 {
fillH = 100
}
}
voltAttr := ""
if p.InputVoltage != nil && *p.InputVoltage > 0 {
voltAttr = fmt.Sprintf("%.0f", *p.InputVoltage)
}
maxSrc := "rated"
if scaleEstimate {
maxSrc = "observed"
}
fmt.Fprintf(&b, `
`)
return b.String()
}
// psuDrawW returns the PSU's current power draw (measured output preferred,
// else measured input).
func psuDrawW(p schema.HardwarePowerSupply) (float64, bool) {
switch {
case p.OutputPowerW != nil && *p.OutputPowerW > 0:
return *p.OutputPowerW, true
case p.InputPowerW != nil && *p.InputPowerW > 0:
return *p.InputPowerW, true
default:
return 0, false
}
}
// renderTopoFanRow renders the COOLING row. ceilByName (from
// platform.ResolveFanMaxRPM) has a value for every fan and drives tile size.
// observedByName (from platform.ObservedFanMaxRPM) holds only ceilings that
// were actually measured under load — a fan present there gets a duty-cycle
// fill; one that isn't shows no fill (ceiling not measured yet).
func renderTopoFanRow(fans []schema.HardwareFanSensor, ceilByName, observedByName map[string]float64) string {
const (
fanTileMin = 34 // px, the smallest-ceiling fan
fanTileMax = 60 // px, the largest-ceiling fan
)
ceilMax := 0.0
for _, v := range ceilByName {
if v > ceilMax {
ceilMax = v
}
}
var b strings.Builder
b.WriteString(topoRowHeading("Cooling"))
b.WriteString(topoFanSpinStyle())
b.WriteString(`
`)
for _, f := range fans {
name := strings.TrimSpace(f.Name)
_, stroke, text := topoSeverityColors(classifyTopoSeverity(f.Status))
ceil := ceilByName[name]
sizeRatio := 1.0
if ceilMax > 0 && ceil > 0 {
sizeRatio = ceil / ceilMax
}
side := fanTileMin + int(float64(fanTileMax-fanTileMin)*sizeRatio+0.5)
glyphSz := side * 7 / 16
// Duty cycle: only when the ceiling was actually measured under load.
duty := -1.0
if _, measured := observedByName[name]; measured && ceil > 0 && f.RPM != nil {
duty = float64(*f.RPM) / ceil * 100
if duty < 0 {
duty = 0
}
if duty > 100 {
duty = 100
}
}
title := name
switch {
case f.RPM == nil:
title = name + " · no reading"
case duty >= 0:
title = fmt.Sprintf("%s · %d RPM · %.0f%% duty (ceiling %d)", name, *f.RPM, duty, int(ceil))
default:
title = fmt.Sprintf("%s · %d RPM · ceiling not measured — run Fan Ceiling Check", name, *f.RPM)
}
glyph := fmt.Sprintf(``
if f.RPM != nil && *f.RPM > 0 {
period := fanSpinPeriodSec(float64(*f.RPM))
glyph = fmt.Sprintf(``
}
measured := 0
fillH := 0.0
if duty >= 0 {
measured = 1
fillH = duty
}
fillBar := fmt.Sprintf(``, fillH, stroke)
fmt.Fprintf(&b, `
`)
return b.String()
}
// topoLiveScript polls the already-collected live-metrics snapshot
// (/api/metrics/latest — served from memory, no BMC call) every 5s and
// refreshes the fan tiles (spin rate, duty fill, tooltip) and PSU tiles
// (wattage) in place. 5s is the metrics collector's own sampling period, so
// polling faster only re-reads identical numbers; the endpoint is a mutex
// read + small JSON, so this stays cheap with many viewers.
func topoLiveScript() string {
return ``
}
// fanSpinPeriodSec maps an absolute fan RPM to a CSS animation period (one
// full turn of the glyph, in seconds). The real period would be 60/RPM — a
// blur at any real fan speed — so it is compressed into a band the eye can
// actually read: at/below fanSpinRPMLo the glyph turns at its slowest still
// clearly-moving rate, at/above fanSpinRPMHi at the fastest rate past which
// faster is indistinguishable (and starts to stutter), linear in between.
func fanSpinPeriodSec(rpm float64) float64 {
const (
fanSpinRPMLo = 1000.0
fanSpinRPMHi = 13000.0
fanSpinSlowSec = 2.2
fanSpinFastSec = 0.35
)
switch {
case rpm <= fanSpinRPMLo:
return fanSpinSlowSec
case rpm >= fanSpinRPMHi:
return fanSpinFastSec
default:
t := (rpm - fanSpinRPMLo) / (fanSpinRPMHi - fanSpinRPMLo)
return fanSpinSlowSec + t*(fanSpinFastSec-fanSpinSlowSec)
}
}
// topoFanSpinStyle emits the keyframes + base class for the spinning fan
// glyph once per row. A repeated identical `
}
// topoFanGlyphPaths is the fan-blade drawing shared by every fan square,
// designed on a 24×24 viewBox.
func topoFanGlyphPaths() string {
return `` +
`` +
`` +
``
}
// dedupeFansByName returns the fan sensors from a snapshot with duplicate
// names collapsed to their first occurrence, matching the ingest contract's
// "(sensor_type, name) — first wins" rule and skipping unnamed sensors.
func dedupeFansByName(sensors *schema.HardwareSensors) []schema.HardwareFanSensor {
if sensors == nil {
return nil
}
seen := map[string]bool{}
var out []schema.HardwareFanSensor
for _, f := range sensors.Fans {
name := strings.TrimSpace(f.Name)
if name == "" || seen[name] {
continue
}
seen[name] = true
out = append(out, f)
}
return out
}
// topoRowHeading renders the small uppercase section label shared by the
// flex rows below the SVG diagram (Firmware / Power Supplies / Cooling / ...).
func topoRowHeading(title string) string {
return fmt.Sprintf(`
%s
`,
html.EscapeString(title))
}
// 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
b.WriteString(topoRowHeading(title))
b.WriteString(`
`)
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, `
`,
stackLayers*topoStackStep, stackLayers*topoStackStep)
for i := stackLayers; i >= 1; i-- {
off := i * topoStackStep
fmt.Fprintf(&b, ``,
off, off, item.fillVar, item.strokeVar)
}
fmt.Fprintf(&b, `
`)
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
// " · 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
// " hctl= ctrl=" 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, ``, 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, ``+"\n",
box.x+off, box.y+off, box.w, box.h, box.fillVar, box.strokeVar)
}
fmt.Fprintf(b, ``+"\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, `%s`+"\n",
box.x+10, box.y+20, box.textVar, html.EscapeString(label))
if box.sublabel != "" {
fmt.Fprintf(b, `%s`+"\n",
box.x+10, box.y+36, box.textVar, html.EscapeString(truncateTopoLabel(box.sublabel, 26)))
}
if box.statusLine != "" {
fmt.Fprintf(b, `%s`+"\n",
box.x+10, box.y+box.h-10, box.textVar, html.EscapeString(box.statusLine))
}
b.WriteString(`` + "\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*`)
)
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", `nvidia-smi nvlink data unavailable`)
}
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, `
`)
} else {
bodyB.WriteString(`No NVLink-bonded GPU pairs found`)
}
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: ":" when a slot/BDF is known, else ":".
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 "fan":
for i, f := range dedupeFansByName(hw.Sensors) {
name := strings.TrimSpace(f.Name)
key := fmt.Sprintf("fan:%d", i)
if name != "" {
key = "fan:" + name
}
records = append(records, app.ComponentStatusRecord{ComponentKey: key, Status: topoSeverityStatus(f.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
}