- Redesign config page with tabs: Spec, CPU, Memory, Power, Storage, GPU, Network, PCIe - Parse detailed memory info from component.log with all fields: Location, Present, Size, Type, Max/Current Speed, Manufacturer, Part Number, Status - Add GPU model extraction from PCIe devices - Add NetworkAdapter model with detailed fields from RESTful API - Update PSU model with power metrics (input/output power, voltage, temperature) - Memory modules with 0GB size (failed) highlighted in warning color - Add memory overview stats (total GB, installed count, active count) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
594 lines
22 KiB
JavaScript
594 lines
22 KiB
JavaScript
// LOGPile Frontend Application
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initUpload();
|
||
initTabs();
|
||
initFilters();
|
||
});
|
||
|
||
// 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);
|
||
} 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) {
|
||
currentVendor = vendor || '';
|
||
document.getElementById('upload-section').classList.add('hidden');
|
||
document.getElementById('data-section').classList.remove('hidden');
|
||
document.getElementById('clear-btn').classList.remove('hidden');
|
||
|
||
// Update vendor badge if exists
|
||
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 = '';
|
||
|
||
// 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) {
|
||
html += '<h3>Процессоры</h3><table class="config-table"><thead><tr><th>Socket</th><th>Модель</th><th>Ядра</th><th>Потоки</th><th>Частота</th><th>Max Turbo</th><th>TDP</th><th>L3 Cache</th><th>PPIN</th></tr></thead><tbody>';
|
||
config.cpus.forEach(cpu => {
|
||
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) {
|
||
html += '<h3>Блоки питания</h3><table class="config-table"><thead><tr><th>Слот</th><th>Производитель</th><th>Модель</th><th>Мощность</th><th>Вход</th><th>Выход</th><th>Напряжение</th><th>Температура</th><th>Статус</th></tr></thead><tbody>';
|
||
config.power_supplies.forEach(psu => {
|
||
const statusClass = psu.status === 'OK' ? '' : 'status-warning';
|
||
html += `<tr>
|
||
<td>${escapeHtml(psu.slot)}</td>
|
||
<td>${escapeHtml(psu.vendor || '-')}</td>
|
||
<td>${escapeHtml(psu.model || '-')}</td>
|
||
<td>${psu.wattage_w || '-'}W</td>
|
||
<td>${psu.input_power_w || '-'}W</td>
|
||
<td>${psu.output_power_w || '-'}W</td>
|
||
<td>${psu.input_voltage ? psu.input_voltage.toFixed(0) : '-'}V</td>
|
||
<td>${psu.temperature_c || '-'}°C</td>
|
||
<td class="${statusClass}">${escapeHtml(psu.status || '-')}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
} else {
|
||
html += '<p class="no-data">Нет данных о блоках питания</p>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Storage tab
|
||
html += '<div class="config-tab-content" id="config-storage">';
|
||
if (config.storage && config.storage.length > 0) {
|
||
html += '<h3>Накопители</h3><table class="config-table"><thead><tr><th>Слот</th><th>Тип</th><th>Интерфейс</th><th>Модель</th><th>Производитель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>';
|
||
config.storage.forEach(s => {
|
||
html += `<tr>
|
||
<td>${escapeHtml(s.slot || '-')}</td>
|
||
<td>${escapeHtml(s.type || '-')}</td>
|
||
<td>${escapeHtml(s.interface || '-')}</td>
|
||
<td>${escapeHtml(s.model || '-')}</td>
|
||
<td>${escapeHtml(s.manufacturer || '-')}</td>
|
||
<td>${s.size_gb} GB</td>
|
||
<td><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) {
|
||
html += '<h3>Графические процессоры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>';
|
||
config.gpus.forEach(gpu => {
|
||
html += `<tr>
|
||
<td>${escapeHtml(gpu.slot || '-')}</td>
|
||
<td>${escapeHtml(gpu.model || '-')}</td>
|
||
<td>${escapeHtml(gpu.manufacturer || '-')}</td>
|
||
<td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
|
||
<td>x${gpu.link_width || '-'} ${escapeHtml(gpu.link_speed || '-')}</td>
|
||
<td><code>${escapeHtml(gpu.serial_number || '-')}</code></td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
} else {
|
||
html += '<p class="no-data">Нет GPU</p>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Network tab
|
||
html += '<div class="config-tab-content" id="config-network">';
|
||
if (config.network_adapters && config.network_adapters.length > 0) {
|
||
html += '<h3>Сетевые адаптеры</h3><table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>';
|
||
config.network_adapters.forEach(nic => {
|
||
const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
|
||
const statusClass = nic.status === 'OK' ? '' : 'status-warning';
|
||
html += `<tr>
|
||
<td>${escapeHtml(nic.location || nic.slot || '-')}</td>
|
||
<td>${escapeHtml(nic.model || '-')}</td>
|
||
<td>${escapeHtml(nic.vendor || '-')}</td>
|
||
<td>${nic.port_count || '-'}</td>
|
||
<td>${escapeHtml(nic.port_type || '-')}</td>
|
||
<td><code>${escapeHtml(macs)}</code></td>
|
||
<td class="${statusClass}">${escapeHtml(nic.status || '-')}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
} else {
|
||
html += '<p class="no-data">Нет данных о сетевых адаптерах</p>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// PCIe Device Inventory tab
|
||
html += '<div class="config-tab-content" id="config-pcie">';
|
||
if (config.pcie_devices && config.pcie_devices.length > 0) {
|
||
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 => {
|
||
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>x${p.link_width || '-'} ${escapeHtml(p.link_speed || '-')}</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);
|
||
}
|
||
}
|
||
|
||
function renderFirmware(firmware) {
|
||
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>';
|
||
return;
|
||
}
|
||
|
||
firmware.forEach(fw => {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>${escapeHtml(fw.device_name)}</td>
|
||
<td><code>${escapeHtml(fw.version)}</code></td>
|
||
<td>${escapeHtml(fw.build_date || '-')}</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': 'БП',
|
||
'FRU': 'FRU'
|
||
};
|
||
|
||
serials.forEach(item => {
|
||
if (!item.serial_number) 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><code>${escapeHtml(item.serial_number)}</code></td>
|
||
<td>${escapeHtml(item.manufacturer || '-')}</td>
|
||
<td>${escapeHtml(item.part_number || '-')}</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);
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
}
|