package webui import ( "encoding/json" "fmt" "html" "path/filepath" "sort" "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 = "Audit" body = renderAudit() case "validate": pageID = "validate" title = "Validate" body = renderValidate(opts) case "burn": pageID = "burn" title = "Burn" body = renderBurn() case "benchmark": pageID = "benchmark" title = "Benchmark" body = renderBenchmark(opts) case "tasks": pageID = "tasks" title = "Tasks" body = renderTasks() case "tools": pageID = "tools" title = "Tools" body = renderTools() // Legacy routes kept accessible but not in nav case "metrics": pageID = "metrics" title = "Live Metrics" body = renderMetrics() case "tests": pageID = "validate" title = "Acceptance Tests" body = renderValidate(opts) case "burn-in": pageID = "burn" title = "Burn-in Tests" body = renderBurn() 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(renderHardwareSummaryCard(opts)) b.WriteString(renderHealthCard(opts)) b.WriteString(renderMetrics()) return b.String() } // 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 { 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() } var b strings.Builder b.WriteString(`
Hardware Summary
`) // 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 := func(label, value, badgeHTML string) { b.WriteString(fmt.Sprintf(``, html.EscapeString(label), html.EscapeString(value), badgeHTML)) } writeRow("CPU", hwDescribeCPU(hw), renderComponentChips(matchedRecords(records, []string{"cpu:all"}, nil))) writeRow("Memory", hwDescribeMemory(hw), renderComponentChips(matchedRecords(records, []string{"memory:all"}, []string{"memory:"}))) writeRow("Storage", hwDescribeStorage(hw), renderComponentChips(matchedRecords(records, []string{"storage:all"}, []string{"storage:"}))) writeRow("GPU", hwDescribeGPU(hw), renderComponentChips(matchedRecords(records, nil, []string{"pcie: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)) 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) } // 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 := false if dev.DeviceClass != nil { c := strings.ToLower(strings.TrimSpace(*dev.DeviceClass)) isNIC = c == "ethernetcontroller" || c == "networkcontroller" || strings.Contains(c, "fibrechannel") } 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 ` ` } func renderHealthCard(opts HandlerOptions) string { data, err := loadSnapshot(filepath.Join(opts.ExportDir, "runtime-health.json")) if err != nil { return `
Runtime Health
No data
` } var health schema.RuntimeHealth if err := json.Unmarshal(data, &health); err != nil { return `
Runtime Health
Parse error
` } status := strings.TrimSpace(health.Status) if status == "" { status = "UNKNOWN" } badge := "badge-ok" if status == "PARTIAL" { badge = "badge-warn" } else if status == "FAIL" || status == "FAILED" { badge = "badge-err" } var b strings.Builder b.WriteString(`
Runtime Health
`) b.WriteString(fmt.Sprintf(`
%s
`, badge, html.EscapeString(status))) if checkedAt := strings.TrimSpace(health.CheckedAt); checkedAt != "" { b.WriteString(`
Checked at: ` + html.EscapeString(checkedAt) + `
`) } rows := []runtimeHealthRow{ buildRuntimeExportRow(health), buildRuntimeNetworkRow(health), buildRuntimeDriverRow(health), buildRuntimeAccelerationRow(health), buildRuntimeToolsRow(health), buildRuntimeServicesRow(health), buildRuntimeUSBExportRow(health), buildRuntimeToRAMRow(health), } b.WriteString(``) for _, row := range rows { b.WriteString(``) } b.WriteString(`
CheckStatusSourceIssue
` + html.EscapeString(row.Title) + `` + runtimeStatusBadge(row.Status) + `` + html.EscapeString(row.Source) + `` + rowIssueHTML(row.Issue) + `
`) b.WriteString(`
`) return b.String() } type runtimeHealthRow struct { Title string Status string Source string Issue string } func buildRuntimeExportRow(health schema.RuntimeHealth) runtimeHealthRow { issue := runtimeIssueDescriptions(health.Issues, "export_dir_unavailable") status := "UNKNOWN" switch { case issue != "": status = "FAILED" case strings.TrimSpace(health.ExportDir) != "": status = "OK" } source := "os.MkdirAll" if dir := strings.TrimSpace(health.ExportDir); dir != "" { source += " " + dir } return runtimeHealthRow{Title: "Export Directory", Status: status, Source: source, Issue: issue} } func buildRuntimeNetworkRow(health schema.RuntimeHealth) runtimeHealthRow { status := strings.TrimSpace(health.NetworkStatus) if status == "" { status = "UNKNOWN" } issue := runtimeIssueDescriptions(health.Issues, "dhcp_partial", "dhcp_failed") return runtimeHealthRow{Title: "Network", Status: status, Source: "ListInterfaces / DHCP", Issue: issue} } func buildRuntimeDriverRow(health schema.RuntimeHealth) runtimeHealthRow { issue := runtimeIssueDescriptions(health.Issues, "nvidia_kernel_module_missing", "nvidia_modeset_failed", "amdgpu_kernel_module_missing") status := "UNKNOWN" switch { case health.DriverReady && issue == "": status = "OK" case health.DriverReady: status = "PARTIAL" case issue != "": status = "FAILED" } return runtimeHealthRow{Title: "NVIDIA/AMD Driver", Status: status, Source: "lsmod / vendor probe", Issue: issue} } func buildRuntimeAccelerationRow(health schema.RuntimeHealth) runtimeHealthRow { issue := runtimeIssueDescriptions(health.Issues, "cuda_runtime_not_ready", "rocm_smi_unavailable") status := "UNKNOWN" switch { case health.CUDAReady && issue == "": status = "OK" case health.CUDAReady: status = "PARTIAL" case issue != "": status = "FAILED" } return runtimeHealthRow{Title: "CUDA / ROCm", Status: status, Source: "bee-gpu-burn / rocm-smi", Issue: issue} } func buildRuntimeToolsRow(health schema.RuntimeHealth) runtimeHealthRow { if len(health.Tools) == 0 { return runtimeHealthRow{Title: "Required Utilities", Status: "UNKNOWN", Source: "CheckTools", Issue: "No tool status data."} } missing := make([]string, 0) for _, tool := range health.Tools { if !tool.OK { missing = append(missing, tool.Name) } } status := "OK" issue := "" if len(missing) > 0 { status = "PARTIAL" issue = "Missing: " + strings.Join(missing, ", ") } return runtimeHealthRow{Title: "Required Utilities", Status: status, Source: "CheckTools", Issue: issue} } func buildRuntimeServicesRow(health schema.RuntimeHealth) runtimeHealthRow { if len(health.Services) == 0 { return runtimeHealthRow{Title: "Bee Services", Status: "UNKNOWN", Source: "systemctl is-active", Issue: "No service status data."} } nonActive := make([]string, 0) for _, svc := range health.Services { state := strings.TrimSpace(strings.ToLower(svc.Status)) // "activating" and "deactivating" are transient states for oneshot services // (RemainAfterExit=yes) — the service is running normally, not failed. // Only "failed" and "inactive" (after services should be running) are problems. switch state { case "active", "activating", "deactivating", "reloading": // OK — service is running or transitioning normally default: nonActive = append(nonActive, svc.Name+"="+svc.Status) } } status := "OK" issue := "" if len(nonActive) > 0 { status = "PARTIAL" issue = strings.Join(nonActive, ", ") } return runtimeHealthRow{Title: "Bee Services", Status: status, Source: "ServiceState", Issue: issue} } func buildRuntimeUSBExportRow(health schema.RuntimeHealth) runtimeHealthRow { path := strings.TrimSpace(health.USBExportPath) if path != "" { return runtimeHealthRow{ Title: "USB Export Drive", Status: "OK", Source: "/proc/mounts + lsblk", Issue: path, } } return runtimeHealthRow{ Title: "USB Export Drive", Status: "WARNING", Source: "/proc/mounts + lsblk", Issue: "No writable USB drive mounted. Plug in a USB drive to enable log export.", } } func buildRuntimeToRAMRow(health schema.RuntimeHealth) runtimeHealthRow { switch strings.ToLower(strings.TrimSpace(health.ToRAMStatus)) { case "ok": return runtimeHealthRow{ Title: "LiveCD in RAM", Status: "OK", Source: "live-boot / /proc/mounts", Issue: "", } case "partial": return runtimeHealthRow{ Title: "LiveCD in RAM", Status: "WARNING", Source: "live-boot / /proc/mounts / /dev/shm/bee-live", Issue: "Partial or staged RAM copy detected. System is not fully running from RAM; Copy to RAM can be retried.", } case "failed": return runtimeHealthRow{ Title: "LiveCD in RAM", Status: "FAILED", Source: "live-boot / /proc/mounts", Issue: "toram boot parameter set but ISO is not mounted from RAM. Copy may have failed.", } default: // toram not active — ISO still on original boot media (USB/CD) return runtimeHealthRow{ Title: "LiveCD in RAM", Status: "WARNING", Source: "live-boot / /proc/mounts", Issue: "ISO not copied to RAM. Use \u201cCopy to RAM\u201d to free the boot drive and improve performance.", } } } func buildHardwareComponentRows(exportDir string) []runtimeHealthRow { path := filepath.Join(exportDir, "component-status.json") db, err := app.OpenComponentStatusDB(path) if err != nil { return []runtimeHealthRow{ {Title: "CPU Component Health", Status: "UNKNOWN", Source: "component-status.json", Issue: "Component status DB not available."}, {Title: "Memory Component Health", Status: "UNKNOWN", Source: "component-status.json", Issue: "Component status DB not available."}, {Title: "Storage Component Health", Status: "UNKNOWN", Source: "component-status.json", Issue: "Component status DB not available."}, {Title: "GPU Component Health", Status: "UNKNOWN", Source: "component-status.json", Issue: "Component status DB not available."}, {Title: "PSU Component Health", Status: "UNKNOWN", Source: "component-status.json", Issue: "No PSU component checks recorded."}, } } records := db.All() return []runtimeHealthRow{ aggregateComponentStatus("CPU", records, []string{"cpu:all"}, nil), aggregateComponentStatus("Memory", records, []string{"memory:all"}, []string{"memory:"}), aggregateComponentStatus("Storage", records, []string{"storage:all"}, []string{"storage:"}), aggregateComponentStatus("GPU", records, nil, []string{"pcie:gpu:"}), aggregateComponentStatus("PSU", records, nil, []string{"psu:"}), } } // matchedRecords returns all ComponentStatusRecord entries whose key matches // any exact key or any of the given prefixes. Used for per-device chip rendering. func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" } func matchedRecords(records []app.ComponentStatusRecord, exact []string, prefixes []string) []app.ComponentStatusRecord { var matched []app.ComponentStatusRecord for _, rec := range records { key := strings.TrimSpace(rec.ComponentKey) if key == "" { continue } if containsExactKey(key, exact) || hasAnyPrefix(key, prefixes) { matched = append(matched, rec) } } return matched } func aggregateComponentStatus(title string, records []app.ComponentStatusRecord, exact []string, prefixes []string) runtimeHealthRow { matched := make([]app.ComponentStatusRecord, 0) for _, rec := range records { key := strings.TrimSpace(rec.ComponentKey) if key == "" { continue } if containsExactKey(key, exact) || hasAnyPrefix(key, prefixes) { matched = append(matched, rec) } } if len(matched) == 0 { return runtimeHealthRow{Title: title, Status: "UNKNOWN", Source: "component-status.json", Issue: "No component status data."} } maxSev := -1 for _, rec := range matched { if sev := runtimeComponentSeverity(rec.Status); sev > maxSev { maxSev = sev } } status := "UNKNOWN" switch maxSev { case 3: status = "CRITICAL" case 2: status = "WARNING" case 1: status = "OK" } sources := make([]string, 0) sourceSeen := map[string]struct{}{} issues := make([]string, 0) issueSeen := map[string]struct{}{} for _, rec := range matched { if runtimeComponentSeverity(rec.Status) != maxSev { continue } source := latestComponentSource(rec) if source == "" { source = "component-status.json" } if _, ok := sourceSeen[source]; !ok { sourceSeen[source] = struct{}{} sources = append(sources, source) } issue := strings.TrimSpace(rec.ErrorSummary) if issue == "" { issue = latestComponentDetail(rec) } if issue == "" { continue } if _, ok := issueSeen[issue]; ok { continue } issueSeen[issue] = struct{}{} issues = append(issues, issue) } if len(sources) == 0 { sources = append(sources, "component-status.json") } issue := strings.Join(issues, "; ") if issue == "" { issue = "—" } return runtimeHealthRow{ Title: title, Status: status, Source: strings.Join(sources, ", "), Issue: issue, } } func containsExactKey(key string, exact []string) bool { for _, candidate := range exact { if key == candidate { return true } } return false } func hasAnyPrefix(key string, prefixes []string) bool { for _, prefix := range prefixes { if strings.HasPrefix(key, prefix) { return true } } return false } func runtimeComponentSeverity(status string) int { switch strings.TrimSpace(strings.ToLower(status)) { case "critical": return 3 case "warning": return 2 case "ok": return 1 default: return 0 } } func latestComponentSource(rec app.ComponentStatusRecord) string { if len(rec.History) == 0 { return "" } return strings.TrimSpace(rec.History[len(rec.History)-1].Source) } func latestComponentDetail(rec app.ComponentStatusRecord) string { if len(rec.History) == 0 { return "" } return strings.TrimSpace(rec.History[len(rec.History)-1].Detail) } func runtimeIssueDescriptions(issues []schema.RuntimeIssue, codes ...string) string { if len(issues) == 0 || len(codes) == 0 { return "" } allowed := make(map[string]struct{}, len(codes)) for _, code := range codes { allowed[code] = struct{}{} } messages := make([]string, 0) for _, issue := range issues { if _, ok := allowed[issue.Code]; !ok { continue } desc := strings.TrimSpace(issue.Description) if desc == "" { desc = issue.Code } messages = append(messages, desc) } return strings.Join(messages, "; ") } // chipLetterClass maps a component status to a single display letter and CSS class. func chipLetterClass(status string) (letter, cls string) { switch strings.ToUpper(strings.TrimSpace(status)) { case "OK": return "O", "chip-ok" case "WARNING", "WARN", "PARTIAL": return "W", "chip-warn" case "CRITICAL", "FAIL", "FAILED", "ERROR": return "F", "chip-fail" default: return "?", "chip-unknown" } } // renderComponentChips renders one 20×20 chip per ComponentStatusRecord. // Hover tooltip shows component key, status, error summary and last check time. // Falls back to a single unknown chip when no records are available. func renderComponentChips(matched []app.ComponentStatusRecord) string { if len(matched) == 0 { return `?` } sort.Slice(matched, func(i, j int) bool { return matched[i].ComponentKey < matched[j].ComponentKey }) var b strings.Builder b.WriteString(``) for _, rec := range matched { letter, cls := chipLetterClass(rec.Status) var tooltip strings.Builder tooltip.WriteString(rec.ComponentKey) tooltip.WriteString(": ") tooltip.WriteString(firstNonEmpty(rec.Status, "UNKNOWN")) if rec.ErrorSummary != "" { tooltip.WriteString(" — ") tooltip.WriteString(rec.ErrorSummary) } if !rec.LastCheckedAt.IsZero() { fmt.Fprintf(&tooltip, " (checked %s)", rec.LastCheckedAt.Format("15:04:05")) } fmt.Fprintf(&b, `%s`, cls, html.EscapeString(tooltip.String()), letter) } b.WriteString(``) return b.String() } func runtimeStatusBadge(status string) string { status = strings.ToUpper(strings.TrimSpace(status)) badge := "badge-unknown" switch status { case "OK": badge = "badge-ok" case "PARTIAL", "WARNING", "WARN": badge = "badge-warn" case "FAIL", "FAILED", "CRITICAL": badge = "badge-err" } return `` + html.EscapeString(status) + `` } func rowIssueHTML(issue string) string { issue = strings.TrimSpace(issue) if issue == "" { return `` } return html.EscapeString(issue) }