// 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 += '
Спецификация сервера
';
spec.forEach(item => {
html += `- ${escapeHtml(item.category)} ${escapeHtml(item.name)} - ${item.quantity} шт.
`;
});
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)}Модель
| Socket | Модель | Ядра | Потоки | Частота | Max Turbo | TDP | L3 Cache | PPIN |
`;
config.cpus.forEach(cpu => {
html += `
| 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 || '-')} |
`;
});
html += '
';
} 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}Активно
| Location | Наличие | Размер | Тип | Max частота | Текущая частота | Производитель | Артикул | Статус |
`;
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 += `
| ${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')} |
`;
});
html += '
';
} 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 += `
| ${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 || '-')} |
`;
});
html += '
';
} 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(', ') || '-'}По типам
| NO. | Статус | Расположение | Backplane ID | Тип | Модель | Размер | Серийный номер |
`;
config.storage.forEach(s => {
const presentIcon = s.present ? '●' : '○';
const presentText = s.present ? 'Present' : 'Empty';
html += `
| ${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) + '' : '-'} |
`;
});
html += '
';
} 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)}Модель
| Слот | Модель | Производитель | BDF | PCIe | Серийный номер |
`;
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 += `
| ${escapeHtml(gpu.slot || '-')} |
${escapeHtml(gpu.model || '-')} |
${escapeHtml(gpu.manufacturer || '-')} |
${escapeHtml(gpu.bdf || '-')} |
${pcieLink} |
${escapeHtml(gpu.serial_number || '-')} |
`;
});
html += '
';
} 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(', ') || '-')}Модели
| Слот | Модель | Производитель | Порты | Тип | MAC адреса | Статус |
`;
config.network_adapters.forEach(nic => {
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
html += `
| ${escapeHtml(nic.location || nic.slot || '-')} |
${escapeHtml(nic.model || '-')} |
${escapeHtml(nic.vendor || '-')} |
${nic.port_count || '-'} |
${escapeHtml(nic.port_type || '-')} |
${escapeHtml(macs)} |
${escapeHtml(nic.status || '-')} |
`;
});
html += '
';
} else {
html += '
Нет данных о сетевых адаптерах
';
}
html += '
';
// PCIe Device Inventory tab
html += '';
if (config.pcie_devices && config.pcie_devices.length > 0) {
html += '
PCIe устройства
| Слот | BDF | Тип | Производитель | Vendor:Device ID | PCIe Link |
';
config.pcie_devices.forEach(p => {
const pcieLink = formatPCIeLink(
p.link_width,
p.link_speed,
p.max_link_width,
p.max_link_speed
);
html += `
| ${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} |
`;
});
html += '
';
} 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;
}
}