Introduce canonical hardware.devices repository and align UI/Reanimator exports
This commit is contained in:
@@ -646,6 +646,22 @@ function renderConfig(data) {
|
||||
|
||||
const config = data.hardware || data;
|
||||
const spec = data.specification;
|
||||
const devices = Array.isArray(config.devices) ? config.devices : [];
|
||||
|
||||
const cpus = devices.filter(d => d.kind === 'cpu');
|
||||
const memory = devices.filter(d => d.kind === 'memory');
|
||||
const powerSupplies = devices.filter(d => d.kind === 'psu');
|
||||
const storage = devices.filter(d => d.kind === 'storage');
|
||||
const gpus = devices.filter(d => d.kind === 'gpu');
|
||||
const networkAdapters = devices.filter(d => d.kind === 'network');
|
||||
const inventoryRows = devices.filter(d => ['pcie', 'storage', 'gpu', 'network'].includes(d.kind));
|
||||
const pcieBalance = calculateCPUToPCIeBalance(inventoryRows, cpus);
|
||||
const pcieByCPU = new Map();
|
||||
pcieBalance.perCPU.forEach(item => {
|
||||
const idx = extractCPUIndex(item.label);
|
||||
if (idx !== null) pcieByCPU.set(idx, item.lanes);
|
||||
});
|
||||
const memoryByCPU = calculateMemoryModulesByCPU(memory);
|
||||
|
||||
let html = '';
|
||||
|
||||
@@ -684,30 +700,56 @@ function renderConfig(data) {
|
||||
|
||||
// CPU tab
|
||||
html += '<div class="config-tab-content" id="config-cpu">';
|
||||
if (config.cpus && config.cpus.length > 0) {
|
||||
const cpuCount = config.cpus.length;
|
||||
const cpuModel = config.cpus[0].model || '-';
|
||||
const totalCores = config.cpus.reduce((sum, c) => sum + (c.cores || 0), 0);
|
||||
const totalThreads = config.cpus.reduce((sum, c) => sum + (c.threads || 0), 0);
|
||||
if (cpus.length > 0) {
|
||||
const cpuCount = cpus.length;
|
||||
const cpuModel = cpus[0].model || '-';
|
||||
const totalCores = cpus.reduce((sum, c) => sum + (c.cores || 0), 0);
|
||||
const totalThreads = cpus.reduce((sum, c) => sum + (c.threads || 0), 0);
|
||||
const balanceClass = pcieBalance.severity === 'critical'
|
||||
? 'pcie-balance-critical'
|
||||
: (pcieBalance.severity === 'warning' ? 'pcie-balance-warning' : 'pcie-balance-ok');
|
||||
const balanceLabel = pcieBalance.severity === 'critical'
|
||||
? 'Перевес высокий'
|
||||
: (pcieBalance.severity === 'warning' ? 'Есть перевес' : 'Распределено ровно');
|
||||
html += `<h3>Процессоры</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${cpuCount}</span><span class="stat-label">Процессоров</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${totalCores}</span><span class="stat-label">Ядер</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${totalThreads}</span><span class="stat-label">Потоков</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${pcieBalance.totalLanes}</span><span class="stat-label">Занято PCIe линий</span></div>
|
||||
<div class="stat-box ${balanceClass}"><span class="stat-value">${balanceLabel}</span><span class="stat-label">Баланс PCIe</span></div>
|
||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(cpuModel)}</span><span class="stat-label">Модель</span></div>
|
||||
</div>
|
||||
<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 => {
|
||||
<div class="pcie-balance-bars">`;
|
||||
pcieBalance.perCPU.forEach(cpu => {
|
||||
html += `<div class="pcie-balance-row">
|
||||
<span class="pcie-balance-cpu">${escapeHtml(cpu.label)}</span>
|
||||
<div class="pcie-balance-track"><div class="pcie-balance-fill ${balanceClass}" style="width:${cpu.percent}%"></div></div>
|
||||
<span class="pcie-balance-value">${cpu.lanes}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += `</div>
|
||||
<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>PCIe линии (занято)</th><th>Модулей памяти</th><th>PPIN</th></tr></thead><tbody>`;
|
||||
cpus.forEach(cpu => {
|
||||
const socket = cpu.slot || '-';
|
||||
const cpuIdx = extractCPUIndex(socket);
|
||||
const pcieUsed = cpuIdx !== null ? (pcieByCPU.get(cpuIdx) || 0) : '-';
|
||||
const memoryModules = cpuIdx !== null ? (memoryByCPU.get(cpuIdx) || 0) : '-';
|
||||
const tdp = (cpu.details && cpu.details.tdp_w) || '-';
|
||||
const l3 = (cpu.details && cpu.details.l3_cache_kb) ? Math.round(cpu.details.l3_cache_kb / 1024) : '-';
|
||||
const ppin = (cpu.details && cpu.details.ppin) || '-';
|
||||
html += `<tr>
|
||||
<td>CPU${cpu.socket}</td>
|
||||
<td>${escapeHtml(cpu.model)}</td>
|
||||
<td>${cpu.cores}</td>
|
||||
<td>${cpu.threads}</td>
|
||||
<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>
|
||||
<td>${escapeHtml(socket)}</td>
|
||||
<td>${escapeHtml(cpu.model || '-')}</td>
|
||||
<td>${cpu.cores || '-'}</td>
|
||||
<td>${cpu.threads || '-'}</td>
|
||||
<td>${cpu.frequency_mhz ? cpu.frequency_mhz + ' MHz' : '-'}</td>
|
||||
<td>${cpu.max_frequency_mhz ? cpu.max_frequency_mhz + ' MHz' : '-'}</td>
|
||||
<td>${tdp !== '-' ? tdp + 'W' : '-'}</td>
|
||||
<td>${l3 !== '-' ? l3 + ' MB' : '-'}</td>
|
||||
<td>${pcieUsed}</td>
|
||||
<td>${memoryModules}</td>
|
||||
<td><code>${escapeHtml(ppin)}</code></td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
@@ -718,10 +760,10 @@ function renderConfig(data) {
|
||||
|
||||
// Memory tab
|
||||
html += '<div class="config-tab-content" id="config-memory">';
|
||||
if (config.memory && config.memory.length > 0) {
|
||||
const totalGB = config.memory.reduce((sum, m) => sum + m.size_mb, 0) / 1024;
|
||||
const presentCount = config.memory.filter(m => m.present !== false).length;
|
||||
const workingCount = config.memory.filter(m => m.size_mb > 0).length;
|
||||
if (memory.length > 0) {
|
||||
const totalGB = memory.reduce((sum, m) => sum + (m.size_mb || 0), 0) / 1024;
|
||||
const presentCount = memory.filter(m => m.present !== false).length;
|
||||
const workingCount = memory.filter(m => (m.size_mb || 0) > 0).length;
|
||||
html += `<h3>Модули памяти</h3>
|
||||
<div class="memory-overview">
|
||||
<div class="stat-box"><span class="stat-value">${totalGB} GB</span><span class="stat-label">Всего</span></div>
|
||||
@@ -731,10 +773,10 @@ function renderConfig(data) {
|
||||
<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 => {
|
||||
memory.forEach(mem => {
|
||||
const present = mem.present !== false ? '✓' : '-';
|
||||
const presentClass = mem.present !== false ? 'present-yes' : 'present-no';
|
||||
const sizeGB = mem.size_mb / 1024;
|
||||
const sizeGB = (mem.size_mb || 0) / 1024;
|
||||
const statusClass = (mem.status === 'OK' || !mem.status) ? '' : 'status-warning';
|
||||
const rowClass = sizeGB === 0 ? 'row-warning' : '';
|
||||
html += `<tr class="${rowClass}">
|
||||
@@ -742,8 +784,8 @@ function renderConfig(data) {
|
||||
<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>${(mem.details && mem.details.max_speed_mhz) || '-'} MHz</td>
|
||||
<td>${(mem.details && mem.details.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>
|
||||
@@ -757,12 +799,12 @@ function renderConfig(data) {
|
||||
|
||||
// Power tab
|
||||
html += '<div class="config-tab-content" id="config-power">';
|
||||
if (config.power_supplies && config.power_supplies.length > 0) {
|
||||
const psuTotal = config.power_supplies.length;
|
||||
const psuPresent = config.power_supplies.filter(p => p.present !== false).length;
|
||||
const psuOK = config.power_supplies.filter(p => p.status === 'OK').length;
|
||||
const psuModel = config.power_supplies[0].model || '-';
|
||||
const psuWattage = config.power_supplies[0].wattage_w || 0;
|
||||
if (powerSupplies.length > 0) {
|
||||
const psuTotal = powerSupplies.length;
|
||||
const psuPresent = powerSupplies.filter(p => p.present !== false).length;
|
||||
const psuOK = powerSupplies.filter(p => p.status === 'OK').length;
|
||||
const psuModel = powerSupplies[0].model || '-';
|
||||
const psuWattage = powerSupplies[0].wattage_w || 0;
|
||||
html += `<h3>Блоки питания</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${psuTotal}</span><span class="stat-label">Всего</span></div>
|
||||
@@ -772,11 +814,11 @@ function renderConfig(data) {
|
||||
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(psuModel)}</span><span class="stat-label">Модель</span></div>
|
||||
</div>
|
||||
<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 => {
|
||||
powerSupplies.forEach(psu => {
|
||||
const statusClass = psu.status === 'OK' ? '' : 'status-warning';
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(psu.slot)}</td>
|
||||
<td>${escapeHtml(psu.vendor || '-')}</td>
|
||||
<td>${escapeHtml(psu.manufacturer || psu.vendor || '-')}</td>
|
||||
<td>${escapeHtml(psu.model || '-')}</td>
|
||||
<td>${psu.wattage_w || '-'}W</td>
|
||||
<td>${psu.input_power_w || '-'}W</td>
|
||||
@@ -794,12 +836,12 @@ function renderConfig(data) {
|
||||
|
||||
// Storage tab
|
||||
html += '<div class="config-tab-content" id="config-storage">';
|
||||
if (config.storage && config.storage.length > 0) {
|
||||
const storTotal = config.storage.length;
|
||||
const storHDD = config.storage.filter(s => s.type === 'HDD').length;
|
||||
const storSSD = config.storage.filter(s => s.type === 'SSD').length;
|
||||
const storNVMe = config.storage.filter(s => s.type === 'NVMe').length;
|
||||
const totalTB = (config.storage.reduce((sum, s) => sum + (s.size_gb || 0), 0) / 1000).toFixed(1);
|
||||
if (storage.length > 0) {
|
||||
const storTotal = storage.length;
|
||||
const storHDD = storage.filter(s => s.type === 'HDD').length;
|
||||
const storSSD = storage.filter(s => s.type === 'SSD').length;
|
||||
const storNVMe = storage.filter(s => s.type === 'NVMe').length;
|
||||
const totalTB = (storage.reduce((sum, s) => sum + (s.size_gb || 0), 0) / 1000).toFixed(1);
|
||||
let typesSummary = [];
|
||||
if (storHDD > 0) typesSummary.push(`${storHDD} HDD`);
|
||||
if (storSSD > 0) typesSummary.push(`${storSSD} SSD`);
|
||||
@@ -807,19 +849,19 @@ function renderConfig(data) {
|
||||
html += `<h3>Накопители</h3>
|
||||
<div class="section-overview">
|
||||
<div class="stat-box"><span class="stat-value">${storTotal}</span><span class="stat-label">Всего слотов</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${config.storage.filter(s => s.present).length}</span><span class="stat-label">Установлено</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${storage.filter(s => s.present).length}</span><span class="stat-label">Установлено</span></div>
|
||||
<div class="stat-box"><span class="stat-value">${totalTB > 0 ? totalTB + ' TB' : '-'}</span><span class="stat-label">Объём</span></div>
|
||||
<div class="stat-box model-box"><span class="stat-value">${typesSummary.join(', ') || '-'}</span><span class="stat-label">По типам</span></div>
|
||||
</div>
|
||||
<table class="config-table"><thead><tr><th>NO.</th><th>Статус</th><th>Расположение</th><th>Backplane ID</th><th>Тип</th><th>Модель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>`;
|
||||
config.storage.forEach(s => {
|
||||
storage.forEach(s => {
|
||||
const presentIcon = s.present ? '<span style="color: #27ae60;">●</span>' : '<span style="color: #95a5a6;">○</span>';
|
||||
const presentText = s.present ? 'Present' : 'Empty';
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(s.slot || '-')}</td>
|
||||
<td>${presentIcon} ${presentText}</td>
|
||||
<td>${escapeHtml(s.location || '-')}</td>
|
||||
<td>${s.backplane_id !== undefined ? s.backplane_id : '-'}</td>
|
||||
<td>${s.details && s.details.backplane_id !== undefined ? s.details.backplane_id : '-'}</td>
|
||||
<td>${escapeHtml(s.type || '-')}</td>
|
||||
<td>${escapeHtml(s.model || '-')}</td>
|
||||
<td>${s.size_gb > 0 ? s.size_gb + ' GB' : '-'}</td>
|
||||
@@ -834,25 +876,7 @@ function renderConfig(data) {
|
||||
|
||||
// GPU tab
|
||||
html += '<div class="config-tab-content" id="config-gpu">';
|
||||
const gpuRows = (config.gpus && config.gpus.length > 0)
|
||||
? config.gpus
|
||||
: (config.pcie_devices || [])
|
||||
.filter((p) => {
|
||||
const cls = String(p.device_class || '').toLowerCase();
|
||||
const mfr = String(p.manufacturer || '').toLowerCase();
|
||||
return cls.includes('gpu') || cls.includes('display') || cls.includes('3d') || mfr.includes('nvidia') || p.vendor_id === 0x10de;
|
||||
})
|
||||
.map((p) => ({
|
||||
slot: p.slot,
|
||||
model: p.part_number || p.device_class,
|
||||
manufacturer: p.manufacturer,
|
||||
bdf: p.bdf,
|
||||
serial_number: p.serial_number,
|
||||
current_link_width: p.link_width,
|
||||
current_link_speed: p.link_speed,
|
||||
max_link_width: p.max_link_width,
|
||||
max_link_speed: p.max_link_speed
|
||||
}));
|
||||
const gpuRows = gpus;
|
||||
if (gpuRows.length > 0) {
|
||||
const gpuCount = gpuRows.length;
|
||||
const gpuModel = gpuRows[0].model || '-';
|
||||
@@ -888,22 +912,7 @@ function renderConfig(data) {
|
||||
|
||||
// Network tab
|
||||
html += '<div class="config-tab-content" id="config-network">';
|
||||
const networkRows = (config.network_adapters && config.network_adapters.length > 0)
|
||||
? config.network_adapters
|
||||
: (config.pcie_devices || [])
|
||||
.filter((p) => {
|
||||
const cls = String(p.device_class || '').toLowerCase();
|
||||
return cls.includes('network') || cls.includes('ethernet') || cls.includes('gigabit');
|
||||
})
|
||||
.map((p) => ({
|
||||
location: p.slot,
|
||||
model: p.part_number || p.device_class,
|
||||
vendor: p.manufacturer,
|
||||
port_count: 0,
|
||||
port_type: '',
|
||||
mac_addresses: p.mac_addresses || [],
|
||||
status: p.status || ''
|
||||
}));
|
||||
const networkRows = networkAdapters;
|
||||
if (networkRows.length > 0) {
|
||||
const nicCount = networkRows.length;
|
||||
const totalPorts = networkRows.reduce((sum, n) => sum + (n.port_count || 0), 0);
|
||||
@@ -923,7 +932,7 @@ function renderConfig(data) {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(nic.location || nic.slot || '-')}</td>
|
||||
<td>${escapeHtml(nic.model || '-')}</td>
|
||||
<td>${escapeHtml(nic.vendor || '-')}</td>
|
||||
<td>${escapeHtml(nic.manufacturer || nic.vendor || '-')}</td>
|
||||
<td>${nic.port_count || '-'}</td>
|
||||
<td>${escapeHtml(nic.port_type || '-')}</td>
|
||||
<td><code>${escapeHtml(macs)}</code></td>
|
||||
@@ -938,65 +947,56 @@ function renderConfig(data) {
|
||||
|
||||
// PCIe Device Inventory tab
|
||||
html += '<div class="config-tab-content" id="config-pcie">';
|
||||
const hasPCIe = config.pcie_devices && config.pcie_devices.length > 0;
|
||||
const hasGPUs = config.gpus && config.gpus.length > 0;
|
||||
if (hasPCIe || hasGPUs) {
|
||||
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><th>Серийный номер</th><th>Прошивка</th></tr></thead><tbody>';
|
||||
const pcieRowKey = (slot, bdf, vendorId, deviceId) => {
|
||||
const normalizedBDF = (bdf || '').trim().toLowerCase();
|
||||
if (normalizedBDF) return `bdf:${normalizedBDF}`;
|
||||
const normalizedSlot = (slot || '').trim().toLowerCase();
|
||||
if (normalizedSlot) return `slot:${normalizedSlot}`;
|
||||
return `id:${vendorId || 0}:${deviceId || 0}`;
|
||||
};
|
||||
const gpuByKey = new Map();
|
||||
(config.gpus || []).forEach(gpu => {
|
||||
gpuByKey.set(pcieRowKey(gpu.slot, gpu.bdf, gpu.vendor_id, gpu.device_id), gpu);
|
||||
if (inventoryRows.length > 0) {
|
||||
html += '<h3>PCIe устройства</h3>';
|
||||
const groups = new Map();
|
||||
inventoryRows.forEach(p => {
|
||||
const idx = extractCPUIndex(p.slot);
|
||||
const key = idx === null ? 'other' : `cpu${idx}`;
|
||||
if (!groups.has(key)) {
|
||||
groups.set(key, {
|
||||
idx,
|
||||
title: idx === null ? 'Без привязки к CPU' : `CPU${idx}`,
|
||||
lanes: 0,
|
||||
rows: []
|
||||
});
|
||||
}
|
||||
const lanes = Number(p.link_width) > 0 ? Number(p.link_width) : (Number(p.max_link_width) > 0 ? Number(p.max_link_width) : 0);
|
||||
const group = groups.get(key);
|
||||
group.lanes += lanes;
|
||||
group.rows.push(p);
|
||||
});
|
||||
|
||||
(config.pcie_devices || []).forEach(p => {
|
||||
const key = pcieRowKey(p.slot, p.bdf, p.vendor_id, p.device_id);
|
||||
const matchedGPU = gpuByKey.get(key);
|
||||
|
||||
const pcieLink = formatPCIeLink(
|
||||
p.link_width,
|
||||
p.link_speed,
|
||||
p.max_link_width,
|
||||
p.max_link_speed
|
||||
);
|
||||
const serial = p.serial_number || (matchedGPU ? matchedGPU.serial_number : '');
|
||||
const firmware = p.firmware || (matchedGPU ? matchedGPU.firmware : '') || findPCIeFirmwareVersion(config.firmware, p);
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(p.slot || '-')}</td>
|
||||
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
||||
<td>${escapeHtml(p.part_number || '-')}</td>
|
||||
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
||||
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
||||
<td>${pcieLink}</td>
|
||||
<td><code>${escapeHtml(serial || '-')}</code></td>
|
||||
<td><code>${escapeHtml(firmware || '-')}</code></td>
|
||||
</tr>`;
|
||||
const sortedGroups = [...groups.values()].sort((a, b) => {
|
||||
if (a.idx === null) return 1;
|
||||
if (b.idx === null) return -1;
|
||||
return a.idx - b.idx;
|
||||
});
|
||||
|
||||
(config.gpus || []).forEach(gpu => {
|
||||
const pcieLink = formatPCIeLink(
|
||||
gpu.current_link_width || gpu.link_width,
|
||||
gpu.current_link_speed || gpu.link_speed,
|
||||
gpu.max_link_width,
|
||||
gpu.max_link_speed
|
||||
);
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(gpu.slot || '-')}</td>
|
||||
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
|
||||
<td>${escapeHtml(gpu.model || gpu.part_number || '-')}</td>
|
||||
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
|
||||
<td><code>${gpu.vendor_id ? gpu.vendor_id.toString(16) : '-'}:${gpu.device_id ? gpu.device_id.toString(16) : '-'}</code></td>
|
||||
<td>${pcieLink}</td>
|
||||
<td><code>${escapeHtml(gpu.serial_number || '-')}</code></td>
|
||||
<td><code>${escapeHtml(gpu.firmware || '-')}</code></td>
|
||||
</tr>`;
|
||||
sortedGroups.forEach(group => {
|
||||
html += `<h4 class="pcie-group-title">${escapeHtml(group.title)} · занято линий: ${group.lanes}</h4>`;
|
||||
html += '<table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Модель</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th><th>Серийный номер</th><th>Прошивка</th></tr></thead><tbody>';
|
||||
group.rows.forEach(p => {
|
||||
const pcieLink = formatPCIeLink(
|
||||
p.link_width,
|
||||
p.link_speed,
|
||||
p.max_link_width,
|
||||
p.max_link_speed
|
||||
);
|
||||
const firmware = p.firmware || findPCIeFirmwareVersion(config.firmware, p);
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(p.slot || '-')}</td>
|
||||
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
|
||||
<td>${escapeHtml(p.model || p.part_number || p.device_class || '-')}</td>
|
||||
<td>${escapeHtml(p.manufacturer || '-')}</td>
|
||||
<td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
|
||||
<td>${pcieLink}</td>
|
||||
<td><code>${escapeHtml(p.serial_number || '-')}</code></td>
|
||||
<td><code>${escapeHtml(firmware || '-')}</code></td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
});
|
||||
html += '</tbody></table>';
|
||||
} else {
|
||||
html += '<p class="no-data">Нет данных о PCIe устройствах</p>';
|
||||
}
|
||||
@@ -1283,6 +1283,83 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function calculateCPUToPCIeBalance(inventoryRows, cpus) {
|
||||
const laneByCPU = new Map();
|
||||
const cpuIndexes = new Set();
|
||||
|
||||
(cpus || []).forEach(cpu => {
|
||||
const idx = extractCPUIndex(cpu.slot);
|
||||
if (idx !== null) {
|
||||
cpuIndexes.add(idx);
|
||||
laneByCPU.set(idx, 0);
|
||||
}
|
||||
});
|
||||
|
||||
(inventoryRows || []).forEach(dev => {
|
||||
const idx = extractCPUIndex(dev.slot);
|
||||
if (idx === null) return;
|
||||
|
||||
const lanes = Number(dev.link_width) > 0
|
||||
? Number(dev.link_width)
|
||||
: (Number(dev.max_link_width) > 0
|
||||
? Number(dev.max_link_width)
|
||||
: (dev.bdf ? 1 : 0));
|
||||
if (lanes <= 0) return;
|
||||
|
||||
if (!laneByCPU.has(idx)) laneByCPU.set(idx, 0);
|
||||
laneByCPU.set(idx, laneByCPU.get(idx) + lanes);
|
||||
cpuIndexes.add(idx);
|
||||
});
|
||||
|
||||
const indexes = [...cpuIndexes].sort((a, b) => a - b);
|
||||
const values = indexes.map(i => laneByCPU.get(i) || 0);
|
||||
const totalLanes = values.reduce((a, b) => a + b, 0);
|
||||
const maxLanes = values.length ? Math.max(...values) : 0;
|
||||
const minLanes = values.length ? Math.min(...values) : 0;
|
||||
const diffRatio = totalLanes > 0 ? (maxLanes - minLanes) / totalLanes : 0;
|
||||
let severity = 'ok';
|
||||
if (values.length > 1) {
|
||||
if (diffRatio >= 0.35) severity = 'critical';
|
||||
else if (diffRatio >= 0.2) severity = 'warning';
|
||||
}
|
||||
|
||||
const denominator = maxLanes > 0 ? maxLanes : 1;
|
||||
const perCPU = indexes.map(i => {
|
||||
const lanes = laneByCPU.get(i) || 0;
|
||||
return {
|
||||
label: `CPU${i}`,
|
||||
lanes,
|
||||
percent: Math.round((lanes / denominator) * 100)
|
||||
};
|
||||
});
|
||||
|
||||
if (perCPU.length === 0) {
|
||||
perCPU.push({ label: 'CPU?', lanes: 0, percent: 0 });
|
||||
}
|
||||
|
||||
return { totalLanes, severity, perCPU };
|
||||
}
|
||||
|
||||
function extractCPUIndex(slot) {
|
||||
const s = String(slot || '').trim();
|
||||
if (!s) return null;
|
||||
const m = s.match(/cpu\s*([0-9]+)/i);
|
||||
if (!m) return null;
|
||||
const idx = Number(m[1]);
|
||||
return Number.isFinite(idx) ? idx : null;
|
||||
}
|
||||
|
||||
function calculateMemoryModulesByCPU(memoryRows) {
|
||||
const out = new Map();
|
||||
(memoryRows || []).forEach(mem => {
|
||||
if (mem.present === false || (mem.size_mb || 0) <= 0) return;
|
||||
const idx = extractCPUIndex(mem.location || mem.slot);
|
||||
if (idx === null) return;
|
||||
out.set(idx, (out.get(idx) || 0) + 1);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function findPCIeFirmwareVersion(firmwareEntries, pcieDevice) {
|
||||
if (!Array.isArray(firmwareEntries) || !pcieDevice) return '';
|
||||
|
||||
@@ -1290,17 +1367,30 @@ function findPCIeFirmwareVersion(firmwareEntries, pcieDevice) {
|
||||
const model = (pcieDevice.part_number || '').trim().toLowerCase();
|
||||
if (!slot && !model) return '';
|
||||
|
||||
const slotPatterns = slot
|
||||
? [
|
||||
new RegExp(`^psu\\s*${escapeRegExp(slot)}\\b`, 'i'),
|
||||
new RegExp(`^nic\\s+${escapeRegExp(slot)}\\b`, 'i'),
|
||||
new RegExp(`^gpu\\s+${escapeRegExp(slot)}\\b`, 'i'),
|
||||
new RegExp(`^nvswitch\\s+${escapeRegExp(slot)}\\b`, 'i')
|
||||
]
|
||||
: [];
|
||||
|
||||
for (const fw of firmwareEntries) {
|
||||
const name = (fw.device_name || '').trim().toLowerCase();
|
||||
const version = (fw.version || '').trim();
|
||||
if (!name || !version) continue;
|
||||
if (slot && name.includes(slot)) return version;
|
||||
if (slot && slotPatterns.some(re => re.test(name))) return version;
|
||||
if (model && name.includes(model)) return version;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function formatPCIeLink(currentWidth, currentSpeed, maxWidth, maxSpeed) {
|
||||
// Helper to convert speed to generation
|
||||
function speedToGen(speed) {
|
||||
|
||||
Reference in New Issue
Block a user