Files
logpile/web/static/js/app.js
Mikhail Chusavitin 70cd541d9e v1.3.0: Add multiple vendor parsers and enhanced hardware detection
New parsers:
- NVIDIA Field Diagnostics parser with dmidecode output support
- NVIDIA Bug Report parser with comprehensive hardware extraction
- Supermicro crashdump (CDump.txt) parser
- Generic fallback parser for unrecognized text files

Enhanced GPU parsing (nvidia-bug-report):
- Model and manufacturer detection (NVIDIA H100 80GB HBM3)
- UUID, Video BIOS version, IRQ information
- Bus location (BDF), DMA size/mask, device minor
- PCIe bus type details

New hardware detection (nvidia-bug-report):
- System Information: server S/N, UUID, manufacturer, product name
- CPU: model, S/N, cores, threads, frequencies from dmidecode
- Memory: P/N, S/N, manufacturer, speed for all DIMMs
- Power Supplies: manufacturer, model, S/N, wattage, status
- Network Adapters: Ethernet/InfiniBand controllers with VPD data
  - Model, P/N, S/N from lspci Vital Product Data
  - Port count/type detection (QSFP56, OSFP, etc.)
  - Support for ConnectX-6/7 adapters

Archive handling improvements:
- Plain .gz file support (not just tar.gz)
- Increased size limit for plain gzip files (50MB)
- Better error handling for mixed archive formats

Web interface enhancements:
- Display parser name and filename badges
- Improved file info section with visual indicators

Co-Authored-By: Claude (qwen3-coder:480b) <noreply@anthropic.com>
2026-01-30 17:19:47 +03:00

796 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// LOGPile Frontend Application
document.addEventListener('DOMContentLoaded', () => {
initUpload();
initTabs();
initFilters();
loadParsersInfo();
});
// Load and display available parsers
async function loadParsersInfo() {
try {
const response = await fetch('/api/parsers');
const data = await response.json();
const container = document.getElementById('parsers-info');
if (data.parsers && data.parsers.length > 0) {
let html = '<p class="parsers-title">Поддерживаемые платформы:</p><div class="parsers-list">';
data.parsers.forEach(p => {
html += `<div class="parser-item">
<span class="parser-name">${escapeHtml(p.name)}</span>
<span class="parser-version">v${escapeHtml(p.version)}</span>
</div>`;
});
html += '</div>';
container.innerHTML = html;
}
} catch (err) {
console.error('Failed to load parsers info:', err);
}
}
// Upload handling
function initUpload() {
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
uploadFile(files[0]);
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
uploadFile(fileInput.files[0]);
}
});
}
async function uploadFile(file) {
const status = document.getElementById('upload-status');
status.textContent = 'Загрузка и анализ...';
status.className = '';
const formData = new FormData();
formData.append('archive', file);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
status.innerHTML = `<strong>${escapeHtml(result.vendor)}</strong><br>` +
`${result.stats.sensors} сенсоров, ${result.stats.fru} компонентов, ${result.stats.events} событий`;
status.className = 'success';
loadData(result.vendor, result.filename);
} else {
status.textContent = result.error || 'Ошибка загрузки';
status.className = 'error';
}
} catch (err) {
status.textContent = 'Ошибка соединения';
status.className = 'error';
}
}
// Tab navigation
function initTabs() {
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.dataset.tab).classList.add('active');
});
});
}
// Filters
function initFilters() {
document.getElementById('sensor-filter').addEventListener('change', (e) => {
filterSensors(e.target.value);
});
document.getElementById('severity-filter').addEventListener('change', (e) => {
filterEvents(e.target.value);
});
document.getElementById('serial-filter').addEventListener('change', (e) => {
filterSerials(e.target.value);
});
}
let allSensors = [];
let allEvents = [];
let allSerials = [];
let currentVendor = '';
// Load data from API
async function loadData(vendor, filename) {
currentVendor = vendor || '';
document.getElementById('upload-section').classList.add('hidden');
document.getElementById('data-section').classList.remove('hidden');
document.getElementById('clear-btn').classList.remove('hidden');
// Update parser name and filename
const parserName = document.getElementById('parser-name');
const fileNameElem = document.getElementById('file-name');
if (parserName && currentVendor) {
parserName.textContent = currentVendor;
}
if (fileNameElem && filename) {
fileNameElem.textContent = filename;
}
// Update vendor badge if exists (legacy support)
const vendorBadge = document.getElementById('vendor-badge');
if (vendorBadge && currentVendor) {
vendorBadge.textContent = currentVendor;
vendorBadge.classList.remove('hidden');
}
await Promise.all([
loadConfig(),
loadFirmware(),
loadSensors(),
loadSerials(),
loadEvents()
]);
}
async function loadConfig() {
try {
const response = await fetch('/api/config');
const config = await response.json();
renderConfig(config);
} catch (err) {
console.error('Failed to load config:', err);
}
}
function renderConfig(data) {
const container = document.getElementById('config-content');
if (!data || Object.keys(data).length === 0) {
container.innerHTML = '<p class="no-data">Нет данных о конфигурации</p>';
return;
}
const config = data.hardware || data;
const spec = data.specification;
let html = '';
// Server info header
if (config.board) {
html += `<div class="server-info">
<div class="server-info-item"><span class="server-info-label">Модель сервера:</span> <strong>${escapeHtml(config.board.product_name || '-')}</strong></div>
<div class="server-info-item"><span class="server-info-label">Серийный номер:</span> <code>${escapeHtml(config.board.serial_number || '-')}</code></div>
</div>`;
}
// 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) {
html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
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 += '</ul></div>';
} else {
html += '<p class="no-data">Нет данных о спецификации</p>';
}
html += '</div>';
// 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);
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 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 => {
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>
</tr>`;
});
html += '</tbody></table>';
} else {
html += '<p class="no-data">Нет данных о процессорах</p>';
}
html += '</div>';
// 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;
html += `<h3>Модули памяти</h3>
<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>';
// 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;
html += `<h3>Блоки питания</h3>
<div class="section-overview">
<div class="stat-box"><span class="stat-value">${psuTotal}</span><span class="stat-label">Всего</span></div>
<div class="stat-box"><span class="stat-value">${psuPresent}</span><span class="stat-label">Подключено</span></div>
<div class="stat-box"><span class="stat-value">${psuOK}</span><span class="stat-label">Работает</span></div>
<div class="stat-box"><span class="stat-value">${psuWattage}W</span><span class="stat-label">Мощность</span></div>
<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 => {
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) {
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);
let typesSummary = [];
if (storHDD > 0) typesSummary.push(`${storHDD} HDD`);
if (storSSD > 0) typesSummary.push(`${storSSD} SSD`);
if (storNVMe > 0) typesSummary.push(`${storNVMe} NVMe`);
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">${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 => {
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>${escapeHtml(s.type || '-')}</td>
<td>${escapeHtml(s.model || '-')}</td>
<td>${s.size_gb > 0 ? s.size_gb + ' GB' : '-'}</td>
<td>${s.serial_number ? '<code>' + escapeHtml(s.serial_number) + '</code>' : '-'}</td>
</tr>`;
});
html += '</tbody></table>';
} else {
html += '<p class="no-data">Нет данных о накопителях</p>';
}
html += '</div>';
// GPU tab
html += '<div class="config-tab-content" id="config-gpu">';
if (config.gpus && config.gpus.length > 0) {
const gpuCount = config.gpus.length;
const gpuModel = config.gpus[0].model || '-';
const gpuVendor = config.gpus[0].manufacturer || '-';
html += `<h3>Графические процессоры</h3>
<div class="section-overview">
<div class="stat-box"><span class="stat-value">${gpuCount}</span><span class="stat-label">Всего GPU</span></div>
<div class="stat-box"><span class="stat-value">${escapeHtml(gpuVendor)}</span><span class="stat-label">Производитель</span></div>
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(gpuModel)}</span><span class="stat-label">Модель</span></div>
</div>
<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 => {
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>${escapeHtml(gpu.model || '-')}</td>
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
<td>${pcieLink}</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) {
const nicCount = config.network_adapters.length;
const totalPorts = config.network_adapters.reduce((sum, n) => sum + (n.port_count || 0), 0);
const nicTypes = [...new Set(config.network_adapters.map(n => n.port_type).filter(t => t))];
const nicModels = [...new Set(config.network_adapters.map(n => n.model).filter(m => m))];
html += `<h3>Сетевые адаптеры</h3>
<div class="section-overview">
<div class="stat-box"><span class="stat-value">${nicCount}</span><span class="stat-label">Адаптеров</span></div>
<div class="stat-box"><span class="stat-value">${totalPorts}</span><span class="stat-label">Портов</span></div>
<div class="stat-box"><span class="stat-value">${nicTypes.join(', ') || '-'}</span><span class="stat-label">Тип портов</span></div>
<div class="stat-box model-box"><span class="stat-value">${escapeHtml(nicModels.join(', ') || '-')}</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>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) {
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 => {
const pcieLink = formatPCIeLink(
p.link_width,
p.link_speed,
p.max_link_width,
p.max_link_speed
);
html += `<tr>
<td>${escapeHtml(p.slot || '-')}</td>
<td><code>${escapeHtml(p.bdf || '-')}</code></td>
<td>${escapeHtml(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>
</tr>`;
});
html += '</tbody></table>';
} else {
html += '<p class="no-data">Нет данных о PCIe устройствах</p>';
}
html += '</div>';
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() {
try {
const response = await fetch('/api/firmware');
const firmware = await response.json();
renderFirmware(firmware);
} catch (err) {
console.error('Failed to load firmware:', err);
}
}
let allFirmware = [];
function renderFirmware(firmware) {
allFirmware = firmware || [];
// Render in Firmware tab
const tbody = document.querySelector('#firmware-table tbody');
tbody.innerHTML = '';
if (!firmware || firmware.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="no-data">Нет данных о прошивках</td></tr>';
} else {
firmware.forEach(fw => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${escapeHtml(fw.component)}</td>
<td>${escapeHtml(fw.model)}</td>
<td><code>${escapeHtml(fw.version)}</code></td>
`;
tbody.appendChild(row);
});
}
}
async function loadSensors() {
try {
const response = await fetch('/api/sensors');
allSensors = await response.json();
renderSensors(allSensors);
} catch (err) {
console.error('Failed to load sensors:', err);
}
}
function renderSensors(sensors) {
const container = document.getElementById('sensors-content');
if (!sensors || sensors.length === 0) {
container.innerHTML = '<p class="no-data">Нет данных о сенсорах</p>';
return;
}
// Group by type
const byType = {};
sensors.forEach(s => {
if (!byType[s.type]) byType[s.type] = [];
byType[s.type].push(s);
});
const typeNames = {
temperature: 'Температура',
voltage: 'Напряжение',
power: 'Мощность',
fan_speed: 'Вентиляторы',
fan_status: 'Статус вентиляторов',
psu_status: 'Статус БП',
cpu_status: 'Статус CPU',
storage_status: 'Статус накопителей',
other: 'Прочее'
};
let html = '';
for (const [type, items] of Object.entries(byType)) {
html += `<div class="sensor-group" data-type="${type}">
<h3>${typeNames[type] || type}</h3>
<div class="sensor-grid">`;
items.forEach(s => {
let valueStr = '';
let statusClass = s.status === 'ok' ? 'ok' : (s.status === 'ns' ? 'ns' : 'warn');
if (s.value) {
valueStr = `${s.value} ${s.unit}`;
} else if (s.raw_value) {
valueStr = s.raw_value;
} else {
valueStr = s.status;
}
html += `<div class="sensor-card ${statusClass}">
<span class="sensor-name">${escapeHtml(s.name)}</span>
<span class="sensor-value">${escapeHtml(valueStr)}</span>
</div>`;
});
html += '</div></div>';
}
container.innerHTML = html;
}
function filterSensors(type) {
if (!type) {
renderSensors(allSensors);
return;
}
const filtered = allSensors.filter(s => s.type === type);
renderSensors(filtered);
}
async function loadSerials() {
try {
const response = await fetch('/api/serials');
allSerials = await response.json();
renderSerials(allSerials);
} catch (err) {
console.error('Failed to load serials:', err);
}
}
function renderSerials(serials) {
const tbody = document.querySelector('#serials-table tbody');
tbody.innerHTML = '';
if (!serials || serials.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="no-data">Нет серийных номеров</td></tr>';
return;
}
const categoryNames = {
'Board': 'Мат. плата',
'CPU': 'Процессор',
'Memory': 'Память',
'Storage': 'Накопитель',
'PCIe': 'PCIe',
'Network': 'Сеть',
'PSU': 'БП',
'Firmware': 'Прошивка',
'FRU': 'FRU'
};
serials.forEach(item => {
// Skip items without serial number or with N/A
if (!item.serial_number || item.serial_number === 'N/A') return;
const row = document.createElement('tr');
row.innerHTML = `
<td><span class="category-badge ${item.category.toLowerCase()}">${categoryNames[item.category] || item.category}</span></td>
<td>${escapeHtml(item.component)}</td>
<td>${escapeHtml(item.location || '-')}</td>
<td><code>${escapeHtml(item.serial_number)}</code></td>
<td>${escapeHtml(item.manufacturer || '-')}</td>
`;
tbody.appendChild(row);
});
}
function filterSerials(category) {
if (!category) {
renderSerials(allSerials);
return;
}
const filtered = allSerials.filter(s => s.category === category);
renderSerials(filtered);
}
async function loadEvents() {
try {
const response = await fetch('/api/events');
allEvents = await response.json();
renderEvents(allEvents);
} catch (err) {
console.error('Failed to load events:', err);
}
}
function renderEvents(events) {
const tbody = document.querySelector('#events-table tbody');
tbody.innerHTML = '';
if (!events || events.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="no-data">Нет событий</td></tr>';
return;
}
events.forEach(event => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${formatDate(event.timestamp)}</td>
<td>${escapeHtml(event.source)}</td>
<td>${escapeHtml(event.description)}</td>
<td><span class="severity ${event.severity}">${event.severity}</span></td>
`;
tbody.appendChild(row);
});
}
function filterEvents(severity) {
if (!severity) {
renderEvents(allEvents);
return;
}
const filtered = allEvents.filter(e => e.severity === severity);
renderEvents(filtered);
}
// Export functions
function exportData(format) {
window.location.href = `/api/export/${format}`;
}
// Clear data
async function clearData() {
try {
await fetch('/api/clear', { method: 'DELETE' });
document.getElementById('upload-section').classList.remove('hidden');
document.getElementById('data-section').classList.add('hidden');
document.getElementById('clear-btn').classList.add('hidden');
document.getElementById('upload-status').textContent = '';
allSensors = [];
allEvents = [];
allSerials = [];
} catch (err) {
console.error('Failed to clear data:', err);
}
}
// Restart app (reload page)
function restartApp() {
if (confirm('Перезапустить приложение? Все загруженные данные будут потеряны.')) {
fetch('/api/clear', { method: 'DELETE' }).then(() => {
window.location.reload();
});
}
}
// Exit app (shutdown server)
async function exitApp() {
if (confirm('Завершить работу приложения?')) {
try {
await fetch('/api/shutdown', { method: 'POST' });
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
} catch (err) {
// Server shutdown, connection will fail
document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
}
}
}
// Utilities
function formatDate(isoString) {
if (!isoString) return '-';
const date = new Date(isoString);
return date.toLocaleString('ru-RU');
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatPCIeLink(currentWidth, currentSpeed, maxWidth, maxSpeed) {
// Helper to convert speed to generation
function speedToGen(speed) {
if (!speed) return '';
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
if (gtMatch) {
const gts = parseFloat(gtMatch[1]);
if (gts >= 32) return 'Gen5';
if (gts >= 16) return 'Gen4';
if (gts >= 8) return 'Gen3';
if (gts >= 5) return 'Gen2';
if (gts >= 2.5) return 'Gen1';
}
return '';
}
// Helper to extract GT/s value for comparison
function extractGTs(speed) {
if (!speed) return 0;
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
return gtMatch ? parseFloat(gtMatch[1]) : 0;
}
// If no data, return dash
if (!currentWidth && !currentSpeed) return '-';
const curGen = speedToGen(currentSpeed);
const maxGen = speedToGen(maxSpeed);
// Check if current is lower than max
const widthDegraded = maxWidth && currentWidth && currentWidth < maxWidth;
const speedDegraded = maxSpeed && currentSpeed && extractGTs(currentSpeed) < extractGTs(maxSpeed);
// Build current link string
const curWidthStr = currentWidth ? `x${currentWidth}` : '';
const curLinkStr = curGen ? `${curWidthStr} ${curGen}` : `${curWidthStr} ${currentSpeed || ''}`;
// Build max link string (if available)
let maxLinkStr = '';
if (maxWidth || maxSpeed) {
const maxWidthStr = maxWidth ? `x${maxWidth}` : '';
maxLinkStr = maxGen ? `${maxWidthStr} ${maxGen}` : `${maxWidthStr} ${maxSpeed || ''}`;
}
// Apply degraded class if needed
const degradedClass = (widthDegraded || speedDegraded) ? ' class="pcie-degraded"' : '';
// Format output: show "current" or "current / max" if max differs
if (maxLinkStr && (widthDegraded || speedDegraded)) {
return `<span${degradedClass}>${curLinkStr}</span> <span class="pcie-max">/ ${maxLinkStr}</span>`;
} else if (maxLinkStr && maxLinkStr !== curLinkStr) {
return `${curLinkStr} <span class="pcie-max">/ ${maxLinkStr}</span>`;
} else {
return curLinkStr;
}
}