package viewer import ( "encoding/json" "fmt" "sort" "strings" "time" "reanimator/chart/web" ) var sectionOrder = []string{ "board", "firmware", "cpus", "memory", "storage", "pcie_devices", "power_supplies", "sensors", } var sectionTitles = map[string]string{ "board": "Board", "firmware": "Firmware", "cpus": "CPUs", "memory": "Memory", "storage": "Storage", "pcie_devices": "PCIe Devices", "power_supplies": "Power Supplies", "sensors": "Sensors", "fans": "Fans", "power": "Power", "temperatures": "Temperatures", "other": "Other", } var preferredMetaKeys = []string{"target_host", "collected_at", "source_type", "protocol", "filename"} var hiddenFields = map[string]struct{}{ "status_at_collection": {}, } var hiddenTableFields = map[string]struct{}{ "status_checked_at": {}, } const vendorDeviceIDField = "ven:dev" var commonPreferredColumns = []string{ "status", "slot", "location", "vendor", "manufacturer", "device_class", "type", "device_name", "name", "model", "product_name", "part_number", "serial_number", vendorDeviceIDField, "firmware", "version", "bdf", } var preferredColumns = map[string][]string{ "firmware": {"device_name", "version"}, "cpus": {"model", "clock", "cores", "threads", "l1", "l2", "l3", "microcode", "socket"}, "memory": {"part_number", "serial_number", "slot"}, "storage": {"type", "model", "serial_number", "firmware", "size_gb", "slot"}, "pcie_devices": {"device_class", "manufacturer", "model", "serial_number", "slot", "bdf"}, "power_supplies": {"vendor", "model", "part_number", "serial_number", "slot"}, "fans": {"name", "rpm"}, "power": {"name", "voltage_v", "current_a", "power_w"}, "temperatures": {"name", "celsius", "threshold_warning_celsius", "threshold_critical_celsius"}, "other": {"name", "value", "unit"}, } func RenderHTML(snapshot []byte, title string) ([]byte, error) { page, err := buildPageData(snapshot, title) if err != nil { return nil, err } return web.Render(page) } func buildPageData(snapshot []byte, title string) (pageData, error) { page := pageData{Title: title} if strings.TrimSpace(string(snapshot)) == "" { return page, nil } var root map[string]any if err := json.Unmarshal(snapshot, &root); err != nil { return pageData{}, fmt.Errorf("decode snapshot: %w", err) } page.HasSnapshot = true page.Meta = buildMeta(root) page.Sections = buildSections(root) return page, nil } func buildMeta(root map[string]any) []fieldRow { rows := make([]fieldRow, 0) used := make(map[string]struct{}) for _, key := range preferredMetaKeys { if isHiddenField(key) { continue } if value, ok := root[key]; ok { rows = append(rows, fieldRow{Key: key, Value: formatValue(value)}) used[key] = struct{}{} } } extraKeys := make([]string, 0) for key := range root { if key == "hardware" { continue } if isHiddenField(key) { continue } if _, ok := used[key]; ok { continue } extraKeys = append(extraKeys, key) } sort.Strings(extraKeys) for _, key := range extraKeys { rows = append(rows, fieldRow{Key: key, Value: formatValue(root[key])}) } return rows } func buildSections(root map[string]any) []sectionView { hardware, _ := root["hardware"].(map[string]any) if len(hardware) == 0 { return nil } sections := make([]sectionView, 0) used := make(map[string]struct{}) for _, key := range sectionOrder { value, ok := hardware[key] if !ok { continue } used[key] = struct{}{} sections = append(sections, buildSection(key, value)...) } extraKeys := make([]string, 0) for key := range hardware { if _, ok := used[key]; ok { continue } extraKeys = append(extraKeys, key) } sort.Strings(extraKeys) for _, key := range extraKeys { sections = append(sections, buildSection(key, hardware[key])...) } return sections } func buildSection(key string, value any) []sectionView { switch typed := value.(type) { case map[string]any: if key == "sensors" { return buildSensorSections(typed) } return []sectionView{{ ID: key, Title: titleFor(key), Kind: "object", Rows: buildFieldRows(typed), }} case []any: if key == "pcie_devices" { return []sectionView{buildPCIeSection(typed)} } return []sectionView{buildTableSection(key, typed)} default: return []sectionView{{ ID: key, Title: titleFor(key), Kind: "object", Rows: []fieldRow{ {Key: key, Value: formatValue(value)}, }, }} } } func buildSensorSections(sensors map[string]any) []sectionView { out := make([]sectionView, 0) for _, key := range []string{"fans", "power", "temperatures", "other"} { value, ok := sensors[key] if !ok { continue } items, ok := value.([]any) if !ok { continue } section := buildTableSection(key, items) section.ID = "sensors-" + key section.Title = "Sensors / " + titleFor(key) out = append(out, section) } return out } func buildTableSection(key string, items []any) sectionView { rows := make([]map[string]any, 0, len(items)) for _, item := range items { if row, ok := item.(map[string]any); ok { rows = append(rows, row) } } columns := collectColumns(key, rows) tableRows := make([]tableRow, 0, len(rows)) for _, row := range rows { cells := make(map[string]string, len(columns)) for _, column := range columns { cells[column] = formatRowValue(column, row) } status := strings.TrimSpace(cells["status"]) tableRows = append(tableRows, tableRow{ Status: status, Cells: cells, RawCells: row, }) } return sectionView{ ID: key, Title: titleFor(key), Kind: "table", Columns: columns, Items: tableRows, } } func buildPCIeSection(items []any) sectionView { grouped := make(map[string][]map[string]any) classNames := make([]string, 0) seenClasses := make(map[string]struct{}) for _, item := range items { row, ok := item.(map[string]any) if !ok { continue } className := strings.TrimSpace(formatValue(row["device_class"])) if className == "" { className = "Unclassified" } grouped[className] = append(grouped[className], row) if _, ok := seenClasses[className]; !ok { seenClasses[className] = struct{}{} classNames = append(classNames, className) } } sort.Strings(classNames) groups := make([]tableGroupView, 0, len(classNames)) for _, className := range classNames { rows := grouped[className] sortPCIeRows(rows) columns := collectColumns("pcie_devices", rows) items := make([]tableRow, 0, len(rows)) for _, row := range rows { cells := make(map[string]string, len(columns)) for _, column := range columns { cells[column] = formatRowValue(column, row) } items = append(items, tableRow{ Status: strings.TrimSpace(cells["status"]), Cells: cells, RawCells: row, }) } groups = append(groups, tableGroupView{ Title: className, Columns: columns, Items: items, }) } return sectionView{ ID: "pcie_devices", Title: titleFor("pcie_devices"), Kind: "grouped_tables", Groups: groups, } } func collectColumns(section string, rows []map[string]any) []string { seen := make(map[string]struct{}) for _, row := range rows { for key := range row { if isHiddenTableField(section, key) { continue } seen[key] = struct{}{} } if hasVendorDeviceID(row) { seen[vendorDeviceIDField] = struct{}{} } } columns := make([]string, 0, len(seen)) for _, key := range append(commonPreferredColumns, preferredColumns[section]...) { if _, ok := seen[key]; ok { columns = append(columns, key) delete(seen, key) } } extra := make([]string, 0, len(seen)) for key := range seen { extra = append(extra, key) } sort.Strings(extra) return append(columns, extra...) } func buildFieldRows(object map[string]any) []fieldRow { keys := make([]string, 0, len(object)) for key := range object { if isHiddenField(key) { continue } keys = append(keys, key) } sort.Strings(keys) rows := make([]fieldRow, 0, len(keys)) if combinedVendorDeviceID := formatVendorDeviceID(object); combinedVendorDeviceID != "" { rows = append(rows, fieldRow{Key: vendorDeviceIDField, Value: combinedVendorDeviceID}) } for _, key := range keys { rows = append(rows, fieldRow{Key: key, Value: formatValue(object[key])}) } return rows } func formatValue(value any) string { if value == nil { return "" } switch typed := value.(type) { case string: return formatStringValue(typed) case []any: parts := make([]string, 0, len(typed)) for _, item := range typed { parts = append(parts, formatValue(item)) } return strings.Join(parts, "\n") case map[string]any: return formatObjectValue(typed) default: data, err := json.Marshal(typed) if err != nil { return fmt.Sprint(typed) } text := string(data) text = strings.TrimPrefix(text, `"`) text = strings.TrimSuffix(text, `"`) return formatStringValue(text) } } func formatObjectValue(value map[string]any) string { keys := make([]string, 0, len(value)) for key := range value { if isHiddenField(key) { continue } keys = append(keys, key) } sort.Strings(keys) lines := make([]string, 0, len(keys)) for _, key := range keys { formatted := formatValue(value[key]) if strings.TrimSpace(formatted) == "" { lines = append(lines, key+":") continue } parts := strings.Split(formatted, "\n") lines = append(lines, key+": "+parts[0]) for _, part := range parts[1:] { lines = append(lines, " "+part) } } return strings.Join(lines, "\n") } func formatStringValue(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } if formatted, ok := formatDate(value); ok { return formatted } return value } func formatRowValue(column string, row map[string]any) string { if column == vendorDeviceIDField { return formatVendorDeviceID(row) } return formatValue(row[column]) } func formatVendorDeviceID(value map[string]any) string { vendorID := strings.TrimSpace(formatValue(value["vendor_id"])) deviceID := strings.TrimSpace(formatValue(value["device_id"])) if vendorID == "" || deviceID == "" { return "" } return vendorID + ":" + deviceID } func formatDate(value string) (string, bool) { layouts := []struct { layout string hasTime bool }{ {time.RFC3339Nano, true}, {time.RFC3339, true}, {"2006-01-02 15:04:05", true}, {"2006-01-02 15:04", true}, {"2006-01-02", false}, } for _, candidate := range layouts { parsed, err := time.Parse(candidate.layout, value) if err != nil { continue } if !candidate.hasTime { return parsed.Format("02 Jan 2006"), true } if parsed.Location() == time.UTC { return parsed.Format("02 Jan 2006, 15:04 UTC"), true } return parsed.Format("02 Jan 2006, 15:04 -07:00"), true } return "", false } func titleFor(key string) string { if value, ok := sectionTitles[key]; ok { return value } return strings.ReplaceAll(strings.Title(strings.ReplaceAll(key, "_", " ")), "Pcie", "PCIe") } func isHiddenField(key string) bool { if key == "vendor_id" || key == "device_id" { return true } _, ok := hiddenFields[key] return ok } func hasVendorDeviceID(value map[string]any) bool { return formatVendorDeviceID(value) != "" } func isHiddenTableField(section string, key string) bool { if isHiddenField(key) { return true } if section == "pcie_devices" && key == "device_class" { return true } _, ok := hiddenTableFields[key] return ok } func sortPCIeRows(rows []map[string]any) { sort.SliceStable(rows, func(i, j int) bool { left := []string{ formatRowValue("slot", rows[i]), formatRowValue("location", rows[i]), formatRowValue("vendor", rows[i]), formatRowValue("model", rows[i]), formatRowValue("serial_number", rows[i]), formatRowValue(vendorDeviceIDField, rows[i]), formatRowValue("bdf", rows[i]), } right := []string{ formatRowValue("slot", rows[j]), formatRowValue("location", rows[j]), formatRowValue("vendor", rows[j]), formatRowValue("model", rows[j]), formatRowValue("serial_number", rows[j]), formatRowValue(vendorDeviceIDField, rows[j]), formatRowValue("bdf", rows[j]), } for idx := range left { if left[idx] == right[idx] { continue } return left[idx] < right[idx] } return false }) }