Add parse errors tab and improve error diagnostics UI
This commit is contained in:
@@ -110,6 +110,12 @@ func (c *RedfishConnector) Collect(ctx context.Context, req Request, emit Progre
|
|||||||
if recoveredN := c.recoverCriticalRedfishDocsPlanB(ctx, criticalClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, emit); recoveredN > 0 {
|
if recoveredN := c.recoverCriticalRedfishDocsPlanB(ctx, criticalClient, req, baseURL, criticalPaths, rawTree, fetchErrMap, emit); recoveredN > 0 {
|
||||||
c.debugSnapshotf("critical plan-b recovered docs=%d", recoveredN)
|
c.debugSnapshotf("critical plan-b recovered docs=%d", recoveredN)
|
||||||
}
|
}
|
||||||
|
// Hide transient fetch errors for endpoints that were eventually recovered into rawTree.
|
||||||
|
for p := range fetchErrMap {
|
||||||
|
if _, ok := rawTree[p]; ok {
|
||||||
|
delete(fetchErrMap, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
if emit != nil {
|
if emit != nil {
|
||||||
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: анализ raw snapshot..."})
|
emit(Progress{Status: "running", Progress: 99, Message: "Redfish: анализ raw snapshot..."})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -590,6 +590,203 @@ func (s *Server) handleGetFirmware(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonResponse(w, buildFirmwareEntries(result.Hardware))
|
jsonResponse(w, buildFirmwareEntries(result.Hardware))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type parseErrorEntry struct {
|
||||||
|
Source string `json:"source"` // redfish | parser | file | collect_log
|
||||||
|
Category string `json:"category"` // fetch | partial_inventory | collect_log
|
||||||
|
Severity string `json:"severity,omitempty"` // error | warning | info
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Detail string `json:"detail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleGetParseErrors(w http.ResponseWriter, r *http.Request) {
|
||||||
|
result := s.GetResult()
|
||||||
|
rawPkg := s.GetRawExport()
|
||||||
|
|
||||||
|
items := make([]parseErrorEntry, 0)
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
add := func(e parseErrorEntry) {
|
||||||
|
key := strings.TrimSpace(e.Source) + "|" + strings.TrimSpace(e.Category) + "|" + strings.TrimSpace(e.Path) + "|" + strings.TrimSpace(e.Message)
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
items = append(items, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fetchErrMap map[string]string
|
||||||
|
if result != nil && result.RawPayloads != nil {
|
||||||
|
fetchErrMap = extractRedfishFetchErrors(result.RawPayloads["redfish_fetch_errors"])
|
||||||
|
for path, msg := range fetchErrMap {
|
||||||
|
add(parseErrorEntry{
|
||||||
|
Source: "redfish",
|
||||||
|
Category: "fetch",
|
||||||
|
Severity: parseErrorSeverityFromMessage(msg),
|
||||||
|
Path: path,
|
||||||
|
Message: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawPkg != nil && len(rawPkg.Source.CollectLogs) > 0 {
|
||||||
|
for _, line := range rawPkg.Source.CollectLogs {
|
||||||
|
if !looksLikeErrorLogLine(line) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
add(parseErrorEntry{
|
||||||
|
Source: "collect_log",
|
||||||
|
Category: "collect_log",
|
||||||
|
Severity: parseErrorSeverityFromMessage(line),
|
||||||
|
Message: strings.TrimSpace(line),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result != nil && result.Protocol == "redfish" && result.Hardware != nil {
|
||||||
|
hw := result.Hardware
|
||||||
|
if len(hw.Memory) == 0 && hasFetchErrorSuffix(fetchErrMap, "/Memory") {
|
||||||
|
add(parseErrorEntry{
|
||||||
|
Source: "parser",
|
||||||
|
Category: "partial_inventory",
|
||||||
|
Severity: "warning",
|
||||||
|
Path: "/redfish/v1/Systems/*/Memory",
|
||||||
|
Message: "Memory inventory is empty because Redfish Memory endpoint failed during collection",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(hw.CPUs) == 0 && hasFetchErrorSuffix(fetchErrMap, "/Processors") {
|
||||||
|
add(parseErrorEntry{
|
||||||
|
Source: "parser",
|
||||||
|
Category: "partial_inventory",
|
||||||
|
Severity: "warning",
|
||||||
|
Path: "/redfish/v1/Systems/*/Processors",
|
||||||
|
Message: "CPU inventory is empty because Redfish Processors endpoint failed during collection",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(hw.Firmware) == 0 && (hasFetchErrorSuffix(fetchErrMap, "/Managers/1") || hasFetchErrorSuffix(fetchErrMap, "/UpdateService")) {
|
||||||
|
add(parseErrorEntry{
|
||||||
|
Source: "parser",
|
||||||
|
Category: "partial_inventory",
|
||||||
|
Severity: "warning",
|
||||||
|
Message: "Firmware inventory may be incomplete due to Redfish Manager/UpdateService endpoint failures",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
if items[i].Severity != items[j].Severity {
|
||||||
|
// error > warning > info
|
||||||
|
return parseErrorSeverityRank(items[i].Severity) < parseErrorSeverityRank(items[j].Severity)
|
||||||
|
}
|
||||||
|
if items[i].Source != items[j].Source {
|
||||||
|
return items[i].Source < items[j].Source
|
||||||
|
}
|
||||||
|
if items[i].Category != items[j].Category {
|
||||||
|
return items[i].Category < items[j].Category
|
||||||
|
}
|
||||||
|
return items[i].Path < items[j].Path
|
||||||
|
})
|
||||||
|
|
||||||
|
jsonResponse(w, map[string]any{
|
||||||
|
"items": items,
|
||||||
|
"summary": map[string]any{
|
||||||
|
"total": len(items),
|
||||||
|
"source_kind": func() string {
|
||||||
|
if rawPkg != nil {
|
||||||
|
return rawPkg.Source.Kind
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRedfishFetchErrors(raw any) map[string]string {
|
||||||
|
out := make(map[string]string)
|
||||||
|
list, ok := raw.([]map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
for _, item := range list {
|
||||||
|
p := strings.TrimSpace(fmt.Sprintf("%v", item["path"]))
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[p] = strings.TrimSpace(fmt.Sprintf("%v", item["error"]))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
if generic, ok := raw.([]interface{}); ok {
|
||||||
|
for _, itemAny := range generic {
|
||||||
|
item, ok := itemAny.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := strings.TrimSpace(fmt.Sprintf("%v", item["path"]))
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[p] = strings.TrimSpace(fmt.Sprintf("%v", item["error"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeErrorLogLine(line string) bool {
|
||||||
|
s := strings.ToLower(strings.TrimSpace(line))
|
||||||
|
if s == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(s, "ошибка") ||
|
||||||
|
strings.Contains(s, "error") ||
|
||||||
|
strings.Contains(s, "failed") ||
|
||||||
|
strings.Contains(s, "timeout") ||
|
||||||
|
strings.Contains(s, "deadline exceeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasFetchErrorSuffix(fetchErrs map[string]string, suffix string) bool {
|
||||||
|
if len(fetchErrs) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for p := range fetchErrs {
|
||||||
|
if strings.HasSuffix(p, suffix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseErrorSeverityFromMessage(msg string) string {
|
||||||
|
s := strings.ToLower(strings.TrimSpace(msg))
|
||||||
|
if s == "" {
|
||||||
|
return "info"
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "timeout") || strings.Contains(s, "deadline exceeded") {
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(s, "status 500 ") || strings.HasPrefix(s, "status 502 ") || strings.HasPrefix(s, "status 503 ") || strings.HasPrefix(s, "status 504 ") {
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(s, "status 401 ") || strings.HasPrefix(s, "status 403 ") {
|
||||||
|
return "error"
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(s, "status 404 ") || strings.HasPrefix(s, "status 405 ") || strings.HasPrefix(s, "status 410 ") || strings.HasPrefix(s, "status 501 ") {
|
||||||
|
return "info"
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "ошибка") || strings.Contains(s, "error") || strings.Contains(s, "failed") {
|
||||||
|
return "warning"
|
||||||
|
}
|
||||||
|
return "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseErrorSeverityRank(severity string) int {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(severity)) {
|
||||||
|
case "error":
|
||||||
|
return 0
|
||||||
|
case "warning":
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type firmwareEntry struct {
|
type firmwareEntry struct {
|
||||||
Component string `json:"component"`
|
Component string `json:"component"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ func (s *Server) setupRoutes() {
|
|||||||
s.mux.HandleFunc("GET /api/config", s.handleGetConfig)
|
s.mux.HandleFunc("GET /api/config", s.handleGetConfig)
|
||||||
s.mux.HandleFunc("GET /api/serials", s.handleGetSerials)
|
s.mux.HandleFunc("GET /api/serials", s.handleGetSerials)
|
||||||
s.mux.HandleFunc("GET /api/firmware", s.handleGetFirmware)
|
s.mux.HandleFunc("GET /api/firmware", s.handleGetFirmware)
|
||||||
|
s.mux.HandleFunc("GET /api/parse-errors", s.handleGetParseErrors)
|
||||||
s.mux.HandleFunc("GET /api/export/csv", s.handleExportCSV)
|
s.mux.HandleFunc("GET /api/export/csv", s.handleExportCSV)
|
||||||
s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON)
|
s.mux.HandleFunc("GET /api/export/json", s.handleExportJSON)
|
||||||
s.mux.HandleFunc("GET /api/export/reanimator", s.handleExportReanimator)
|
s.mux.HandleFunc("GET /api/export/reanimator", s.handleExportReanimator)
|
||||||
|
|||||||
@@ -473,6 +473,12 @@ table {
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-scroll {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -488,6 +494,54 @@ tr:hover {
|
|||||||
background: #f8f9fa;
|
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 {
|
code {
|
||||||
font-family: 'Monaco', 'Menlo', monospace;
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ function initFilters() {
|
|||||||
let allSensors = [];
|
let allSensors = [];
|
||||||
let allEvents = [];
|
let allEvents = [];
|
||||||
let allSerials = [];
|
let allSerials = [];
|
||||||
|
let allParseErrors = [];
|
||||||
|
|
||||||
let currentVendor = '';
|
let currentVendor = '';
|
||||||
|
|
||||||
@@ -622,7 +623,8 @@ async function loadData(vendor, filename) {
|
|||||||
loadFirmware(),
|
loadFirmware(),
|
||||||
loadSensors(),
|
loadSensors(),
|
||||||
loadSerials(),
|
loadSerials(),
|
||||||
loadEvents()
|
loadEvents(),
|
||||||
|
loadParseErrors()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,7 +649,6 @@ function renderConfig(data) {
|
|||||||
const config = data.hardware || data;
|
const config = data.hardware || data;
|
||||||
const spec = data.specification;
|
const spec = data.specification;
|
||||||
const redfishFetchErrors = Array.isArray(data.redfish_fetch_errors) ? data.redfish_fetch_errors : [];
|
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 devices = Array.isArray(config.devices) ? config.devices : [];
|
||||||
const volumes = Array.isArray(config.volumes) ? config.volumes : [];
|
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>
|
<p class="no-data" style="margin-top: 0;">${escapeHtml(partialInventory)}</p>
|
||||||
</div>`;
|
</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) {
|
if (spec && spec.length > 0) {
|
||||||
html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
|
html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
|
||||||
spec.forEach(item => {
|
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 class="stat-box"><span class="stat-value">${workingCount}</span><span class="stat-label">Активно</span></div>
|
||||||
</div>
|
</div>
|
||||||
<table class="config-table memory-table"><thead><tr>
|
<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>`;
|
</tr></thead><tbody>`;
|
||||||
memory.forEach(mem => {
|
memory.forEach(mem => {
|
||||||
const present = mem.present !== false ? '✓' : '-';
|
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>${(mem.details && mem.details.current_speed_mhz) || mem.speed_mhz || '-'} MHz</td>
|
||||||
<td>${escapeHtml(mem.manufacturer || '-')}</td>
|
<td>${escapeHtml(mem.manufacturer || '-')}</td>
|
||||||
<td><code>${escapeHtml(mem.part_number || '-')}</code></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>
|
<td class="${statusClass}">${escapeHtml(mem.status || 'OK')}</td>
|
||||||
</tr>`;
|
</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) {
|
function renderEvents(events) {
|
||||||
const tbody = document.querySelector('#events-table tbody');
|
const tbody = document.querySelector('#events-table tbody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
@@ -1306,6 +1334,7 @@ async function clearData() {
|
|||||||
allSensors = [];
|
allSensors = [];
|
||||||
allEvents = [];
|
allEvents = [];
|
||||||
allSerials = [];
|
allSerials = [];
|
||||||
|
allParseErrors = [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to clear data:', err);
|
console.error('Failed to clear data:', err);
|
||||||
}
|
}
|
||||||
@@ -1347,19 +1376,6 @@ function escapeHtml(text) {
|
|||||||
return div.innerHTML;
|
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 }) {
|
function detectPartialRedfishInventory({ cpus, memory, redfishFetchErrors }) {
|
||||||
const errors = Array.isArray(redfishFetchErrors) ? redfishFetchErrors : [];
|
const errors = Array.isArray(redfishFetchErrors) ? redfishFetchErrors : [];
|
||||||
const paths = errors.map(item => String(item && typeof item === 'object' ? (item.path || '') : '')).filter(Boolean);
|
const paths = errors.map(item => String(item && typeof item === 'object' ? (item.path || '') : '')).filter(Boolean);
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
<button class="tab" data-tab="sensors">Сенсоры</button>
|
<button class="tab" data-tab="sensors">Сенсоры</button>
|
||||||
<button class="tab" data-tab="serials">Серийные номера</button>
|
<button class="tab" data-tab="serials">Серийные номера</button>
|
||||||
<button class="tab" data-tab="events">События</button>
|
<button class="tab" data-tab="events">События</button>
|
||||||
|
<button class="tab" data-tab="parse-errors">Ошибки разбора</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="tab-content active" id="config">
|
<div class="tab-content active" id="config">
|
||||||
@@ -196,6 +197,26 @@
|
|||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content" id="parse-errors">
|
||||||
|
<div class="toolbar">
|
||||||
|
<span class="toolbar-label">Ошибки сборки / разбора (Redfish, parser, файл)</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table id="parse-errors-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Источник</th>
|
||||||
|
<th>Категория</th>
|
||||||
|
<th>Важность</th>
|
||||||
|
<th>Endpoint / Path</th>
|
||||||
|
<th>Сообщение</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user