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, ``+"\n", svgWidth, svgHeight, svgWidth, svgHeight) for _, e := range pcieEdges { fmt.Fprintf(&b, ``+"\n", e.x1, e.y1, e.x2, e.y2, e.color) } for _, box := range boxes { writeTopoBoxSVG(&b, box) } b.WriteString(`
`) // 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. scaleMax := float64(rating) scaleEstimate := false if scaleMax <= 0 { if m := observedMaxW[strconv.Itoa(i)]; m > 0 { 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, `
`, i, scaleMax, maxSrc, voltAttr, stroke, text) fmt.Fprintf(&b, `
`, fillH, stroke) fmt.Fprintf(&b, `
%s
`, html.EscapeString(label)) fmt.Fprintf(&b, `
%s
`, html.EscapeString(detail)) if statusWord != "" { fmt.Fprintf(&b, `
%s
`, html.EscapeString(strings.ToUpper(statusWord))) } b.WriteString(`
`) } b.WriteString(`
`) 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, `
`+ `%s%s
`, html.EscapeString(name), int(ceil), measured, html.EscapeString(title), side, side, stroke, text, fillBar, glyph) } b.WriteString(`
`) 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, ``, 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, `
%s
`, html.EscapeString(label)) if item.sublabel != "" { fmt.Fprintf(&b, `
%s
`, html.EscapeString(item.sublabel)) } if item.statusLine != "" { fmt.Fprintf(&b, `
%s
`, html.EscapeString(item.statusLine)) } b.WriteString(`
`) } b.WriteString(``) 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, `
`+ `
GPU %d
`+ `
`+ `
GPU %d
`+ `
%d/%d links active%s
`+ `
`, 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(``) for _, pair := range pairs { fmt.Fprintf(&bodyB, ``, pair.GPUA, pair.GPUB, pair.NVLinks) } bodyB.WriteString(`
GPU AGPU BNVLinks
GPU %dGPU %d%d
`) } 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 }