Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3836a34cc | ||
|
|
ba9a52a61a | ||
|
|
27373aa104 | ||
|
|
4f7b5b826a | ||
|
|
dfd64550cf | ||
|
|
9505303d1d | ||
|
|
f2c04cf0e8 |
Submodule internal/chart updated: 2a15bc87f1...8105c7ec08
@@ -16,11 +16,21 @@ type AnalysisResult struct {
|
||||
SourceTimezone string `json:"source_timezone,omitempty"` // Source timezone/offset used during collection (e.g. +08:00)
|
||||
CollectedAt time.Time `json:"collected_at,omitempty"` // Collection/upload timestamp
|
||||
InventoryLastModifiedAt time.Time `json:"inventory_last_modified_at,omitempty"` // Redfish inventory last modified (InventoryData/Status)
|
||||
RawPayloads map[string]any `json:"raw_payloads,omitempty"` // Additional source payloads (e.g. Redfish tree)
|
||||
Events []Event `json:"events"`
|
||||
FRU []FRUInfo `json:"fru"`
|
||||
Sensors []SensorReading `json:"sensors"`
|
||||
Hardware *HardwareConfig `json:"hardware"`
|
||||
RawPayloads map[string]any `json:"raw_payloads,omitempty"` // Additional source payloads (e.g. Redfish tree)
|
||||
CollectionErrors []CollectionError `json:"collection_errors,omitempty"` // BMC-reported failures to collect specific sections
|
||||
Events []Event `json:"events"`
|
||||
FRU []FRUInfo `json:"fru"`
|
||||
Sensors []SensorReading `json:"sensors"`
|
||||
Hardware *HardwareConfig `json:"hardware"`
|
||||
}
|
||||
|
||||
// CollectionError represents a BMC-reported failure to collect a specific data section.
|
||||
// Populated by vendor parsers when the source explicitly returns an error response
|
||||
// instead of structured data (e.g. {"error":"...","code":1458} in Inspur component.log).
|
||||
type CollectionError struct {
|
||||
Section string `json:"section"`
|
||||
Message string `json:"message"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// Event represents a single log event
|
||||
|
||||
5
internal/parser/vendors/inspur/asset.go
vendored
5
internal/parser/vendors/inspur/asset.go
vendored
@@ -117,7 +117,6 @@ func ParseAssetJSON(content []byte, pcieSlotDeviceNames map[int]string, pcieSlot
|
||||
}
|
||||
|
||||
// Parse CPU info
|
||||
seenMicrocode := make(map[string]bool)
|
||||
for i, cpu := range asset.CpuInfo {
|
||||
config.CPUs = append(config.CPUs, models.CPU{
|
||||
Socket: i,
|
||||
@@ -133,13 +132,11 @@ func ParseAssetJSON(content []byte, pcieSlotDeviceNames map[int]string, pcieSlot
|
||||
PPIN: cpu.PPIN,
|
||||
})
|
||||
|
||||
// Add CPU microcode to firmware list (deduplicated)
|
||||
if cpu.MicroCodeVer != "" && !seenMicrocode[cpu.MicroCodeVer] {
|
||||
if cpu.MicroCodeVer != "" {
|
||||
config.Firmware = append(config.Firmware, models.FirmwareInfo{
|
||||
DeviceName: fmt.Sprintf("CPU%d Microcode", i),
|
||||
Version: cpu.MicroCodeVer,
|
||||
})
|
||||
seenMicrocode[cpu.MicroCodeVer] = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
145
internal/parser/vendors/inspur/component.go
vendored
145
internal/parser/vendors/inspur/component.go
vendored
@@ -19,6 +19,11 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
||||
|
||||
text := string(content)
|
||||
|
||||
// Parse RESTful CPU info — fallback when asset.json is absent
|
||||
if len(hw.CPUs) == 0 {
|
||||
parseCPUInfo(text, hw)
|
||||
}
|
||||
|
||||
// Parse RESTful Memory info (detailed memory data)
|
||||
parseMemoryInfo(text, hw)
|
||||
|
||||
@@ -51,6 +56,52 @@ func ParseComponentLogEvents(content []byte) []models.Event {
|
||||
return events
|
||||
}
|
||||
|
||||
// ParseComponentLogCollectionErrors detects BMC-reported collection failures in component.log.
|
||||
// When a RESTful section returns {"error":"...","code":N} instead of structured data,
|
||||
// the BMC itself failed to collect that subsystem — the parser emits a CollectionError
|
||||
// so the UI can surface it explicitly rather than showing an empty section.
|
||||
func ParseComponentLogCollectionErrors(content []byte) []models.CollectionError {
|
||||
type bmcErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
// Map of section name (for display) → regex that captures its JSON payload.
|
||||
// Sections that return arrays use \[ ... \]; object sections use \{ ... \}.
|
||||
// We only probe sections that are expected to have structured hardware data.
|
||||
sections := []struct {
|
||||
name string
|
||||
re *regexp.Regexp
|
||||
}{
|
||||
{"HDD", regexp.MustCompile(`RESTful HDD info:\s*(\{[^\n]*\})`)},
|
||||
{"PCIe Devices", regexp.MustCompile(`RESTful PCIE Device info:\s*(\{[^\n]*\})`)},
|
||||
{"Network Adapters", regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[^\n]*\})`)},
|
||||
{"Disk Backplane", regexp.MustCompile(`RESTful diskbackplane info:\s*(\{[^\n]*\})`)},
|
||||
}
|
||||
|
||||
text := string(content)
|
||||
var out []models.CollectionError
|
||||
for _, s := range sections {
|
||||
m := s.re.FindStringSubmatch(text)
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
var errResp bmcErrorResponse
|
||||
if err := json.Unmarshal([]byte(m[1]), &errResp); err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(errResp.Error) == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, models.CollectionError{
|
||||
Section: s.name,
|
||||
Message: errResp.Error,
|
||||
Code: errResp.Code,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseComponentLogSensors extracts sensor readings from component.log JSON sections.
|
||||
func ParseComponentLogSensors(content []byte) []models.SensorReading {
|
||||
text := string(content)
|
||||
@@ -61,6 +112,68 @@ func ParseComponentLogSensors(content []byte) []models.SensorReading {
|
||||
return out
|
||||
}
|
||||
|
||||
// CPURESTInfo represents the RESTful CPU info structure in component.log
|
||||
type CPURESTInfo struct {
|
||||
Processors []struct {
|
||||
ProcID int `json:"proc_id"`
|
||||
CPUID string `json:"PROC_ID"` // uppercase key — prevents case-insensitive collision with proc_id
|
||||
Manufacturer string `json:"Manufacturer"`
|
||||
MaxSpeedMHz int `json:"MaxSpeedMHz"`
|
||||
ConfigStatus int `json:"configStatus"`
|
||||
ProcName string `json:"proc_name"`
|
||||
ProcStatus int `json:"proc_status"`
|
||||
ProcSpeed int `json:"proc_speed"`
|
||||
CoreCount int `json:"proc_core_count"`
|
||||
ThreadCount int `json:"proc_thread_count"`
|
||||
TDP int `json:"proc_tdp"`
|
||||
L1Cache int `json:"proc_l1cache_size"`
|
||||
L2Cache int `json:"proc_l2cache_size"`
|
||||
L3Cache int `json:"proc_l3cache_size"`
|
||||
MicroCode string `json:"micro_code"`
|
||||
PPIN string `json:"ppin"`
|
||||
Status string `json:"status"`
|
||||
} `json:"processors"`
|
||||
}
|
||||
|
||||
func parseCPUInfo(text string, hw *models.HardwareConfig) {
|
||||
re := regexp.MustCompile(`RESTful CPU info:\s*(\{[\s\S]*?\})\s*RESTful Memory`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return
|
||||
}
|
||||
|
||||
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||
var cpuInfo CPURESTInfo
|
||||
if err := json.Unmarshal([]byte(jsonStr), &cpuInfo); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, proc := range cpuInfo.Processors {
|
||||
if proc.ProcStatus != 1 && proc.ConfigStatus != 1 {
|
||||
continue
|
||||
}
|
||||
hw.CPUs = append(hw.CPUs, models.CPU{
|
||||
Socket: proc.ProcID,
|
||||
Model: strings.TrimSpace(proc.ProcName),
|
||||
Cores: proc.CoreCount,
|
||||
Threads: proc.ThreadCount,
|
||||
FrequencyMHz: proc.ProcSpeed,
|
||||
MaxFreqMHz: proc.MaxSpeedMHz,
|
||||
L1CacheKB: proc.L1Cache,
|
||||
L2CacheKB: proc.L2Cache,
|
||||
L3CacheKB: proc.L3Cache,
|
||||
TDP: proc.TDP,
|
||||
PPIN: proc.PPIN,
|
||||
})
|
||||
if proc.MicroCode != "" {
|
||||
hw.Firmware = append(hw.Firmware, models.FirmwareInfo{
|
||||
DeviceName: fmt.Sprintf("CPU%d Microcode", proc.ProcID),
|
||||
Version: proc.MicroCode,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MemoryRESTInfo represents the RESTful Memory info structure
|
||||
type MemoryRESTInfo struct {
|
||||
MemModules []struct {
|
||||
@@ -112,9 +225,10 @@ func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
||||
}
|
||||
for _, mem := range memInfo.MemModules {
|
||||
item := models.MemoryDIMM{
|
||||
Slot: mem.MemModSlot,
|
||||
Location: mem.MemModSlot,
|
||||
Present: mem.MemModStatus == 1 && mem.MemModSize > 0,
|
||||
Slot: mem.MemModSlot,
|
||||
Location: mem.MemModSlot,
|
||||
// status=1 with a known serial/part is definitely present even if BMC reports size=0
|
||||
Present: mem.MemModStatus == 1 && (mem.MemModSize > 0 || strings.TrimSpace(mem.MemModSerial) != "" || strings.TrimSpace(mem.MemModPartNum) != ""),
|
||||
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
|
||||
Type: mem.MemModType,
|
||||
Technology: strings.TrimSpace(mem.MemModTechnology),
|
||||
@@ -136,6 +250,25 @@ func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
||||
}
|
||||
merged = append(merged, item)
|
||||
}
|
||||
|
||||
// If a present DIMM has size=0 (BMC firmware glitch), infer size from
|
||||
// another present DIMM with the same part number in the same batch.
|
||||
partSize := make(map[string]int)
|
||||
for _, m := range merged {
|
||||
if m.Present && m.SizeMB > 0 && strings.TrimSpace(m.PartNumber) != "" {
|
||||
partSize[strings.TrimSpace(m.PartNumber)] = m.SizeMB
|
||||
}
|
||||
}
|
||||
for i := range merged {
|
||||
if merged[i].Present && merged[i].SizeMB == 0 {
|
||||
if pn := strings.TrimSpace(merged[i].PartNumber); pn != "" {
|
||||
if sz, ok := partSize[pn]; ok {
|
||||
merged[i].SizeMB = sz
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hw.Memory = merged
|
||||
}
|
||||
|
||||
@@ -163,7 +296,7 @@ type PSURESTInfo struct {
|
||||
|
||||
func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
||||
// Find RESTful PSU info section
|
||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return
|
||||
@@ -793,7 +926,7 @@ func parseDiskBackplaneSensors(text string) []models.SensorReading {
|
||||
}
|
||||
|
||||
func parsePSUSummarySensors(text string) []models.SensorReading {
|
||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
|
||||
match := re.FindStringSubmatch(text)
|
||||
if match == nil {
|
||||
return nil
|
||||
@@ -941,7 +1074,7 @@ func extractComponentFirmware(text string, hw *models.HardwareConfig) {
|
||||
// Skip extracting from component.log to avoid duplicates
|
||||
|
||||
// Extract PSU firmware from RESTful PSU info
|
||||
rePSU := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
||||
rePSU := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful (?:PCIE|Network)`)
|
||||
if match := rePSU.FindStringSubmatch(text); match != nil {
|
||||
jsonStr := strings.ReplaceAll(match[1], "\n", "")
|
||||
var psuInfo PSURESTInfo
|
||||
|
||||
83
internal/parser/vendors/inspur/cpu_mem_fix_test.go
vendored
Normal file
83
internal/parser/vendors/inspur/cpu_mem_fix_test.go
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package inspur
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.mchus.pro/mchus/logpile/internal/models"
|
||||
)
|
||||
|
||||
const cpuMemComponentLog = `RESTful version info:
|
||||
[]
|
||||
RESTful CPU info:
|
||||
{ "processors": [ { "proc_id": 0, "PROC_ID": "A6-06-06-00-FF-FB-EB-BF", "InstructionSet": "x86-64", "Manufacturer": "Intel(R) Corporation", "MaxSpeedMHz": 3100, "configStatus": 1, "proc_name": "Intel(R) Xeon(R) Gold 6330 CPU @ 2.00GHz", "proc_status": 1, "proc_speed": 2000, "proc_core_count": 28, "proc_used_core_count": 28, "proc_thread_count": 56, "proc_tdp": 205, "proc_l1cache_size": 80, "proc_l2cache_size": 1280, "proc_l3cache_size": 43008, "micro_code": "0x0D000410", "ppin": "47149E2253E81688", "status": "OK" }, { "proc_id": 1, "PROC_ID": "A6-06-06-00-FF-FB-EB-BF", "InstructionSet": "x86-64", "Manufacturer": "Intel(R) Corporation", "MaxSpeedMHz": 3100, "configStatus": 1, "proc_name": "Intel(R) Xeon(R) Gold 6330 CPU @ 2.00GHz", "proc_status": 1, "proc_speed": 2000, "proc_core_count": 28, "proc_thread_count": 56, "proc_tdp": 205, "proc_l1cache_size": 80, "proc_l2cache_size": 1280, "proc_l3cache_size": 43008, "micro_code": "0x0D000410", "ppin": "475AC1221D41F557", "status": "OK" } ] }
|
||||
RESTful Memory info:
|
||||
{ "mem_modules": [ { "mem_mod_id": 0, "config_status": 1, "mem_mod_slot": "CPU0_C0D0", "mem_mod_status": 1, "mem_mod_size": 32, "mem_mod_type": "DDR4", "mem_mod_technology": "Synchronous", "mem_mod_frequency": 3200, "mem_mod_current_frequency": 2933, "mem_mod_vendor": "Samsung", "mem_mod_part_num": "M393A4K40EB3-CWE", "mem_mod_serial_num": "S1440202433526FC12", "mem_mod_ranks": 2, "status": "OK" }, { "mem_mod_id": 16, "config_status": 1, "mem_mod_slot": "CPU1_C0D0", "mem_mod_status": 1, "mem_mod_size": 0, "mem_mod_type": "DDR4", "mem_mod_technology": "Synchronous", "mem_mod_frequency": 3200, "mem_mod_current_frequency": 2933, "mem_mod_vendor": "Samsung", "mem_mod_part_num": "M393A4K40EB3-CWE", "mem_mod_serial_num": "K0UX000401205D2037", "mem_mod_ranks": 2, "status": "OK" } ], "total_memory_count": 2, "present_memory_count": 2, "mem_total_mem_size": 32 }
|
||||
RESTful HDD info:
|
||||
[]
|
||||
RESTful PSU info:
|
||||
{ "power_supplies": [] }
|
||||
RESTful Network Adapter info:
|
||||
{ "sys_adapters": [] }
|
||||
RESTful fan info:
|
||||
{ "fans": [] }
|
||||
RESTful diskbackplane info:
|
||||
[]
|
||||
BMC done
|
||||
`
|
||||
|
||||
func TestParseCPUInfo_FromComponentLog(t *testing.T) {
|
||||
hw := &models.HardwareConfig{}
|
||||
ParseComponentLog([]byte(cpuMemComponentLog), hw)
|
||||
|
||||
if len(hw.CPUs) != 2 {
|
||||
t.Fatalf("expected 2 CPUs, got %d", len(hw.CPUs))
|
||||
}
|
||||
if !strings.Contains(hw.CPUs[0].Model, "Gold 6330") {
|
||||
t.Errorf("unexpected CPU model: %s", hw.CPUs[0].Model)
|
||||
}
|
||||
if hw.CPUs[0].Cores != 28 {
|
||||
t.Errorf("expected 28 cores, got %d", hw.CPUs[0].Cores)
|
||||
}
|
||||
if hw.CPUs[0].PPIN != "47149E2253E81688" {
|
||||
t.Errorf("unexpected PPIN: %s", hw.CPUs[0].PPIN)
|
||||
}
|
||||
if hw.CPUs[1].PPIN != "475AC1221D41F557" {
|
||||
t.Errorf("unexpected CPU1 PPIN: %s", hw.CPUs[1].PPIN)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMemoryInfo_PresentWithZeroSize(t *testing.T) {
|
||||
hw := &models.HardwareConfig{}
|
||||
ParseComponentLog([]byte(cpuMemComponentLog), hw)
|
||||
|
||||
presentCount := 0
|
||||
for _, m := range hw.Memory {
|
||||
if m.Present {
|
||||
presentCount++
|
||||
}
|
||||
}
|
||||
if presentCount != 2 {
|
||||
t.Errorf("expected 2 present DIMMs, got %d", presentCount)
|
||||
}
|
||||
|
||||
// Find CPU1_C0D0 (size=0 but serial present — size should be inferred from same part number)
|
||||
found := false
|
||||
for _, m := range hw.Memory {
|
||||
if m.Slot == "CPU1_C0D0" {
|
||||
found = true
|
||||
if !m.Present {
|
||||
t.Error("CPU1_C0D0 should be Present=true despite size=0")
|
||||
}
|
||||
if m.SerialNumber != "K0UX000401205D2037" {
|
||||
t.Errorf("wrong serial: %s", m.SerialNumber)
|
||||
}
|
||||
if m.SizeMB != 32768 {
|
||||
t.Errorf("expected SizeMB=32768 inferred from part number, got %d", m.SizeMB)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("CPU1_C0D0 not found in memory list")
|
||||
}
|
||||
}
|
||||
22
internal/parser/vendors/inspur/parser.go
vendored
22
internal/parser/vendors/inspur/parser.go
vendored
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
// parserVersion - version of this parser module
|
||||
// IMPORTANT: Increment this version when making changes to parser logic!
|
||||
const parserVersion = "1.8"
|
||||
const parserVersion = "2.0"
|
||||
|
||||
func init() {
|
||||
parser.Register(&Parser{})
|
||||
@@ -163,6 +163,26 @@ func (p *Parser) Parse(files []parser.ExtractedFile) (*models.AnalysisResult, er
|
||||
// (fan RPM, backplane temperature, PSU summary power, etc.).
|
||||
componentSensors := ParseComponentLogSensors(f.Content)
|
||||
result.Sensors = mergeSensorReadings(result.Sensors, componentSensors)
|
||||
|
||||
// Record sections where BMC itself returned an error instead of data,
|
||||
// and mirror each one into the Events stream so they appear in the log viewer.
|
||||
// Source is set to "BMC/<section>" so the viewer can show the specific module.
|
||||
for _, ce := range ParseComponentLogCollectionErrors(f.Content) {
|
||||
result.CollectionErrors = append(result.CollectionErrors, ce)
|
||||
desc := ce.Message
|
||||
if ce.Code != 0 {
|
||||
desc = fmt.Sprintf("%s (code %d)", ce.Message, ce.Code)
|
||||
}
|
||||
result.Events = append(result.Events, models.Event{
|
||||
ID: fmt.Sprintf("bmc_collection_error_%s", strings.ToLower(strings.ReplaceAll(ce.Section, " ", "_"))),
|
||||
Timestamp: time.Time{}, // no timestamp available
|
||||
Source: fmt.Sprintf("BMC/%s", ce.Section),
|
||||
SensorType: "bmc_collection_error",
|
||||
EventType: "Collection Error",
|
||||
Severity: models.SeverityWarning,
|
||||
Description: desc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Enrich runtime component data from Redis snapshot (serials, FW, telemetry),
|
||||
|
||||
1677
internal/parser/vendors/pciids/pci.ids
vendored
1677
internal/parser/vendors/pciids/pci.ids
vendored
File diff suppressed because it is too large
Load Diff
@@ -854,6 +854,28 @@ func (s *Server) handleGetParseErrors(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// BMC-reported collection failures surfaced by vendor parsers.
|
||||
if result != nil {
|
||||
for _, ce := range result.CollectionErrors {
|
||||
msg := strings.TrimSpace(ce.Message)
|
||||
if msg == "" {
|
||||
continue
|
||||
}
|
||||
detail := ""
|
||||
if ce.Code != 0 {
|
||||
detail = fmt.Sprintf("code %d", ce.Code)
|
||||
}
|
||||
add(parseErrorEntry{
|
||||
Source: "bmc",
|
||||
Category: "bmc_collection_error",
|
||||
Severity: "warning",
|
||||
Path: ce.Section,
|
||||
Message: msg,
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
if items[i].Severity != items[j].Severity {
|
||||
// error > warning > info
|
||||
|
||||
2
third_party/pciids
vendored
2
third_party/pciids
vendored
Submodule third_party/pciids updated: 82b1a68f47...9186887530
@@ -933,3 +933,71 @@ code {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Parse / collection errors panel ───────────────────────────────────── */
|
||||
.parse-errors-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.parse-errors-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: .55rem .75rem;
|
||||
background: var(--warn-bg);
|
||||
border: 1px solid #f0e0c0;
|
||||
border-radius: 6px 6px 0 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
font-size: .85rem;
|
||||
color: var(--warn-fg);
|
||||
}
|
||||
|
||||
.parse-errors-toggle {
|
||||
font-size: .75rem;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.parse-errors-body {
|
||||
border: 1px solid #f0e0c0;
|
||||
border-top: none;
|
||||
border-radius: 0 0 6px 6px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.parse-errors-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: .82rem;
|
||||
}
|
||||
|
||||
.parse-errors-table th {
|
||||
background: var(--surface-3);
|
||||
padding: .4rem .65rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.parse-errors-table td {
|
||||
padding: .38rem .65rem;
|
||||
border-bottom: 1px solid var(--border-lite);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.parse-errors-table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.parse-error-row.parse-error-error td:first-child {
|
||||
color: var(--crit-fg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.parse-error-row.parse-error-warning td:first-child {
|
||||
color: #7a5200;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1413,6 +1413,62 @@ async function loadData(vendor, filename) {
|
||||
document.getElementById('header-log-meta').classList.remove('hidden');
|
||||
|
||||
loadAuditViewer();
|
||||
loadParseErrors();
|
||||
}
|
||||
|
||||
async function loadParseErrors() {
|
||||
const section = document.getElementById('parse-errors-section');
|
||||
const rows = document.getElementById('parse-errors-rows');
|
||||
const title = document.getElementById('parse-errors-title');
|
||||
if (!section || !rows) return;
|
||||
|
||||
let data;
|
||||
try {
|
||||
const resp = await fetch('/api/parse-errors');
|
||||
if (!resp.ok) return;
|
||||
data = await resp.json();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = (data && data.items) ? data.items : [];
|
||||
if (items.length === 0) {
|
||||
section.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const errorCount = items.filter(i => i.severity === 'error').length;
|
||||
const warnCount = items.filter(i => i.severity === 'warning').length;
|
||||
const parts = [];
|
||||
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? 's' : ''}`);
|
||||
if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? 's' : ''}`);
|
||||
const otherCount = items.length - errorCount - warnCount;
|
||||
if (otherCount > 0) parts.push(`${otherCount} notice${otherCount > 1 ? 's' : ''}`);
|
||||
title.textContent = `Collection diagnostics — ${parts.join(', ')}`;
|
||||
|
||||
rows.innerHTML = '';
|
||||
for (const item of items) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = `parse-error-row parse-error-${item.severity || 'info'}`;
|
||||
tr.innerHTML =
|
||||
`<td>${escapeHtml(item.source || '')}</td>` +
|
||||
`<td>${escapeHtml(item.path || item.category || '')}</td>` +
|
||||
`<td>${escapeHtml(item.message || '')}</td>` +
|
||||
`<td>${escapeHtml(item.detail || '')}</td>`;
|
||||
rows.appendChild(tr);
|
||||
}
|
||||
|
||||
section.classList.remove('hidden');
|
||||
}
|
||||
|
||||
let parseErrorsCollapsed = false;
|
||||
function toggleParseErrors() {
|
||||
const body = document.getElementById('parse-errors-body');
|
||||
const toggle = document.getElementById('parse-errors-toggle');
|
||||
if (!body) return;
|
||||
parseErrorsCollapsed = !parseErrorsCollapsed;
|
||||
body.style.display = parseErrorsCollapsed ? 'none' : '';
|
||||
toggle.textContent = parseErrorsCollapsed ? '▼' : '▲';
|
||||
}
|
||||
|
||||
function loadAuditViewer() {
|
||||
@@ -1468,6 +1524,15 @@ async function clearData() {
|
||||
if (frame) {
|
||||
frame.src = 'about:blank';
|
||||
}
|
||||
const parseErrSection = document.getElementById('parse-errors-section');
|
||||
if (parseErrSection) parseErrSection.classList.add('hidden');
|
||||
const parseErrRows = document.getElementById('parse-errors-rows');
|
||||
if (parseErrRows) parseErrRows.innerHTML = '';
|
||||
parseErrorsCollapsed = false;
|
||||
const parseErrBody = document.getElementById('parse-errors-body');
|
||||
if (parseErrBody) parseErrBody.style.display = '';
|
||||
const parseErrToggle = document.getElementById('parse-errors-toggle');
|
||||
if (parseErrToggle) parseErrToggle.textContent = '▲';
|
||||
} catch (err) {
|
||||
console.error('Failed to clear data:', err);
|
||||
}
|
||||
|
||||
@@ -170,6 +170,25 @@
|
||||
</iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="parse-errors-section" class="parse-errors-section hidden">
|
||||
<div class="parse-errors-header" onclick="toggleParseErrors()">
|
||||
<span id="parse-errors-title">Collection warnings</span>
|
||||
<span id="parse-errors-toggle" class="parse-errors-toggle">▲</span>
|
||||
</div>
|
||||
<div id="parse-errors-body" class="parse-errors-body">
|
||||
<table class="parse-errors-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Section</th>
|
||||
<th>Message</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="parse-errors-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user