v1.1.0: Parser versioning, server info, auto-browser, section overviews

- Add parser versioning with Version() method and version display on main screen
- Add server model and serial number to Configuration tab and TXT export
- Add auto-browser opening on startup with --no-browser flag
- Add Restart and Exit buttons with graceful shutdown
- Add section overview stats (CPU, Power, Storage, GPU, Network)
- Change PCIe Link display to "x16 PCIe Gen4" format
- Add Location column to Serials section
- Extract BoardInfo from FRU and PlatformId from ThermalConfig

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 13:49:43 +03:00
parent e52eb909f7
commit c7422e95aa
11 changed files with 507 additions and 61 deletions

View File

@@ -88,6 +88,48 @@ main {
color: #27ae60;
}
/* Parsers info */
.parsers-info {
margin-top: 1.5rem;
text-align: center;
}
.parsers-title {
font-size: 0.85rem;
color: #666;
margin-bottom: 0.5rem;
}
.parsers-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
justify-content: center;
}
.parser-item {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #f8f9fa;
padding: 0.4rem 0.8rem;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.parser-name {
font-size: 0.85rem;
color: #2c3e50;
}
.parser-version {
font-size: 0.75rem;
color: #888;
background: #e8e8e8;
padding: 0.1rem 0.4rem;
border-radius: 3px;
}
/* Tabs */
.tabs {
display: flex;
@@ -375,6 +417,13 @@ footer {
padding: 2rem;
}
.footer-buttons {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.footer-info {
margin-top: 1rem;
font-size: 0.8rem;
@@ -403,6 +452,32 @@ footer {
background: #c0392b;
}
#restart-btn {
background: #3498db;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
#restart-btn:hover {
background: #2980b9;
}
#exit-btn {
background: #95a5a6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
#exit-btn:hover {
background: #7f8c8d;
}
/* Utility */
.hidden {
display: none !important;
@@ -414,6 +489,40 @@ footer {
padding: 2rem;
}
/* Server info header */
.server-info {
background: #2c3e50;
color: white;
padding: 1rem 1.5rem;
border-radius: 8px 8px 0 0;
margin-bottom: 0;
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.server-info-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.server-info-label {
opacity: 0.8;
font-size: 0.875rem;
}
.server-info strong {
font-size: 1.1rem;
}
.server-info code {
background: rgba(255,255,255,0.15);
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 1rem;
}
/* Configuration tabs */
.config-tabs {
display: flex;
@@ -510,30 +619,46 @@ footer {
font-weight: bold;
}
/* Memory overview stats */
.memory-overview {
/* Section overview stats */
.memory-overview,
.section-overview {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.stat-box {
background: #f8f9fa;
padding: 1rem 1.5rem;
padding: 0.75rem 1.25rem;
border-radius: 8px;
text-align: center;
border-left: 4px solid #3498db;
min-width: 80px;
}
.stat-box.model-box {
flex-grow: 1;
text-align: left;
border-left-color: #27ae60;
}
.stat-box.model-box .stat-value {
font-size: 1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stat-value {
display: block;
font-size: 1.5rem;
font-size: 1.25rem;
font-weight: bold;
color: #2c3e50;
}
.stat-label {
font-size: 0.75rem;
font-size: 0.7rem;
color: #666;
text-transform: uppercase;
}

View File

@@ -4,8 +4,32 @@ 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');
@@ -145,6 +169,14 @@ function renderConfig(data) {
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>
@@ -155,7 +187,6 @@ function renderConfig(data) {
<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>
<button class="config-tab" data-config-tab="fw">Firmware</button>
</div>`;
// Specification tab
@@ -174,7 +205,18 @@ function renderConfig(data) {
// 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>';
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>
@@ -236,7 +278,20 @@ function renderConfig(data) {
// 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>';
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>
@@ -260,7 +315,22 @@ function renderConfig(data) {
// 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>';
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">${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>Слот</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>
@@ -281,7 +351,16 @@ function renderConfig(data) {
// 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>';
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 => {
html += `<tr>
<td>${escapeHtml(gpu.slot || '-')}</td>
@@ -301,7 +380,18 @@ function renderConfig(data) {
// 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>';
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';
@@ -326,13 +416,14 @@ function renderConfig(data) {
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);
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>
<td>${pcieLink}</td>
</tr>`;
});
html += '</tbody></table>';
@@ -341,9 +432,6 @@ function renderConfig(data) {
}
html += '</div>';
// Firmware tab (content will be populated after firmware loads)
html += '<div class="config-tab-content" id="config-fw"><div id="config-fw-content"><p class="no-data">Загрузка...</p></div></div>';
container.innerHTML = html;
// Initialize config sub-tabs
@@ -394,25 +482,6 @@ function renderFirmware(firmware) {
tbody.appendChild(row);
});
}
// Render in Config -> Firmware tab
const configFwContent = document.getElementById('config-fw-content');
if (configFwContent) {
if (!firmware || firmware.length === 0) {
configFwContent.innerHTML = '<p class="no-data">Нет данных о прошивках</p>';
} else {
let html = '<h3>Прошивки компонентов</h3><table class="config-table"><thead><tr><th>Компонент</th><th>Модель</th><th>Версия</th></tr></thead><tbody>';
firmware.forEach(fw => {
html += `<tr>
<td>${escapeHtml(fw.component)}</td>
<td>${escapeHtml(fw.model)}</td>
<td><code>${escapeHtml(fw.version)}</code></td>
</tr>`;
});
html += '</tbody></table>';
configFwContent.innerHTML = html;
}
}
}
async function loadSensors() {
@@ -528,9 +597,9 @@ function renderSerials(serials) {
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>
<td>${escapeHtml(item.part_number || '-')}</td>
`;
tbody.appendChild(row);
});
@@ -606,6 +675,28 @@ async function clearData() {
}
}
// 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 '-';
@@ -619,3 +710,24 @@ function escapeHtml(text) {
div.textContent = text;
return div.innerHTML;
}
function formatPCIeLink(width, speed) {
if (!width && !speed) return '-';
// Convert GT/s to PCIe generation
let gen = '';
if (speed) {
const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
if (gtMatch) {
const gts = parseFloat(gtMatch[1]);
if (gts >= 32) gen = 'Gen5';
else if (gts >= 16) gen = 'Gen4';
else if (gts >= 8) gen = 'Gen3';
else if (gts >= 5) gen = 'Gen2';
else if (gts >= 2.5) gen = 'Gen1';
}
}
const widthStr = width ? `x${width}` : '';
return gen ? `${widthStr} PCIe ${gen}` : `${widthStr} ${speed || ''}`;
}