Add parse errors tab and improve error diagnostics UI
This commit is contained in:
@@ -473,6 +473,12 @@ table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
@@ -488,6 +494,54 @@ tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
#parse-errors-table {
|
||||
min-width: 980px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
#parse-errors-table th:nth-child(1),
|
||||
#parse-errors-table td:nth-child(1) {
|
||||
width: 92px;
|
||||
}
|
||||
|
||||
#parse-errors-table th:nth-child(2),
|
||||
#parse-errors-table td:nth-child(2) {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
#parse-errors-table th:nth-child(3),
|
||||
#parse-errors-table td:nth-child(3) {
|
||||
width: 95px;
|
||||
}
|
||||
|
||||
#parse-errors-table th:nth-child(4),
|
||||
#parse-errors-table td:nth-child(4) {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
#parse-errors-table th:nth-child(5),
|
||||
#parse-errors-table td:nth-child(5) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#parse-errors-table td,
|
||||
#parse-errors-table th {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#parse-errors-table td code {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#parse-errors-table td:last-child {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.85em;
|
||||
|
||||
@@ -590,6 +590,7 @@ function initFilters() {
|
||||
let allSensors = [];
|
||||
let allEvents = [];
|
||||
let allSerials = [];
|
||||
let allParseErrors = [];
|
||||
|
||||
let currentVendor = '';
|
||||
|
||||
@@ -622,7 +623,8 @@ async function loadData(vendor, filename) {
|
||||
loadFirmware(),
|
||||
loadSensors(),
|
||||
loadSerials(),
|
||||
loadEvents()
|
||||
loadEvents(),
|
||||
loadParseErrors()
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -647,7 +649,6 @@ function renderConfig(data) {
|
||||
const config = data.hardware || data;
|
||||
const spec = data.specification;
|
||||
const redfishFetchErrors = Array.isArray(data.redfish_fetch_errors) ? data.redfish_fetch_errors : [];
|
||||
const visibleRedfishFetchErrors = filterVisibleRedfishFetchErrors(redfishFetchErrors);
|
||||
const devices = Array.isArray(config.devices) ? config.devices : [];
|
||||
const volumes = Array.isArray(config.volumes) ? config.volumes : [];
|
||||
|
||||
@@ -701,21 +702,6 @@ function renderConfig(data) {
|
||||
<p class="no-data" style="margin-top: 0;">${escapeHtml(partialInventory)}</p>
|
||||
</div>`;
|
||||
}
|
||||
if (visibleRedfishFetchErrors.length > 0) {
|
||||
html += `<div class="spec-section">
|
||||
<h3>Redfish fetch errors (${visibleRedfishFetchErrors.length})</h3>
|
||||
<p class="no-data" style="margin-top: 0;">Сохранено в raw snapshot для последующего анализа в GUI.</p>
|
||||
<table class="config-table"><thead><tr><th>Endpoint</th><th>Ошибка</th></tr></thead><tbody>`;
|
||||
visibleRedfishFetchErrors.forEach(item => {
|
||||
const path = item && typeof item === 'object' ? (item.path || '-') : '-';
|
||||
const err = item && typeof item === 'object' ? (item.error || '-') : String(item || '-');
|
||||
html += `<tr>
|
||||
<td><code>${escapeHtml(String(path))}</code></td>
|
||||
<td>${escapeHtml(String(err))}</td>
|
||||
</tr>`;
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
if (spec && spec.length > 0) {
|
||||
html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
|
||||
spec.forEach(item => {
|
||||
@@ -800,7 +786,7 @@ function renderConfig(data) {
|
||||
<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>
|
||||
<th>Location</th><th>Наличие</th><th>Размер</th><th>Тип</th><th>Max частота</th><th>Текущая частота</th><th>Производитель</th><th>Артикул</th><th>Серийный номер</th><th>Статус</th>
|
||||
</tr></thead><tbody>`;
|
||||
memory.forEach(mem => {
|
||||
const present = mem.present !== false ? '✓' : '-';
|
||||
@@ -817,6 +803,7 @@ function renderConfig(data) {
|
||||
<td>${(mem.details && mem.details.current_speed_mhz) || mem.speed_mhz || '-'} MHz</td>
|
||||
<td>${escapeHtml(mem.manufacturer || '-')}</td>
|
||||
<td><code>${escapeHtml(mem.part_number || '-')}</code></td>
|
||||
<td><code>${escapeHtml(mem.serial_number || '-')}</code></td>
|
||||
<td class="${statusClass}">${escapeHtml(mem.status || 'OK')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
@@ -1260,6 +1247,47 @@ async function loadEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadParseErrors() {
|
||||
try {
|
||||
const response = await fetch('/api/parse-errors');
|
||||
const payload = await response.json();
|
||||
allParseErrors = Array.isArray(payload && payload.items) ? payload.items : [];
|
||||
renderParseErrors(allParseErrors);
|
||||
} catch (err) {
|
||||
console.error('Failed to load parse errors:', err);
|
||||
allParseErrors = [];
|
||||
renderParseErrors([]);
|
||||
}
|
||||
}
|
||||
|
||||
function renderParseErrors(items) {
|
||||
const tbody = document.querySelector('#parse-errors-table tbody');
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="no-data">Ошибок разбора не обнаружено</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
const row = document.createElement('tr');
|
||||
const severity = (item.severity || 'info').toLowerCase();
|
||||
const source = item.source || '-';
|
||||
const category = item.category || '-';
|
||||
const path = item.path || '-';
|
||||
const message = item.message || item.detail || '-';
|
||||
row.innerHTML = `
|
||||
<td>${escapeHtml(source)}</td>
|
||||
<td>${escapeHtml(category)}</td>
|
||||
<td><span class="severity ${escapeHtml(severity)}">${escapeHtml(severity)}</span></td>
|
||||
<td><code>${escapeHtml(path)}</code></td>
|
||||
<td>${escapeHtml(message)}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderEvents(events) {
|
||||
const tbody = document.querySelector('#events-table tbody');
|
||||
tbody.innerHTML = '';
|
||||
@@ -1306,6 +1334,7 @@ async function clearData() {
|
||||
allSensors = [];
|
||||
allEvents = [];
|
||||
allSerials = [];
|
||||
allParseErrors = [];
|
||||
} catch (err) {
|
||||
console.error('Failed to clear data:', err);
|
||||
}
|
||||
@@ -1347,19 +1376,6 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function filterVisibleRedfishFetchErrors(items) {
|
||||
if (!Array.isArray(items)) return [];
|
||||
return items.filter(item => {
|
||||
const message = String(item && typeof item === 'object' ? (item.error || '') : item || '').toLowerCase();
|
||||
return !(
|
||||
message.startsWith('status 404 ') ||
|
||||
message.startsWith('status 405 ') ||
|
||||
message.startsWith('status 410 ') ||
|
||||
message.startsWith('status 501 ')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function detectPartialRedfishInventory({ cpus, memory, redfishFetchErrors }) {
|
||||
const errors = Array.isArray(redfishFetchErrors) ? redfishFetchErrors : [];
|
||||
const paths = errors.map(item => String(item && typeof item === 'object' ? (item.path || '') : '')).filter(Boolean);
|
||||
|
||||
Reference in New Issue
Block a user