package viewer import ( "bytes" "encoding/json" "fmt" "sort" "strings" "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 preferredColumns = map[string][]string{ "firmware": {"device_name", "version"}, "cpus": {"socket", "model", "manufacturer", "status"}, "memory": {"slot", "location", "serial_number", "part_number", "size_mb", "status"}, "storage": {"slot", "type", "model", "serial_number", "firmware", "size_gb", "status"}, "pcie_devices": {"slot", "bdf", "device_class", "manufacturer", "model", "serial_number", "status"}, "power_supplies": {"slot", "vendor", "model", "serial_number", "part_number", "status"}, "fans": {"name", "location", "rpm", "status"}, "power": {"name", "location", "voltage_v", "current_a", "power_w", "status"}, "temperatures": {"name", "location", "celsius", "threshold_warning_celsius", "threshold_critical_celsius", "status"}, "other": {"name", "location", "value", "unit", "status"}, } 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.InputJSON = strings.TrimSpace(string(snapshot)) 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 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 _, 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: 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] = formatValue(row[column]) } 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 collectColumns(section string, rows []map[string]any) []string { seen := make(map[string]struct{}) for _, row := range rows { for key := range row { seen[key] = struct{}{} } } columns := make([]string, 0, len(seen)) for _, key := range 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 { keys = append(keys, key) } sort.Strings(keys) rows := make([]fieldRow, 0, len(keys)) 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 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: data, _ := json.MarshalIndent(typed, "", " ") return string(data) 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 text } } func titleFor(key string) string { if value, ok := sectionTitles[key]; ok { return value } return strings.ReplaceAll(strings.Title(strings.ReplaceAll(key, "_", " ")), "Pcie", "PCIe") } func prettyJSON(input string) string { if strings.TrimSpace(input) == "" { return "" } var out bytes.Buffer if err := json.Indent(&out, []byte(input), "", " "); err != nil { return input } return out.String() }