Add detailed hardware configuration view with sub-tabs
- Redesign config page with tabs: Spec, CPU, Memory, Power, Storage, GPU, Network, PCIe - Parse detailed memory info from component.log with all fields: Location, Present, Size, Type, Max/Current Speed, Manufacturer, Part Number, Status - Add GPU model extraction from PCIe devices - Add NetworkAdapter model with detailed fields from RESTful API - Update PSU model with power metrics (input/output power, voltage, temperature) - Memory modules with 0GB size (failed) highlighted in warning color - Add memory overview stats (total GB, installed count, active count) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -159,7 +159,7 @@ func (e *Exporter) ExportTXT(w io.Writer) error {
|
|||||||
totalMB += mem.SizeMB
|
totalMB += mem.SizeMB
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, " Total: %d GB (%d DIMMs)\n", totalMB/1024, len(hw.Memory))
|
fmt.Fprintf(w, " Total: %d GB (%d DIMMs)\n", totalMB/1024, len(hw.Memory))
|
||||||
fmt.Fprintf(w, " Type: %s @ %d MHz\n", hw.Memory[0].Type, hw.Memory[0].SpeedMHz)
|
fmt.Fprintf(w, " Type: %s @ %d MHz\n", hw.Memory[0].Type, hw.Memory[0].CurrentSpeedMHz)
|
||||||
fmt.Fprintf(w, " Manufacturer: %s\n", hw.Memory[0].Manufacturer)
|
fmt.Fprintf(w, " Manufacturer: %s\n", hw.Memory[0].Manufacturer)
|
||||||
fmt.Fprintln(w)
|
fmt.Fprintln(w)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,14 +59,16 @@ type FRUInfo struct {
|
|||||||
|
|
||||||
// HardwareConfig represents server hardware configuration
|
// HardwareConfig represents server hardware configuration
|
||||||
type HardwareConfig struct {
|
type HardwareConfig struct {
|
||||||
Firmware []FirmwareInfo `json:"firmware,omitempty"`
|
Firmware []FirmwareInfo `json:"firmware,omitempty"`
|
||||||
BoardInfo BoardInfo `json:"board,omitempty"`
|
BoardInfo BoardInfo `json:"board,omitempty"`
|
||||||
CPUs []CPU `json:"cpus,omitempty"`
|
CPUs []CPU `json:"cpus,omitempty"`
|
||||||
Memory []MemoryDIMM `json:"memory,omitempty"`
|
Memory []MemoryDIMM `json:"memory,omitempty"`
|
||||||
Storage []Storage `json:"storage,omitempty"`
|
Storage []Storage `json:"storage,omitempty"`
|
||||||
PCIeDevices []PCIeDevice `json:"pcie_devices,omitempty"`
|
PCIeDevices []PCIeDevice `json:"pcie_devices,omitempty"`
|
||||||
NetworkCards []NIC `json:"network_cards,omitempty"`
|
GPUs []GPU `json:"gpus,omitempty"`
|
||||||
PowerSupply []PSU `json:"power_supplies,omitempty"`
|
NetworkCards []NIC `json:"network_cards,omitempty"`
|
||||||
|
NetworkAdapters []NetworkAdapter `json:"network_adapters,omitempty"`
|
||||||
|
PowerSupply []PSU `json:"power_supplies,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FirmwareInfo represents firmware version information
|
// FirmwareInfo represents firmware version information
|
||||||
@@ -102,13 +104,19 @@ type CPU struct {
|
|||||||
|
|
||||||
// MemoryDIMM represents a memory module
|
// MemoryDIMM represents a memory module
|
||||||
type MemoryDIMM struct {
|
type MemoryDIMM struct {
|
||||||
Slot int `json:"slot"`
|
Slot string `json:"slot"`
|
||||||
SizeMB int `json:"size_mb"`
|
Location string `json:"location"`
|
||||||
Type string `json:"type"`
|
Present bool `json:"present"`
|
||||||
SpeedMHz int `json:"speed_mhz"`
|
SizeMB int `json:"size_mb"`
|
||||||
Manufacturer string `json:"manufacturer,omitempty"`
|
Type string `json:"type"`
|
||||||
SerialNumber string `json:"serial_number,omitempty"`
|
Technology string `json:"technology,omitempty"`
|
||||||
PartNumber string `json:"part_number,omitempty"`
|
MaxSpeedMHz int `json:"max_speed_mhz"`
|
||||||
|
CurrentSpeedMHz int `json:"current_speed_mhz"`
|
||||||
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
|
PartNumber string `json:"part_number,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
Ranks int `json:"ranks,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage represents a storage device
|
// Storage represents a storage device
|
||||||
@@ -151,9 +159,51 @@ type NIC struct {
|
|||||||
|
|
||||||
// PSU represents a power supply unit
|
// PSU represents a power supply unit
|
||||||
type PSU struct {
|
type PSU struct {
|
||||||
|
Slot string `json:"slot"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Vendor string `json:"vendor,omitempty"`
|
||||||
|
WattageW int `json:"wattage_w,omitempty"`
|
||||||
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
|
PartNumber string `json:"part_number,omitempty"`
|
||||||
|
Firmware string `json:"firmware,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
InputType string `json:"input_type,omitempty"`
|
||||||
|
InputPowerW int `json:"input_power_w,omitempty"`
|
||||||
|
OutputPowerW int `json:"output_power_w,omitempty"`
|
||||||
|
InputVoltage float64 `json:"input_voltage,omitempty"`
|
||||||
|
OutputVoltage float64 `json:"output_voltage,omitempty"`
|
||||||
|
TemperatureC int `json:"temperature_c,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPU represents a graphics processing unit
|
||||||
|
type GPU struct {
|
||||||
Slot string `json:"slot"`
|
Slot string `json:"slot"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
WattageW int `json:"wattage_w,omitempty"`
|
Manufacturer string `json:"manufacturer,omitempty"`
|
||||||
|
VendorID int `json:"vendor_id,omitempty"`
|
||||||
|
DeviceID int `json:"device_id,omitempty"`
|
||||||
|
BDF string `json:"bdf,omitempty"`
|
||||||
SerialNumber string `json:"serial_number,omitempty"`
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
Status string `json:"status,omitempty"`
|
PartNumber string `json:"part_number,omitempty"`
|
||||||
|
LinkWidth int `json:"link_width,omitempty"`
|
||||||
|
LinkSpeed string `json:"link_speed,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkAdapter represents a network adapter with detailed info
|
||||||
|
type NetworkAdapter struct {
|
||||||
|
Slot string `json:"slot"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
Present bool `json:"present"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Vendor string `json:"vendor,omitempty"`
|
||||||
|
VendorID int `json:"vendor_id,omitempty"`
|
||||||
|
DeviceID int `json:"device_id,omitempty"`
|
||||||
|
SerialNumber string `json:"serial_number,omitempty"`
|
||||||
|
PartNumber string `json:"part_number,omitempty"`
|
||||||
|
Firmware string `json:"firmware,omitempty"`
|
||||||
|
PortCount int `json:"port_count,omitempty"`
|
||||||
|
PortType string `json:"port_type,omitempty"`
|
||||||
|
MACAddresses []string `json:"mac_addresses,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
47
internal/parser/vendors/inspur/asset.go
vendored
47
internal/parser/vendors/inspur/asset.go
vendored
@@ -125,18 +125,24 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse memory info
|
// Memory info is parsed from component.log (RESTful Memory info) which has more details
|
||||||
|
// Only use asset.json memory data as fallback if component.log is not available
|
||||||
if len(asset.MemInfo.MemCommonInfo) > 0 {
|
if len(asset.MemInfo.MemCommonInfo) > 0 {
|
||||||
common := asset.MemInfo.MemCommonInfo[0]
|
common := asset.MemInfo.MemCommonInfo[0]
|
||||||
for i, dimm := range asset.MemInfo.DimmInfo {
|
for i, dimm := range asset.MemInfo.DimmInfo {
|
||||||
|
slot := fmt.Sprintf("DIMM%d", i)
|
||||||
config.Memory = append(config.Memory, models.MemoryDIMM{
|
config.Memory = append(config.Memory, models.MemoryDIMM{
|
||||||
Slot: i,
|
Slot: slot,
|
||||||
SizeMB: common.PhysicalSize * 1024,
|
Location: slot,
|
||||||
Type: memoryTypeToString(common.MemoryType),
|
Present: true,
|
||||||
SpeedMHz: common.CurrentSpeed,
|
SizeMB: common.PhysicalSize * 1024,
|
||||||
Manufacturer: common.Manufacturer,
|
Type: memoryTypeToString(common.MemoryType),
|
||||||
SerialNumber: dimm.SerialNumber,
|
MaxSpeedMHz: common.MaxSpeed,
|
||||||
PartNumber: strings.TrimSpace(dimm.PartNumber),
|
CurrentSpeedMHz: common.CurrentSpeed,
|
||||||
|
Manufacturer: common.Manufacturer,
|
||||||
|
SerialNumber: dimm.SerialNumber,
|
||||||
|
PartNumber: strings.TrimSpace(dimm.PartNumber),
|
||||||
|
Ranks: common.Rank,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,6 +201,31 @@ func ParseAssetJSON(content []byte) (*models.HardwareConfig, error) {
|
|||||||
device.DeviceClass = deviceName
|
device.DeviceClass = deviceName
|
||||||
}
|
}
|
||||||
config.PCIeDevices = append(config.PCIeDevices, device)
|
config.PCIeDevices = append(config.PCIeDevices, device)
|
||||||
|
|
||||||
|
// Extract GPUs (class 3 = display controller)
|
||||||
|
if pcie.ClassCode == 3 {
|
||||||
|
gpuModel := deviceName
|
||||||
|
if gpuModel == "" {
|
||||||
|
gpuModel = pcieClassToString(pcie.ClassCode, pcie.SubClassCode)
|
||||||
|
}
|
||||||
|
gpu := models.GPU{
|
||||||
|
Slot: pcie.LocString,
|
||||||
|
Model: gpuModel,
|
||||||
|
Manufacturer: vendor,
|
||||||
|
VendorID: pcie.VendorId,
|
||||||
|
DeviceID: pcie.DeviceId,
|
||||||
|
BDF: formatBDF(pcie.BusNumber, pcie.DeviceNumber, pcie.FunctionNumber),
|
||||||
|
LinkWidth: pcie.NegotiatedLinkWidth,
|
||||||
|
LinkSpeed: pcieLinkSpeedToString(pcie.CurrentLinkSpeed),
|
||||||
|
}
|
||||||
|
if pcie.PartNumber != nil {
|
||||||
|
gpu.PartNumber = strings.TrimSpace(*pcie.PartNumber)
|
||||||
|
}
|
||||||
|
if pcie.SerialNumber != nil {
|
||||||
|
gpu.SerialNumber = strings.TrimSpace(*pcie.SerialNumber)
|
||||||
|
}
|
||||||
|
config.GPUs = append(config.GPUs, gpu)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
|
|||||||
300
internal/parser/vendors/inspur/component.go
vendored
300
internal/parser/vendors/inspur/component.go
vendored
@@ -10,7 +10,7 @@ import (
|
|||||||
"git.mchus.pro/mchus/logpile/internal/models"
|
"git.mchus.pro/mchus/logpile/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseComponentLog parses component.log file and extracts PSU and other info
|
// ParseComponentLog parses component.log file and extracts detailed hardware info
|
||||||
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
||||||
if hw == nil {
|
if hw == nil {
|
||||||
return
|
return
|
||||||
@@ -18,8 +18,17 @@ func ParseComponentLog(content []byte, hw *models.HardwareConfig) {
|
|||||||
|
|
||||||
text := string(content)
|
text := string(content)
|
||||||
|
|
||||||
|
// Parse RESTful Memory info (detailed memory data)
|
||||||
|
parseMemoryInfo(text, hw)
|
||||||
|
|
||||||
// Parse RESTful PSU info
|
// Parse RESTful PSU info
|
||||||
parsePSUInfo(text, hw)
|
parsePSUInfo(text, hw)
|
||||||
|
|
||||||
|
// Parse RESTful HDD info
|
||||||
|
parseHDDInfo(text, hw)
|
||||||
|
|
||||||
|
// Parse RESTful Network Adapter info
|
||||||
|
parseNetworkAdapterInfo(text, hw)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
|
// ParseComponentLogEvents extracts events from component.log (memory errors, etc.)
|
||||||
@@ -34,33 +43,99 @@ func ParseComponentLogEvents(content []byte) []models.Event {
|
|||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
// PSUInfo represents the RESTful PSU info structure
|
// MemoryRESTInfo represents the RESTful Memory info structure
|
||||||
type PSUInfo struct {
|
type MemoryRESTInfo struct {
|
||||||
PowerSupplies []struct {
|
MemModules []struct {
|
||||||
ID int `json:"id"`
|
MemModID int `json:"mem_mod_id"`
|
||||||
Present int `json:"present"`
|
ConfigStatus int `json:"config_status"`
|
||||||
VendorID string `json:"vendor_id"`
|
MemModSlot string `json:"mem_mod_slot"`
|
||||||
Model string `json:"model"`
|
MemModSize int `json:"mem_mod_size"`
|
||||||
SerialNum string `json:"serial_num"`
|
MemModType string `json:"mem_mod_type"`
|
||||||
FwVer string `json:"fw_ver"`
|
MemModTechnology string `json:"mem_mod_technology"`
|
||||||
RatedPower int `json:"rated_power"`
|
MemModFrequency int `json:"mem_mod_frequency"`
|
||||||
Status string `json:"status"`
|
MemModCurrentFreq int `json:"mem_mod_current_frequency"`
|
||||||
} `json:"power_supplies"`
|
MemModVendor string `json:"mem_mod_vendor"`
|
||||||
|
MemModPartNum string `json:"mem_mod_part_num"`
|
||||||
|
MemModSerial string `json:"mem_mod_serial_num"`
|
||||||
|
MemModRanks int `json:"mem_mod_ranks"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
} `json:"mem_modules"`
|
||||||
|
TotalMemoryCount int `json:"total_memory_count"`
|
||||||
|
PresentMemoryCount int `json:"present_memory_count"`
|
||||||
|
MemTotalMemSize int `json:"mem_total_mem_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
func parseMemoryInfo(text string, hw *models.HardwareConfig) {
|
||||||
// Find RESTful PSU info section
|
// Find RESTful Memory info section
|
||||||
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*(?:RESTful|BMC|$)`)
|
re := regexp.MustCompile(`RESTful Memory info:\s*(\{[\s\S]*?\})\s*RESTful HDD`)
|
||||||
match := re.FindStringSubmatch(text)
|
match := re.FindStringSubmatch(text)
|
||||||
if match == nil {
|
if match == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonStr := match[1]
|
jsonStr := match[1]
|
||||||
// Clean up the JSON (it might have newlines)
|
|
||||||
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||||
|
|
||||||
var psuInfo PSUInfo
|
var memInfo MemoryRESTInfo
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace memory data with detailed info from component.log
|
||||||
|
hw.Memory = nil
|
||||||
|
for _, mem := range memInfo.MemModules {
|
||||||
|
hw.Memory = append(hw.Memory, models.MemoryDIMM{
|
||||||
|
Slot: mem.MemModSlot,
|
||||||
|
Location: mem.MemModSlot,
|
||||||
|
Present: mem.ConfigStatus == 1,
|
||||||
|
SizeMB: mem.MemModSize * 1024, // Convert GB to MB
|
||||||
|
Type: mem.MemModType,
|
||||||
|
Technology: strings.TrimSpace(mem.MemModTechnology),
|
||||||
|
MaxSpeedMHz: mem.MemModFrequency,
|
||||||
|
CurrentSpeedMHz: mem.MemModCurrentFreq,
|
||||||
|
Manufacturer: mem.MemModVendor,
|
||||||
|
SerialNumber: mem.MemModSerial,
|
||||||
|
PartNumber: strings.TrimSpace(mem.MemModPartNum),
|
||||||
|
Status: mem.Status,
|
||||||
|
Ranks: mem.MemModRanks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSURESTInfo represents the RESTful PSU info structure
|
||||||
|
type PSURESTInfo struct {
|
||||||
|
PowerSupplies []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Present int `json:"present"`
|
||||||
|
VendorID string `json:"vendor_id"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
SerialNum string `json:"serial_num"`
|
||||||
|
PartNum string `json:"part_num"`
|
||||||
|
FwVer string `json:"fw_ver"`
|
||||||
|
InputType string `json:"input_type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RatedPower int `json:"rated_power"`
|
||||||
|
PSInPower int `json:"ps_in_power"`
|
||||||
|
PSOutPower int `json:"ps_out_power"`
|
||||||
|
PSInVolt float64 `json:"ps_in_volt"`
|
||||||
|
PSOutVolt float64 `json:"ps_out_volt"`
|
||||||
|
PSUMaxTemp int `json:"psu_max_temperature"`
|
||||||
|
} `json:"power_supplies"`
|
||||||
|
PresentPowerReading int `json:"present_power_reading"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
||||||
|
// Find RESTful PSU info section
|
||||||
|
re := regexp.MustCompile(`RESTful PSU info:\s*(\{[\s\S]*?\})\s*RESTful Network`)
|
||||||
|
match := re.FindStringSubmatch(text)
|
||||||
|
if match == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStr := match[1]
|
||||||
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||||
|
|
||||||
|
var psuInfo PSURESTInfo
|
||||||
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
|
if err := json.Unmarshal([]byte(jsonStr), &psuInfo); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -68,34 +143,177 @@ func parsePSUInfo(text string, hw *models.HardwareConfig) {
|
|||||||
// Clear existing PSU data and populate with RESTful data
|
// Clear existing PSU data and populate with RESTful data
|
||||||
hw.PowerSupply = nil
|
hw.PowerSupply = nil
|
||||||
for _, psu := range psuInfo.PowerSupplies {
|
for _, psu := range psuInfo.PowerSupplies {
|
||||||
if psu.Present != 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hw.PowerSupply = append(hw.PowerSupply, models.PSU{
|
hw.PowerSupply = append(hw.PowerSupply, models.PSU{
|
||||||
Slot: formatPSUSlot(psu.ID),
|
Slot: fmt.Sprintf("PSU%d", psu.ID),
|
||||||
Model: psu.Model,
|
Present: psu.Present == 1,
|
||||||
WattageW: psu.RatedPower,
|
Model: strings.TrimSpace(psu.Model),
|
||||||
SerialNumber: psu.SerialNum,
|
Vendor: strings.TrimSpace(psu.VendorID),
|
||||||
Status: psu.Status,
|
WattageW: psu.RatedPower,
|
||||||
|
SerialNumber: strings.TrimSpace(psu.SerialNum),
|
||||||
|
PartNumber: strings.TrimSpace(psu.PartNum),
|
||||||
|
Firmware: psu.FwVer,
|
||||||
|
Status: psu.Status,
|
||||||
|
InputType: psu.InputType,
|
||||||
|
InputPowerW: psu.PSInPower,
|
||||||
|
OutputPowerW: psu.PSOutPower,
|
||||||
|
InputVoltage: psu.PSInVolt,
|
||||||
|
OutputVoltage: psu.PSOutVolt,
|
||||||
|
TemperatureC: psu.PSUMaxTemp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatPSUSlot(id int) string {
|
// HDDRESTInfo represents the RESTful HDD info structure
|
||||||
return fmt.Sprintf("PSU%d", id)
|
type HDDRESTInfo []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Present int `json:"present"`
|
||||||
|
Enable int `json:"enable"`
|
||||||
|
SN string `json:"SN"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Capacity int `json:"capacity"`
|
||||||
|
Manufacture string `json:"manufacture"`
|
||||||
|
Firmware string `json:"firmware"`
|
||||||
|
LocationString string `json:"locationstring"`
|
||||||
|
CapableSpeed int `json:"capablespeed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MemoryInfo represents the RESTful Memory info structure
|
func parseHDDInfo(text string, hw *models.HardwareConfig) {
|
||||||
type MemoryInfo struct {
|
// Find RESTful HDD info section
|
||||||
MemModules []struct {
|
re := regexp.MustCompile(`RESTful HDD info:\s*(\[[\s\S]*?\])\s*RESTful PSU`)
|
||||||
MemModID int `json:"mem_mod_id"`
|
match := re.FindStringSubmatch(text)
|
||||||
MemModSlot string `json:"mem_mod_slot"`
|
if match == nil {
|
||||||
MemModSize int `json:"mem_mod_size"`
|
return
|
||||||
MemModVendor string `json:"mem_mod_vendor"`
|
}
|
||||||
MemModPartNum string `json:"mem_mod_part_num"`
|
|
||||||
MemModSerial string `json:"mem_mod_serial_num"`
|
jsonStr := match[1]
|
||||||
Status string `json:"status"`
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||||
} `json:"mem_modules"`
|
|
||||||
|
var hddInfo HDDRESTInfo
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &hddInfo); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update storage with detailed info (merge with existing data from asset.json)
|
||||||
|
hddMap := make(map[string]struct {
|
||||||
|
SN string
|
||||||
|
Model string
|
||||||
|
Firmware string
|
||||||
|
Mfr string
|
||||||
|
})
|
||||||
|
for _, hdd := range hddInfo {
|
||||||
|
if hdd.Present == 1 {
|
||||||
|
hddMap[hdd.LocationString] = struct {
|
||||||
|
SN string
|
||||||
|
Model string
|
||||||
|
Firmware string
|
||||||
|
Mfr string
|
||||||
|
}{
|
||||||
|
SN: strings.TrimSpace(hdd.SN),
|
||||||
|
Model: strings.TrimSpace(hdd.Model),
|
||||||
|
Firmware: strings.TrimSpace(hdd.Firmware),
|
||||||
|
Mfr: strings.TrimSpace(hdd.Manufacture),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If storage is empty, populate from HDD info
|
||||||
|
if len(hw.Storage) == 0 {
|
||||||
|
for _, hdd := range hddInfo {
|
||||||
|
if hdd.Present != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
storType := "HDD"
|
||||||
|
model := strings.TrimSpace(hdd.Model)
|
||||||
|
if strings.Contains(strings.ToUpper(model), "SSD") || strings.Contains(model, "MZ7") {
|
||||||
|
storType = "SSD"
|
||||||
|
}
|
||||||
|
|
||||||
|
iface := "SATA"
|
||||||
|
if hdd.CapableSpeed == 12 {
|
||||||
|
iface = "SAS"
|
||||||
|
}
|
||||||
|
|
||||||
|
hw.Storage = append(hw.Storage, models.Storage{
|
||||||
|
Slot: hdd.LocationString,
|
||||||
|
Type: storType,
|
||||||
|
Model: model,
|
||||||
|
SizeGB: hdd.Capacity,
|
||||||
|
SerialNumber: strings.TrimSpace(hdd.SN),
|
||||||
|
Manufacturer: extractStorageManufacturer(model),
|
||||||
|
Firmware: strings.TrimSpace(hdd.Firmware),
|
||||||
|
Interface: iface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkAdapterRESTInfo represents the RESTful Network Adapter info structure
|
||||||
|
type NetworkAdapterRESTInfo struct {
|
||||||
|
SysAdapters []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Location string `json:"Location"`
|
||||||
|
Present int `json:"present"`
|
||||||
|
Slot int `json:"slot"`
|
||||||
|
VendorID int `json:"vendor_id"`
|
||||||
|
DeviceID int `json:"device_id"`
|
||||||
|
Vendor string `json:"vendor"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
FwVer string `json:"fw_ver"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
SN string `json:"sn"`
|
||||||
|
PN string `json:"pn"`
|
||||||
|
PortNum int `json:"port_num"`
|
||||||
|
PortType string `json:"port_type"`
|
||||||
|
Ports []struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
MacAddr string `json:"mac_addr"`
|
||||||
|
} `json:"ports"`
|
||||||
|
} `json:"sys_adapters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNetworkAdapterInfo(text string, hw *models.HardwareConfig) {
|
||||||
|
// Find RESTful Network Adapter info section
|
||||||
|
re := regexp.MustCompile(`RESTful Network Adapter info:\s*(\{[\s\S]*?\})\s*RESTful fan`)
|
||||||
|
match := re.FindStringSubmatch(text)
|
||||||
|
if match == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonStr := match[1]
|
||||||
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||||
|
|
||||||
|
var netInfo NetworkAdapterRESTInfo
|
||||||
|
if err := json.Unmarshal([]byte(jsonStr), &netInfo); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hw.NetworkAdapters = nil
|
||||||
|
for _, adapter := range netInfo.SysAdapters {
|
||||||
|
var macs []string
|
||||||
|
for _, port := range adapter.Ports {
|
||||||
|
if port.MacAddr != "" {
|
||||||
|
macs = append(macs, port.MacAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hw.NetworkAdapters = append(hw.NetworkAdapters, models.NetworkAdapter{
|
||||||
|
Slot: fmt.Sprintf("Slot %d", adapter.Slot),
|
||||||
|
Location: adapter.Location,
|
||||||
|
Present: adapter.Present == 1,
|
||||||
|
Model: strings.TrimSpace(adapter.Model),
|
||||||
|
Vendor: strings.TrimSpace(adapter.Vendor),
|
||||||
|
VendorID: adapter.VendorID,
|
||||||
|
DeviceID: adapter.DeviceID,
|
||||||
|
SerialNumber: strings.TrimSpace(adapter.SN),
|
||||||
|
PartNumber: strings.TrimSpace(adapter.PN),
|
||||||
|
Firmware: adapter.FwVer,
|
||||||
|
PortCount: adapter.PortNum,
|
||||||
|
PortType: adapter.PortType,
|
||||||
|
MACAddresses: macs,
|
||||||
|
Status: adapter.Status,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMemoryEvents(text string) []models.Event {
|
func parseMemoryEvents(text string) []models.Event {
|
||||||
@@ -111,7 +329,7 @@ func parseMemoryEvents(text string) []models.Event {
|
|||||||
jsonStr := match[1]
|
jsonStr := match[1]
|
||||||
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
jsonStr = strings.ReplaceAll(jsonStr, "\n", "")
|
||||||
|
|
||||||
var memInfo MemoryInfo
|
var memInfo MemoryRESTInfo
|
||||||
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
|
if err := json.Unmarshal([]byte(jsonStr), &memInfo); err != nil {
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
@@ -131,7 +349,7 @@ func parseMemoryEvents(text string) []models.Event {
|
|||||||
|
|
||||||
events = append(events, models.Event{
|
events = append(events, models.Event{
|
||||||
ID: fmt.Sprintf("mem_%d", mem.MemModID),
|
ID: fmt.Sprintf("mem_%d", mem.MemModID),
|
||||||
Timestamp: time.Now(), // No timestamp in source, use current
|
Timestamp: time.Now(),
|
||||||
Source: "Memory",
|
Source: "Memory",
|
||||||
SensorType: "memory",
|
SensorType: "memory",
|
||||||
SensorName: mem.MemModSlot,
|
SensorName: mem.MemModSlot,
|
||||||
|
|||||||
@@ -389,6 +389,130 @@ footer {
|
|||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Configuration tabs */
|
||||||
|
.config-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: #f0f0f0;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-tab {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-tab:hover {
|
||||||
|
color: #333;
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-tab.active {
|
||||||
|
color: white;
|
||||||
|
background: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-tab-content h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Config tables */
|
||||||
|
.config-table {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table th {
|
||||||
|
background: #2c3e50;
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table td {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table tbody tr:nth-child(even) {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table tbody tr:hover {
|
||||||
|
background: #e8f4fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Memory table specific */
|
||||||
|
.memory-table .row-warning {
|
||||||
|
background: #fff3cd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-table .row-warning:hover {
|
||||||
|
background: #ffe8a1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-table .present-yes {
|
||||||
|
color: #27ae60;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-table .present-no {
|
||||||
|
color: #95a5a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-table .status-warning {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Memory overview stats */
|
||||||
|
.memory-overview {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
border-left: 4px solid #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sensor-grid {
|
.sensor-grid {
|
||||||
@@ -402,4 +526,25 @@ footer {
|
|||||||
th, td {
|
th, td {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-tabs {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-tab {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-overview {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table th,
|
||||||
|
.config-table td {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,72 +140,222 @@ function renderConfig(data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle both old format (direct hardware) and new format (with specification)
|
|
||||||
const config = data.hardware || data;
|
const config = data.hardware || data;
|
||||||
const spec = data.specification;
|
const spec = data.specification;
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
// Specification summary (new section)
|
// Configuration sub-tabs
|
||||||
|
html += `<div class="config-tabs">
|
||||||
|
<button class="config-tab active" data-config-tab="spec">Спецификация</button>
|
||||||
|
<button class="config-tab" data-config-tab="cpu">CPU</button>
|
||||||
|
<button class="config-tab" data-config-tab="memory">Memory</button>
|
||||||
|
<button class="config-tab" data-config-tab="power">Power</button>
|
||||||
|
<button class="config-tab" data-config-tab="storage">Hard Drive</button>
|
||||||
|
<button class="config-tab" data-config-tab="gpu">GPU</button>
|
||||||
|
<button class="config-tab" data-config-tab="network">Network</button>
|
||||||
|
<button class="config-tab" data-config-tab="pcie">Device Inventory</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Specification tab
|
||||||
|
html += '<div class="config-tab-content active" id="config-spec">';
|
||||||
if (spec && spec.length > 0) {
|
if (spec && spec.length > 0) {
|
||||||
html += '<div class="config-section spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
|
html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
|
||||||
spec.forEach(item => {
|
spec.forEach(item => {
|
||||||
html += `<li><span class="spec-category">${escapeHtml(item.category)}</span> ${escapeHtml(item.name)} <span class="spec-qty">- ${item.quantity} шт.</span></li>`;
|
html += `<li><span class="spec-category">${escapeHtml(item.category)}</span> ${escapeHtml(item.name)} <span class="spec-qty">- ${item.quantity} шт.</span></li>`;
|
||||||
});
|
});
|
||||||
html += '</ul></div>';
|
html += '</ul></div>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет данных о спецификации</p>';
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
// CPUs
|
// CPU tab
|
||||||
|
html += '<div class="config-tab-content" id="config-cpu">';
|
||||||
if (config.cpus && config.cpus.length > 0) {
|
if (config.cpus && config.cpus.length > 0) {
|
||||||
html += '<div class="config-section"><h3>Процессоры</h3>';
|
html += '<h3>Процессоры</h3><table class="config-table"><thead><tr><th>Socket</th><th>Модель</th><th>Ядра</th><th>Потоки</th><th>Частота</th><th>Max Turbo</th><th>TDP</th><th>L3 Cache</th><th>PPIN</th></tr></thead><tbody>';
|
||||||
config.cpus.forEach(cpu => {
|
config.cpus.forEach(cpu => {
|
||||||
html += `<div class="card">
|
html += `<tr>
|
||||||
<strong>Socket ${cpu.socket}: ${escapeHtml(cpu.model)}</strong><br>
|
<td>CPU${cpu.socket}</td>
|
||||||
Ядра: ${cpu.cores}, Потоки: ${cpu.threads}<br>
|
<td>${escapeHtml(cpu.model)}</td>
|
||||||
Частота: ${cpu.frequency_mhz} MHz (Turbo: ${cpu.max_frequency_mhz} MHz)<br>
|
<td>${cpu.cores}</td>
|
||||||
TDP: ${cpu.tdp_w}W, L3: ${Math.round(cpu.l3_cache_kb/1024)} MB
|
<td>${cpu.threads}</td>
|
||||||
</div>`;
|
<td>${cpu.frequency_mhz} MHz</td>
|
||||||
|
<td>${cpu.max_frequency_mhz} MHz</td>
|
||||||
|
<td>${cpu.tdp_w}W</td>
|
||||||
|
<td>${Math.round(cpu.l3_cache_kb/1024)} MB</td>
|
||||||
|
<td><code>${escapeHtml(cpu.ppin || '-')}</code></td>
|
||||||
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет данных о процессорах</p>';
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
// Memory summary
|
// Memory tab
|
||||||
|
html += '<div class="config-tab-content" id="config-memory">';
|
||||||
if (config.memory && config.memory.length > 0) {
|
if (config.memory && config.memory.length > 0) {
|
||||||
const totalGB = config.memory.reduce((sum, m) => sum + m.size_mb, 0) / 1024;
|
const totalGB = config.memory.reduce((sum, m) => sum + m.size_mb, 0) / 1024;
|
||||||
html += `<div class="config-section"><h3>Память</h3>
|
const presentCount = config.memory.filter(m => m.present !== false).length;
|
||||||
<p>Всего: ${totalGB} GB (${config.memory.length} модулей ${config.memory[0].type} @ ${config.memory[0].speed_mhz} MHz)</p>
|
const workingCount = config.memory.filter(m => m.size_mb > 0).length;
|
||||||
<p>Производитель: ${escapeHtml(config.memory[0].manufacturer)}</p>
|
html += `<h3>Модули памяти</h3>
|
||||||
</div>`;
|
<div class="memory-overview">
|
||||||
|
<div class="stat-box"><span class="stat-value">${totalGB} GB</span><span class="stat-label">Всего</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${presentCount}</span><span class="stat-label">Установлено</span></div>
|
||||||
|
<div class="stat-box"><span class="stat-value">${workingCount}</span><span class="stat-label">Активно</span></div>
|
||||||
|
</div>
|
||||||
|
<table class="config-table memory-table"><thead><tr>
|
||||||
|
<th>Location</th><th>Наличие</th><th>Размер</th><th>Тип</th><th>Max частота</th><th>Текущая частота</th><th>Производитель</th><th>Артикул</th><th>Статус</th>
|
||||||
|
</tr></thead><tbody>`;
|
||||||
|
config.memory.forEach(mem => {
|
||||||
|
const present = mem.present !== false ? '✓' : '-';
|
||||||
|
const presentClass = mem.present !== false ? 'present-yes' : 'present-no';
|
||||||
|
const sizeGB = mem.size_mb / 1024;
|
||||||
|
const statusClass = (mem.status === 'OK' || !mem.status) ? '' : 'status-warning';
|
||||||
|
const rowClass = sizeGB === 0 ? 'row-warning' : '';
|
||||||
|
html += `<tr class="${rowClass}">
|
||||||
|
<td>${escapeHtml(mem.location || mem.slot)}</td>
|
||||||
|
<td class="${presentClass}">${present}</td>
|
||||||
|
<td>${sizeGB} GB</td>
|
||||||
|
<td>${escapeHtml(mem.type || '-')}</td>
|
||||||
|
<td>${mem.max_speed_mhz || '-'} MHz</td>
|
||||||
|
<td>${mem.current_speed_mhz || mem.speed_mhz || '-'} MHz</td>
|
||||||
|
<td>${escapeHtml(mem.manufacturer || '-')}</td>
|
||||||
|
<td><code>${escapeHtml(mem.part_number || '-')}</code></td>
|
||||||
|
<td class="${statusClass}">${escapeHtml(mem.status || 'OK')}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет данных о памяти</p>';
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
// Storage summary
|
// Power tab
|
||||||
|
html += '<div class="config-tab-content" id="config-power">';
|
||||||
|
if (config.power_supplies && config.power_supplies.length > 0) {
|
||||||
|
html += '<h3>Блоки питания</h3><table class="config-table"><thead><tr><th>Слот</th><th>Производитель</th><th>Модель</th><th>Мощность</th><th>Вход</th><th>Выход</th><th>Напряжение</th><th>Температура</th><th>Статус</th></tr></thead><tbody>';
|
||||||
|
config.power_supplies.forEach(psu => {
|
||||||
|
const statusClass = psu.status === 'OK' ? '' : 'status-warning';
|
||||||
|
html += `<tr>
|
||||||
|
<td>${escapeHtml(psu.slot)}</td>
|
||||||
|
<td>${escapeHtml(psu.vendor || '-')}</td>
|
||||||
|
<td>${escapeHtml(psu.model || '-')}</td>
|
||||||
|
<td>${psu.wattage_w || '-'}W</td>
|
||||||
|
<td>${psu.input_power_w || '-'}W</td>
|
||||||
|
<td>${psu.output_power_w || '-'}W</td>
|
||||||
|
<td>${psu.input_voltage ? psu.input_voltage.toFixed(0) : '-'}V</td>
|
||||||
|
<td>${psu.temperature_c || '-'}°C</td>
|
||||||
|
<td class="${statusClass}">${escapeHtml(psu.status || '-')}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет данных о блоках питания</p>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Storage tab
|
||||||
|
html += '<div class="config-tab-content" id="config-storage">';
|
||||||
if (config.storage && config.storage.length > 0) {
|
if (config.storage && config.storage.length > 0) {
|
||||||
html += '<div class="config-section"><h3>Накопители</h3><table><thead><tr><th>Слот</th><th>Модель</th><th>Размер</th></tr></thead><tbody>';
|
html += '<h3>Накопители</h3><table class="config-table"><thead><tr><th>Слот</th><th>Тип</th><th>Интерфейс</th><th>Модель</th><th>Производитель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>';
|
||||||
config.storage.forEach(s => {
|
config.storage.forEach(s => {
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(s.slot || s.interface)}</td>
|
<td>${escapeHtml(s.slot || '-')}</td>
|
||||||
<td>${escapeHtml(s.model)}</td>
|
<td>${escapeHtml(s.type || '-')}</td>
|
||||||
|
<td>${escapeHtml(s.interface || '-')}</td>
|
||||||
|
<td>${escapeHtml(s.model || '-')}</td>
|
||||||
|
<td>${escapeHtml(s.manufacturer || '-')}</td>
|
||||||
<td>${s.size_gb} GB</td>
|
<td>${s.size_gb} GB</td>
|
||||||
|
<td><code>${escapeHtml(s.serial_number || '-')}</code></td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += '</tbody></table></div>';
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет данных о накопителях</p>';
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
// PCIe summary
|
// GPU tab
|
||||||
|
html += '<div class="config-tab-content" id="config-gpu">';
|
||||||
|
if (config.gpus && config.gpus.length > 0) {
|
||||||
|
html += '<h3>Графические процессоры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>';
|
||||||
|
config.gpus.forEach(gpu => {
|
||||||
|
html += `<tr>
|
||||||
|
<td>${escapeHtml(gpu.slot || '-')}</td>
|
||||||
|
<td>${escapeHtml(gpu.model || '-')}</td>
|
||||||
|
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
|
||||||
|
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
|
||||||
|
<td>x${gpu.link_width || '-'} ${escapeHtml(gpu.link_speed || '-')}</td>
|
||||||
|
<td><code>${escapeHtml(gpu.serial_number || '-')}</code></td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет GPU</p>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Network tab
|
||||||
|
html += '<div class="config-tab-content" id="config-network">';
|
||||||
|
if (config.network_adapters && config.network_adapters.length > 0) {
|
||||||
|
html += '<h3>Сетевые адаптеры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>';
|
||||||
|
config.network_adapters.forEach(nic => {
|
||||||
|
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
|
||||||
|
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
|
||||||
|
html += `<tr>
|
||||||
|
<td>${escapeHtml(nic.location || nic.slot || '-')}</td>
|
||||||
|
<td>${escapeHtml(nic.model || '-')}</td>
|
||||||
|
<td>${escapeHtml(nic.vendor || '-')}</td>
|
||||||
|
<td>${nic.port_count || '-'}</td>
|
||||||
|
<td>${escapeHtml(nic.port_type || '-')}</td>
|
||||||
|
<td><code>${escapeHtml(macs)}</code></td>
|
||||||
|
<td class="${statusClass}">${escapeHtml(nic.status || '-')}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет данных о сетевых адаптерах</p>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// PCIe Device Inventory tab
|
||||||
|
html += '<div class="config-tab-content" id="config-pcie">';
|
||||||
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
||||||
html += '<div class="config-section"><h3>PCIe устройства</h3><table><thead><tr><th>Слот</th><th>Тип</th><th>Производитель</th><th>Скорость</th></tr></thead><tbody>';
|
html += '<h3>PCIe устройства</h3><table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Тип</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th></tr></thead><tbody>';
|
||||||
config.pcie_devices.forEach(p => {
|
config.pcie_devices.forEach(p => {
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(p.slot)}</td>
|
<td>${escapeHtml(p.slot || '-')}</td>
|
||||||
<td>${escapeHtml(p.device_class)}</td>
|
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
||||||
|
<td>${escapeHtml(p.device_class || '-')}</td>
|
||||||
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
||||||
<td>x${p.link_width} ${escapeHtml(p.link_speed)}</td>
|
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
||||||
|
<td>x${p.link_width || '-'} ${escapeHtml(p.link_speed || '-')}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
html += '</tbody></table></div>';
|
html += '</tbody></table>';
|
||||||
|
} else {
|
||||||
|
html += '<p class="no-data">Нет данных о PCIe устройствах</p>';
|
||||||
}
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Initialize config sub-tabs
|
||||||
|
initConfigTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initConfigTabs() {
|
||||||
|
const tabs = document.querySelectorAll('.config-tab');
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.config-tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
document.getElementById('config-' + tab.dataset.configTab).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFirmware() {
|
async function loadFirmware() {
|
||||||
|
|||||||
Reference in New Issue
Block a user