// 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 = '

Поддерживаемые платформы:

'; data.parsers.forEach(p => { html += `
${escapeHtml(p.name)} v${escapeHtml(p.version)}
`; }); html += '
'; 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 = `${escapeHtml(result.vendor)}
` + `${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 = '

Нет данных о конфигурации

'; return; } const config = data.hardware || data; const spec = data.specification; let html = ''; // Server info header if (config.board) { html += `
Модель сервера: ${escapeHtml(config.board.product_name || '-')}
Серийный номер: ${escapeHtml(config.board.serial_number || '-')}
`; } // Configuration sub-tabs html += `
`; // Specification tab html += '
'; if (spec && spec.length > 0) { html += '

Спецификация сервера

'; } else { html += '

Нет данных о спецификации

'; } html += '
'; // CPU tab html += '
'; 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 += `

Процессоры

${cpuCount}Процессоров
${totalCores}Ядер
${totalThreads}Потоков
${escapeHtml(cpuModel)}Модель
`; config.cpus.forEach(cpu => { html += ``; }); html += '
SocketМодельЯдраПотокиЧастотаMax TurboTDPL3 CachePPIN
CPU${cpu.socket} ${escapeHtml(cpu.model)} ${cpu.cores} ${cpu.threads} ${cpu.frequency_mhz} MHz ${cpu.max_frequency_mhz} MHz ${cpu.tdp_w}W ${Math.round(cpu.l3_cache_kb/1024)} MB ${escapeHtml(cpu.ppin || '-')}
'; } else { html += '

Нет данных о процессорах

'; } html += '
'; // Memory tab html += '
'; 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 += `

Модули памяти

${totalGB} GBВсего
${presentCount}Установлено
${workingCount}Активно
`; 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 += ``; }); html += '
LocationНаличиеРазмерТипMax частотаТекущая частотаПроизводительАртикулСтатус
${escapeHtml(mem.location || mem.slot)} ${present} ${sizeGB} GB ${escapeHtml(mem.type || '-')} ${mem.max_speed_mhz || '-'} MHz ${mem.current_speed_mhz || mem.speed_mhz || '-'} MHz ${escapeHtml(mem.manufacturer || '-')} ${escapeHtml(mem.part_number || '-')} ${escapeHtml(mem.status || 'OK')}
'; } else { html += '

Нет данных о памяти

'; } html += '
'; // Power tab html += '
'; 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 += `

Блоки питания

${psuTotal}Всего
${psuPresent}Подключено
${psuOK}Работает
${psuWattage}WМощность
${escapeHtml(psuModel)}Модель
`; config.power_supplies.forEach(psu => { const statusClass = psu.status === 'OK' ? '' : 'status-warning'; html += ``; }); html += '
СлотПроизводительМодельМощностьВходВыходНапряжениеТемператураСтатус
${escapeHtml(psu.slot)} ${escapeHtml(psu.vendor || '-')} ${escapeHtml(psu.model || '-')} ${psu.wattage_w || '-'}W ${psu.input_power_w || '-'}W ${psu.output_power_w || '-'}W ${psu.input_voltage ? psu.input_voltage.toFixed(0) : '-'}V ${psu.temperature_c || '-'}°C ${escapeHtml(psu.status || '-')}
'; } else { html += '

Нет данных о блоках питания

'; } html += '
'; // Storage tab html += '
'; 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 += `

Накопители

${storTotal}Всего слотов
${config.storage.filter(s => s.present).length}Установлено
${totalTB > 0 ? totalTB + ' TB' : '-'}Объём
${typesSummary.join(', ') || '-'}По типам
`; config.storage.forEach(s => { const presentIcon = s.present ? '' : ''; const presentText = s.present ? 'Present' : 'Empty'; html += ``; }); html += '
NO.СтатусРасположениеBackplane IDТипМодельРазмерСерийный номер
${escapeHtml(s.slot || '-')} ${presentIcon} ${presentText} ${escapeHtml(s.location || '-')} ${s.backplane_id !== undefined ? s.backplane_id : '-'} ${escapeHtml(s.type || '-')} ${escapeHtml(s.model || '-')} ${s.size_gb > 0 ? s.size_gb + ' GB' : '-'} ${s.serial_number ? '' + escapeHtml(s.serial_number) + '' : '-'}
'; } else { html += '

Нет данных о накопителях

'; } html += '
'; // GPU tab html += '
'; 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 += `

Графические процессоры

${gpuCount}Всего GPU
${escapeHtml(gpuVendor)}Производитель
${escapeHtml(gpuModel)}Модель
`; 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 += ``; }); html += '
СлотМодельПроизводительBDFPCIeСерийный номер
${escapeHtml(gpu.slot || '-')} ${escapeHtml(gpu.model || '-')} ${escapeHtml(gpu.manufacturer || '-')} ${escapeHtml(gpu.bdf || '-')} ${pcieLink} ${escapeHtml(gpu.serial_number || '-')}
'; } else { html += '

Нет GPU

'; } html += '
'; // Network tab html += '
'; 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 += `

Сетевые адаптеры

${nicCount}Адаптеров
${totalPorts}Портов
${nicTypes.join(', ') || '-'}Тип портов
${escapeHtml(nicModels.join(', ') || '-')}Модели
`; config.network_adapters.forEach(nic => { const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-'; const statusClass = nic.status === 'OK' ? '' : 'status-warning'; html += ``; }); html += '
СлотМодельПроизводительПортыТипMAC адресаСтатус
${escapeHtml(nic.location || nic.slot || '-')} ${escapeHtml(nic.model || '-')} ${escapeHtml(nic.vendor || '-')} ${nic.port_count || '-'} ${escapeHtml(nic.port_type || '-')} ${escapeHtml(macs)} ${escapeHtml(nic.status || '-')}
'; } else { html += '

Нет данных о сетевых адаптерах

'; } html += '
'; // PCIe Device Inventory tab html += '
'; if (config.pcie_devices && config.pcie_devices.length > 0) { html += '

PCIe устройства

'; config.pcie_devices.forEach(p => { const pcieLink = formatPCIeLink( p.link_width, p.link_speed, p.max_link_width, p.max_link_speed ); html += ``; }); html += '
СлотBDFТипПроизводительVendor:Device IDPCIe Link
${escapeHtml(p.slot || '-')} ${escapeHtml(p.bdf || '-')} ${escapeHtml(p.device_class || '-')} ${escapeHtml(p.manufacturer || '-')} ${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'} ${pcieLink}
'; } else { html += '

Нет данных о PCIe устройствах

'; } 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() { 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 = 'Нет данных о прошивках'; } else { firmware.forEach(fw => { const row = document.createElement('tr'); row.innerHTML = ` ${escapeHtml(fw.component)} ${escapeHtml(fw.model)} ${escapeHtml(fw.version)} `; 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 = '

Нет данных о сенсорах

'; 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 += `

${typeNames[type] || type}

`; 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 += `
${escapeHtml(s.name)} ${escapeHtml(valueStr)}
`; }); html += '
'; } 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 = 'Нет серийных номеров'; 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 = ` ${categoryNames[item.category] || item.category} ${escapeHtml(item.component)} ${escapeHtml(item.location || '-')} ${escapeHtml(item.serial_number)} ${escapeHtml(item.manufacturer || '-')} `; 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 = 'Нет событий'; return; } events.forEach(event => { const row = document.createElement('tr'); row.innerHTML = ` ${formatDate(event.timestamp)} ${escapeHtml(event.source)} ${escapeHtml(event.description)} ${event.severity} `; 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 = '

LOGPile

Приложение завершено. Можете закрыть эту вкладку.

'; } catch (err) { // Server shutdown, connection will fail document.body.innerHTML = '

LOGPile

Приложение завершено. Можете закрыть эту вкладку.

'; } } } // 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 `${curLinkStr} / ${maxLinkStr}`; } else if (maxLinkStr && maxLinkStr !== curLinkStr) { return `${curLinkStr} / ${maxLinkStr}`; } else { return curLinkStr; } }