package webui import ( "encoding/json" "fmt" "html" "path/filepath" "strings" "bee/audit/internal/app" "bee/audit/internal/schema" ) // renderPage dispatches to the appropriate page renderer. func renderPage(page string, opts HandlerOptions) string { var pageID, title, body string switch page { case "dashboard", "": pageID = "dashboard" title = "Dashboard" body = renderDashboard(opts) case "audit": pageID = "audit" title = "1. Audit" body = renderAudit() case "check": pageID = "check" title = "2. Check" body = renderCheck(opts) case "load": pageID = "load" title = "3. Load" body = renderValidateStress(opts) case "burn": pageID = "burn" title = "4. Burn" body = renderBurn() case "benchmark": pageID = "benchmark" title = "5. Benchmark" body = renderBenchmark(opts) case "scenario": pageID = "scenario" title = "6. Scenario" body = renderScenario(opts) case "tools": pageID = "tools" title = "Tools" body = renderTools() case "topo": pageID = "topo" title = "Topology" body = renderTopo(opts) case "settings": pageID = "settings" title = "Settings" body = renderSettings(opts) // Legacy routes (redirected at HTTP level in handlePage; these are fallbacks) case "validate", "tests": pageID = "load" title = "3. Load" body = renderValidate(opts) case "burn-in": pageID = "burn" title = "4. Burn" body = renderBurn() case "speed", "endurance": pageID = "benchmark" title = "5. Benchmark" body = renderBenchmark(opts) case "tasks": pageID = "tasks" title = "Tasks" body = renderTasks() // Hidden pages (not in nav, accessible by direct URL) case "metrics": pageID = "metrics" title = "Live Metrics" body = renderMetrics() case "network": pageID = "network" title = "Network" body = renderNetwork() case "services": pageID = "services" title = "Services" body = renderServices() case "export": pageID = "export" title = "Export" body = renderExport(opts.ExportDir) case "install": pageID = "install" title = "Install to Disk" body = renderInstall() default: pageID = "dashboard" title = "Not Found" body = `
Page not found.
` } return layoutHead(opts.Title+" — "+title) + layoutNav(pageID, opts.BuildLabel) + `

` + html.EscapeString(title) + `

` + body + `
` + renderAuditModal() + `
` + `` + `` } // ── Dashboard ───────────────────────────────────────────────────────────────── func renderDashboard(opts HandlerOptions) string { var b strings.Builder b.WriteString(renderAuditStatusBanner(opts)) b.WriteString(renderTimeSyncCard()) b.WriteString(renderHardwareSummaryCard(opts)) b.WriteString(renderHealthCard(opts)) b.WriteString(renderMetrics()) b.WriteString(``) return b.String() } // renderTimeSyncCard shows the server's current clock and timezone next to the // browser's own, highlights any drift, and offers a button that syncs the host // clock and timezone to whatever the client's browser reports. func renderTimeSyncCard() string { return `
System Time
Server
Browser
` } // renderAuditStatusBanner shows a live progress banner when an audit task is // running and auto-reloads the page when it completes. func renderAuditStatusBanner(opts HandlerOptions) string { // If audit data already exists, no banner needed — data is fresh. // We still inject the polling script so a newly-triggered audit also reloads. hasData := false if _, err := loadSnapshot(opts.AuditPath); err == nil { hasData = true } _ = hasData return ` ` } func renderAudit() string { return `
Audit Viewer
` } func renderHardwareSummaryCard(opts HandlerOptions) string { const cardID = ` id="hw-summary-card"` data, err := loadSnapshot(opts.AuditPath) if err != nil { return `
Hardware Summary
` } var ingest schema.HardwareIngestRequest if err := json.Unmarshal(data, &ingest); err != nil { return `
Hardware Summary
Parse error
` } hw := ingest.Hardware var records []app.ComponentStatusRecord if db, err := app.OpenComponentStatusDB(filepath.Join(opts.ExportDir, "component-status.json")); err == nil { records = db.All() } gpuRecords := matchedRecords(records, nil, []string{"pcie:gpu:"}) var b strings.Builder b.WriteString(`
Hardware Summary
`) if reason, needsReboot := gpuNeedsPhysicalReboot(gpuRecords); needsReboot { fmt.Fprintf(&b, `
⚠ GPU needs a physical reboot/power-cycle — further GPU tests will keep failing until then: %s
`, html.EscapeString(reason)) } // Server identity block above the component table. { var model, serial string parts := []string{} if hw.Board.Manufacturer != nil && strings.TrimSpace(*hw.Board.Manufacturer) != "" { parts = append(parts, strings.TrimSpace(*hw.Board.Manufacturer)) } if hw.Board.ProductName != nil && strings.TrimSpace(*hw.Board.ProductName) != "" { parts = append(parts, strings.TrimSpace(*hw.Board.ProductName)) } if len(parts) > 0 { model = strings.Join(parts, " ") } serial = strings.TrimSpace(hw.Board.SerialNumber) if model != "" || serial != "" { b.WriteString(`
`) if model != "" { fmt.Fprintf(&b, `
%s
`, html.EscapeString(model)) } if serial != "" { fmt.Fprintf(&b, `
S/N: %s
`, html.EscapeString(serial)) } b.WriteString(`
`) } } b.WriteString(``) // writeRow renders one component row. compType is the URL path segment for the detail // endpoint (e.g. "cpu"). Pass "" for rows that have no detail view. writeRow := func(label, value, badgeHTML, compType string) { var labelHTML string if compType != "" { labelHTML = fmt.Sprintf( `%s`, compType, html.EscapeString(label)) } else { labelHTML = html.EscapeString(label) } fmt.Fprintf(&b, ``, labelHTML, html.EscapeString(value), badgeHTML) } writeRow("CPU", hwDescribeCPU(hw), renderComponentChips(matchedRecords(records, []string{"cpu:all"}, nil)), "cpu") writeRow("Memory", hwDescribeMemory(hw), renderComponentChips(matchedRecords(records, []string{"memory:all"}, []string{"memory:"})), "memory") writeRow("Storage", hwDescribeStorage(hw), renderComponentChips(matchedRecords(records, []string{"storage:all"}, []string{"storage:"})), "storage") writeRow("GPU", hwDescribeGPU(hw), renderComponentChips(gpuRecords), "gpu") psuMatched := matchedRecords(records, nil, []string{"psu:"}) if len(psuMatched) == 0 && len(hw.PowerSupplies) > 0 { // No PSU records yet — synthesise a single chip from IPMI status. psuStatus := hwPSUStatus(hw.PowerSupplies) psuMatched = []app.ComponentStatusRecord{{ComponentKey: "psu:ipmi", Status: psuStatus}} } writeRow("PSU", hwDescribePSU(hw), renderComponentChips(psuMatched), "psu") if nicDesc := hwDescribeNIC(hw); nicDesc != "" { writeRow("Network", nicDesc, "", "") } b.WriteString(`
%s%s%s
`) b.WriteString(`
`) return b.String() } // hwDescribeCPU returns a human-readable CPU summary, e.g. "2× Intel Xeon Gold 6338". func hwDescribeCPU(hw schema.HardwareSnapshot) string { counts := map[string]int{} order := []string{} for _, cpu := range hw.CPUs { model := "Unknown CPU" if cpu.Model != nil && *cpu.Model != "" { model = *cpu.Model } if counts[model] == 0 { order = append(order, model) } counts[model]++ } if len(order) == 0 { return "—" } parts := make([]string, 0, len(order)) for _, m := range order { if counts[m] > 1 { parts = append(parts, fmt.Sprintf("%d× %s", counts[m], m)) } else { parts = append(parts, m) } } return strings.Join(parts, ", ") } // hwDescribeMemory returns a summary like "16× 32 GB DDR4". func hwDescribeMemory(hw schema.HardwareSnapshot) string { type key struct { sizeMB int typ string } counts := map[key]int{} order := []key{} for _, dimm := range hw.Memory { if dimm.SizeMB == nil || *dimm.SizeMB == 0 { continue } t := "" if dimm.Type != nil { t = *dimm.Type } k := key{*dimm.SizeMB, t} if counts[k] == 0 { order = append(order, k) } counts[k]++ } if len(order) == 0 { return "—" } parts := make([]string, 0, len(order)) for _, k := range order { gb := k.sizeMB / 1024 desc := fmt.Sprintf("%d× %d GB", counts[k], gb) if k.typ != "" { desc += " " + k.typ } parts = append(parts, desc) } return strings.Join(parts, ", ") } // hwDescribeStorage returns a summary like "4× 3.84 TB NVMe, 2× 1.92 TB SATA". func hwDescribeStorage(hw schema.HardwareSnapshot) string { type key struct { sizeGB int iface string } counts := map[key]int{} order := []key{} for _, disk := range hw.Storage { sz := 0 if disk.SizeGB != nil { sz = *disk.SizeGB } iface := "" if disk.Interface != nil { iface = *disk.Interface } else if disk.Type != nil { iface = *disk.Type } k := key{sz, iface} if counts[k] == 0 { order = append(order, k) } counts[k]++ } if len(order) == 0 { return "—" } parts := make([]string, 0, len(order)) for _, k := range order { var sizeStr string if k.sizeGB >= 1000 { sizeStr = fmt.Sprintf("%.2g TB", float64(k.sizeGB)/1000) } else if k.sizeGB > 0 { sizeStr = fmt.Sprintf("%d GB", k.sizeGB) } else { sizeStr = "?" } desc := fmt.Sprintf("%d× %s", counts[k], sizeStr) if k.iface != "" { desc += " " + k.iface } parts = append(parts, desc) } return strings.Join(parts, ", ") } // hwDescribeGPU returns a summary like "8× NVIDIA H100 80GB". func hwDescribeGPU(hw schema.HardwareSnapshot) string { counts := map[string]int{} order := []string{} for _, dev := range hw.PCIeDevices { if dev.DeviceClass == nil { continue } if !isGPUDeviceClass(*dev.DeviceClass) { continue } model := "Unknown GPU" if dev.Model != nil && *dev.Model != "" { model = *dev.Model } if counts[model] == 0 { order = append(order, model) } counts[model]++ } if len(order) == 0 { return "—" } parts := make([]string, 0, len(order)) for _, m := range order { if counts[m] > 1 { parts = append(parts, fmt.Sprintf("%d× %s", counts[m], m)) } else { parts = append(parts, m) } } return strings.Join(parts, ", ") } // hwPSUStatus returns "OK", "CRITICAL", "WARNING", or "UNKNOWN" based on // PSU statuses from the audit snapshot. Used as fallback when component-status.json // has no psu: records yet (e.g. first boot before audit writes them). func hwPSUStatus(psus []schema.HardwarePowerSupply) string { worst := "UNKNOWN" for _, psu := range psus { if psu.Status == nil { continue } switch strings.ToUpper(strings.TrimSpace(*psu.Status)) { case "CRITICAL": return "CRITICAL" case "WARNING": if worst != "CRITICAL" { worst = "WARNING" } case "OK": if worst == "UNKNOWN" { worst = "OK" } } } return worst } // hwDescribePSU returns a summary like "2× 1600 W" or "2× PSU". func hwDescribePSU(hw schema.HardwareSnapshot) string { n := len(hw.PowerSupplies) if n == 0 { return "—" } // Try to get a consistent wattage watt := 0 consistent := true for _, psu := range hw.PowerSupplies { if psu.WattageW == nil { consistent = false break } if watt == 0 { watt = *psu.WattageW } else if *psu.WattageW != watt { consistent = false break } } if consistent && watt > 0 { return fmt.Sprintf("%d× %d W", n, watt) } return fmt.Sprintf("%d× PSU", n) } // isNICDeviceClass reports whether a normalized PCIe DeviceClass string // (see collector.mapPCIeDeviceClass) identifies a NIC/HBA-class device. // webui does not import collector (see the "Classification helpers" note // in page_topo.go), so this mirrors collector.isNICClass locally. func isNICDeviceClass(class string) bool { c := strings.ToLower(strings.TrimSpace(class)) return c == "ethernetcontroller" || c == "networkcontroller" || strings.Contains(c, "fibrechannel") } // hwDescribeNIC returns a summary like "2× Mellanox ConnectX-6". func hwDescribeNIC(hw schema.HardwareSnapshot) string { counts := map[string]int{} order := []string{} for _, dev := range hw.PCIeDevices { isNIC := dev.DeviceClass != nil && isNICDeviceClass(*dev.DeviceClass) if !isNIC && len(dev.MacAddresses) == 0 { continue } model := "" if dev.Model != nil && *dev.Model != "" { model = *dev.Model } else if dev.Manufacturer != nil && *dev.Manufacturer != "" { model = *dev.Manufacturer + " NIC" } else { model = "NIC" } if counts[model] == 0 { order = append(order, model) } counts[model]++ } if len(order) == 0 { return "" } parts := make([]string, 0, len(order)) for _, m := range order { if counts[m] > 1 { parts = append(parts, fmt.Sprintf("%d× %s", counts[m], m)) } else { parts = append(parts, m) } } return strings.Join(parts, ", ") } func isGPUDeviceClass(class string) bool { switch strings.TrimSpace(class) { case "VideoController", "DisplayController", "ProcessingAccelerator": return true default: return false } } func renderAuditModal() string { return ` ` }