feat: surface BMC collection errors in parse-errors panel and event log

When Inspur component.log sections return {"error":"...","code":N} instead
of hardware data, the parser now:
- stores them in AnalysisResult.CollectionErrors (new model field)
- mirrors each one into result.Events with Source="BMC/<section>"
  so the chart viewer event table shows the specific BMC module
- feeds them into /api/parse-errors as bmc_collection_error entries

UI adds a collapsible "Collection diagnostics" panel below the chart
iframe (outside /chart) that appears when /api/parse-errors returns
any items; resets on data clear.

Affected sections in this dump: HDD (1458), PCIe Devices (1458),
Network Adapters (1458), Disk Backplane.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-05-21 14:30:01 +03:00
parent 4f7b5b826a
commit 27373aa104
7 changed files with 258 additions and 6 deletions
+70
View File
@@ -933,3 +933,73 @@ code {
grid-template-columns: 1fr;
}
}
/* ── Parse / collection errors panel ───────────────────────────────────── */
.parse-errors-section {
margin: 0 auto;
max-width: 1200px;
padding: 0 1.5rem 1.5rem;
}
.parse-errors-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: .55rem .75rem;
background: var(--warn-bg);
border: 1px solid #f0e0c0;
border-radius: 6px 6px 0 0;
cursor: pointer;
user-select: none;
font-weight: 600;
font-size: .85rem;
color: var(--warn-fg);
}
.parse-errors-toggle {
font-size: .75rem;
opacity: .7;
}
.parse-errors-body {
border: 1px solid #f0e0c0;
border-top: none;
border-radius: 0 0 6px 6px;
overflow-x: auto;
}
.parse-errors-table {
width: 100%;
border-collapse: collapse;
font-size: .82rem;
}
.parse-errors-table th {
background: var(--surface-3);
padding: .4rem .65rem;
text-align: left;
font-weight: 600;
color: var(--muted);
border-bottom: 1px solid var(--border);
white-space: nowrap;
}
.parse-errors-table td {
padding: .38rem .65rem;
border-bottom: 1px solid var(--border-lite);
vertical-align: top;
}
.parse-errors-table tr:last-child td {
border-bottom: none;
}
.parse-error-row.parse-error-error td:first-child {
color: var(--crit-fg);
font-weight: 600;
}
.parse-error-row.parse-error-warning td:first-child {
color: #7a5200;
font-weight: 600;
}
+65
View File
@@ -1413,6 +1413,62 @@ async function loadData(vendor, filename) {
document.getElementById('header-log-meta').classList.remove('hidden');
loadAuditViewer();
loadParseErrors();
}
async function loadParseErrors() {
const section = document.getElementById('parse-errors-section');
const rows = document.getElementById('parse-errors-rows');
const title = document.getElementById('parse-errors-title');
if (!section || !rows) return;
let data;
try {
const resp = await fetch('/api/parse-errors');
if (!resp.ok) return;
data = await resp.json();
} catch (e) {
return;
}
const items = (data && data.items) ? data.items : [];
if (items.length === 0) {
section.classList.add('hidden');
return;
}
const errorCount = items.filter(i => i.severity === 'error').length;
const warnCount = items.filter(i => i.severity === 'warning').length;
const parts = [];
if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? 's' : ''}`);
if (warnCount > 0) parts.push(`${warnCount} warning${warnCount > 1 ? 's' : ''}`);
const otherCount = items.length - errorCount - warnCount;
if (otherCount > 0) parts.push(`${otherCount} notice${otherCount > 1 ? 's' : ''}`);
title.textContent = `Collection diagnostics — ${parts.join(', ')}`;
rows.innerHTML = '';
for (const item of items) {
const tr = document.createElement('tr');
tr.className = `parse-error-row parse-error-${item.severity || 'info'}`;
tr.innerHTML =
`<td>${escapeHtml(item.source || '')}</td>` +
`<td>${escapeHtml(item.path || item.category || '')}</td>` +
`<td>${escapeHtml(item.message || '')}</td>` +
`<td>${escapeHtml(item.detail || '')}</td>`;
rows.appendChild(tr);
}
section.classList.remove('hidden');
}
let parseErrorsCollapsed = false;
function toggleParseErrors() {
const body = document.getElementById('parse-errors-body');
const toggle = document.getElementById('parse-errors-toggle');
if (!body) return;
parseErrorsCollapsed = !parseErrorsCollapsed;
body.style.display = parseErrorsCollapsed ? 'none' : '';
toggle.textContent = parseErrorsCollapsed ? '▼' : '▲';
}
function loadAuditViewer() {
@@ -1468,6 +1524,15 @@ async function clearData() {
if (frame) {
frame.src = 'about:blank';
}
const parseErrSection = document.getElementById('parse-errors-section');
if (parseErrSection) parseErrSection.classList.add('hidden');
const parseErrRows = document.getElementById('parse-errors-rows');
if (parseErrRows) parseErrRows.innerHTML = '';
parseErrorsCollapsed = false;
const parseErrBody = document.getElementById('parse-errors-body');
if (parseErrBody) parseErrBody.style.display = '';
const parseErrToggle = document.getElementById('parse-errors-toggle');
if (parseErrToggle) parseErrToggle.textContent = '▲';
} catch (err) {
console.error('Failed to clear data:', err);
}
+19
View File
@@ -170,6 +170,25 @@
</iframe>
</div>
</section>
<section id="parse-errors-section" class="parse-errors-section hidden">
<div class="parse-errors-header" onclick="toggleParseErrors()">
<span id="parse-errors-title">Collection warnings</span>
<span id="parse-errors-toggle" class="parse-errors-toggle"></span>
</div>
<div id="parse-errors-body" class="parse-errors-body">
<table class="parse-errors-table">
<thead>
<tr>
<th>Source</th>
<th>Section</th>
<th>Message</th>
<th>Detail</th>
</tr>
</thead>
<tbody id="parse-errors-rows"></tbody>
</table>
</div>
</section>
</section>
</main>