webui/topo: responsive PSU/firmware layout, stacked group cards, fix zero-CPU crash

PSU and firmware (BMC/BIOS) boxes were absolutely-positioned SVG rects in a
single fixed-width row with no wrap, so an arbitrary/larger count piled up
and overlapped once a board had more of either than fit in that row. Move
them to plain flex-wrap HTML below the diagram (Firmware row first, then
Power Supplies), which reflows naturally for any count. The main CPU/PCIe/
GPU diagram now scrolls horizontally (overflow-x:auto) instead of being
squashed to fit narrow viewports, matching the wide-table convention used
elsewhere in webui.

Also fixes a crash: renderTopoMainDiagram forced numCols to 1 for layout
purposes when a snapshot has zero CPUs, then unconditionally indexed
hw.CPUs[0], panicking the whole /topo page on any audit without CPU data.

Same-kind/same-column components (e.g. 4 GPUs in one NUMA node) now render
as one stacked card summarizing worst-case status plus a tally line ("3 OK,
1 Warning") instead of one box per component, and card severity coloring
uses real fill/stroke vars instead of HTML badge classes that don't apply
any style to SVG shapes.
This commit is contained in:
Mikhail Chusavitin
2026-07-09 10:52:38 +03:00
parent 10557ec0f6
commit 1d5c02ebaa
2 changed files with 527 additions and 161 deletions
+150 -7
View File
@@ -6,6 +6,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
@@ -84,6 +85,73 @@ func TestTopoPageRendersCPUAndDegradedPCIeLink(t *testing.T) {
}
}
func TestTopoPageRendersArbitraryPSUAndFirmwareCountsAsFlexRows(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
okStatus := "OK"
watt := 3000
var psus []schema.HardwarePowerSupply
for i := 0; i < 6; i++ {
slot := strconv.Itoa(i)
psus = append(psus, schema.HardwarePowerSupply{
HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus},
Slot: &slot,
WattageW: &watt,
})
}
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
Firmware: []schema.HardwareFirmwareRecord{
{DeviceName: "BIOS", Version: "2.1.0"},
{DeviceName: "BMC", Version: "5.17.00"},
},
PowerSupplies: psus,
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
body := rec.Body.String()
// Firmware row must render every record (BIOS + BMC, not just BMC).
if !strings.Contains(body, "BIOS") || !strings.Contains(body, "BMC") {
t.Fatalf("topo page missing BIOS/BMC firmware boxes: %s", body)
}
// All 6 PSUs must be represented, grouped into one stacked card with a
// count rather than 6 separate boxes.
if !strings.Contains(body, "Power Supplies ×6") {
t.Fatalf("topo page missing grouped Power Supplies x6 card: %s", body)
}
if strings.Count(body, `onclick="openComponentDetail(&#39;psu&#39;)"`) != 1 &&
strings.Count(body, `onclick="openComponentDetail('psu')"`) != 1 {
t.Fatalf("expected exactly one clickable PSU group card, not one per PSU: %s", body)
}
// Firmware/PSU rows must be flex-wrap HTML (arbitrary count, no overlap),
// not absolutely-positioned SVG rects sharing fixed x/y coordinates.
if !strings.Contains(body, "flex-wrap:wrap") {
t.Fatalf("topo page missing flex-wrap layout for firmware/PSU rows: %s", body)
}
// Firmware row must come before the PSU row.
if strings.Index(body, "BIOS") > strings.Index(body, "Power Supplies") {
t.Fatalf("firmware row should render before PSU row: %s", body)
}
}
func TestTopoPageLinkedFromNav(t *testing.T) {
handler := NewHandler(HandlerOptions{})
rec := httptest.NewRecorder()
@@ -237,15 +305,90 @@ func TestIsNICDeviceClassDev(t *testing.T) {
}
}
func TestTopoStatusBadgeClassNilIsUnknown(t *testing.T) {
label, cls := topoStatusBadgeClass(nil)
if label != "?" || cls != "badge-unknown" {
t.Fatalf("nil status = (%q,%q) want (?, badge-unknown)", label, cls)
func TestClassifyTopoSeverityNilIsUnknown(t *testing.T) {
if sev := classifyTopoSeverity(nil); sev != 0 {
t.Fatalf("nil status severity=%d want 0 (unknown)", sev)
}
ok := "OK"
label, cls = topoStatusBadgeClass(&ok)
if label != "OK" || cls != "badge-ok" {
t.Fatalf("OK status = (%q,%q) want (OK, badge-ok)", label, cls)
if sev := classifyTopoSeverity(&ok); sev != 1 {
t.Fatalf("OK status severity=%d want 1", sev)
}
warn := "Warning"
if sev := classifyTopoSeverity(&warn); sev != 2 {
t.Fatalf("Warning status severity=%d want 2", sev)
}
crit := "Critical"
if sev := classifyTopoSeverity(&crit); sev != 3 {
t.Fatalf("Critical status severity=%d want 3", sev)
}
}
func TestTopoStatusTallyLine(t *testing.T) {
var t1 topoStatusTally
t1.add(1)
if got := t1.line(); got != "OK" {
t.Fatalf("single-OK line=%q want %q", got, "OK")
}
var t2 topoStatusTally
t2.add(1)
t2.add(1)
t2.add(1)
t2.add(2)
if got := t2.line(); got != "1 Warning, 3 OK" {
t.Fatalf("mixed line=%q want %q", got, "1 Warning, 3 OK")
}
}
func TestTopoMainDiagramGroupsSameKindSameColumnIntoOneStackedCard(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket := 0
numaNode := 0
deviceClass := "VideoController"
model := "NVIDIA H100 80GB HBM3"
okStatus := "OK"
var gpus []schema.HardwarePCIeDevice
for i := 0; i < 4; i++ {
gpus = append(gpus, schema.HardwarePCIeDevice{
HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus},
DeviceClass: &deviceClass,
Model: &model,
NUMANode: &numaNode,
})
}
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket},
},
PCIeDevices: gpus,
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if !strings.Contains(body, "GPU ×4") {
t.Fatalf("expected one grouped GPU x4 card, got: %s", body)
}
if strings.Count(body, "openComponentDetail('gpu')") != 1 {
t.Fatalf("expected exactly one clickable GPU card, not one per GPU: %s", body)
}
if !strings.Contains(body, "4 OK") {
t.Fatalf("expected group status line '4 OK': %s", body)
}
}