webui: per-source status bar in FRU/Elabel card + fix stale runtime-health test
Show an explicit per-source status line after "Read All" instead of hiding
failed/blocked sources in a "(skipped: …)" tail. Sources blocked by a missing
Supermicro license (SFT-OOB-LIC / SFT-DCMS-SINGLE) are flagged in red with an
actionable message, so engineers see that SAA DMI is gated rather than silently
falling back to the futile ipmitool FRU path (BIOS re-syncs FRU from DMI on boot).
Also fix TestDashboardRendersRuntimeHealthTable, stale since 4f6579e moved
"inactive" to the OK service states: the fixture now uses a failed service and
the assertion matches the current contract (failed flagged, inactive not).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -413,6 +413,7 @@ func renderFRUEditorCard() string {
|
|||||||
return `<div class="card"><div class="card-head card-head-actions">FRU / Elabel<div class="card-head-buttons"><button class="btn btn-sm btn-secondary" onclick="fruAllRead()">Read All</button></div></div><div class="card-body">
|
return `<div class="card"><div class="card-head card-head-actions">FRU / Elabel<div class="card-head-buttons"><button class="btn btn-sm btn-secondary" onclick="fruAllRead()">Read All</button></div></div><div class="card-body">
|
||||||
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits hardware identity fields from all available sources. Each field shows its source method.</p>
|
<p style="font-size:13px;color:var(--muted);margin-bottom:12px">Reads and edits hardware identity fields from all available sources. Each field shows its source method.</p>
|
||||||
<div id="fru-all-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
<div id="fru-all-status" style="font-size:13px;color:var(--muted);margin-bottom:8px"></div>
|
||||||
|
<div id="fru-src-status" style="display:none;margin-bottom:10px"></div>
|
||||||
<div id="fru-all-table"></div>
|
<div id="fru-all-table"></div>
|
||||||
</div></div>
|
</div></div>
|
||||||
<style>
|
<style>
|
||||||
@@ -480,6 +481,31 @@ var SOURCES = [
|
|||||||
|
|
||||||
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
||||||
|
|
||||||
|
function renderSrcStatus(perSource) {
|
||||||
|
var bar = document.getElementById('fru-src-status');
|
||||||
|
if (!perSource.length) { bar.style.display = 'none'; bar.innerHTML = ''; return; }
|
||||||
|
var html = '';
|
||||||
|
perSource.forEach(function(p) {
|
||||||
|
var state, color;
|
||||||
|
if (p.ok) {
|
||||||
|
state = p.count + ' field(s) available';
|
||||||
|
color = 'var(--ok-fg,green)';
|
||||||
|
} else if (/not activated|product key|SFT-DCMS|SFT-OOB/i.test(p.reason)) {
|
||||||
|
state = 'requires Supermicro license (SFT-OOB-LIC / SFT-DCMS-SINGLE) — activate on BMC';
|
||||||
|
color = 'var(--crit-fg,#9f3a38)';
|
||||||
|
} else {
|
||||||
|
state = p.reason || 'unavailable';
|
||||||
|
color = 'var(--muted)';
|
||||||
|
}
|
||||||
|
html += '<div style="display:flex;align-items:center;gap:8px;font-size:12px;margin:3px 0">'
|
||||||
|
+ '<span class="fru-chip '+p.src.chipClass+'">'+p.src.label+'</span>'
|
||||||
|
+ '<span style="color:'+color+'">'+esc(state)+'</span>'
|
||||||
|
+ '</div>';
|
||||||
|
});
|
||||||
|
bar.innerHTML = html;
|
||||||
|
bar.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
window.fruAllRead = function() {
|
window.fruAllRead = function() {
|
||||||
var status = document.getElementById('fru-all-status');
|
var status = document.getElementById('fru-all-status');
|
||||||
var table = document.getElementById('fru-all-table');
|
var table = document.getElementById('fru-all-table');
|
||||||
@@ -494,14 +520,18 @@ window.fruAllRead = function() {
|
|||||||
Promise.allSettled(fetches).then(function(results) {
|
Promise.allSettled(fetches).then(function(results) {
|
||||||
var rows = '';
|
var rows = '';
|
||||||
var totalFields = 0;
|
var totalFields = 0;
|
||||||
var failedSources = [];
|
var perSource = [];
|
||||||
|
|
||||||
results.forEach(function(res, i) {
|
results.forEach(function(res, i) {
|
||||||
var src = SOURCES[i];
|
var src = SOURCES[i];
|
||||||
if (res.status === 'rejected' || !Array.isArray(res.value) || res.value.length === 0) {
|
if (res.status === 'rejected' || !Array.isArray(res.value) || res.value.length === 0) {
|
||||||
failedSources.push(src.label + (res.reason ? ': ' + res.reason.message : ''));
|
var reason = '';
|
||||||
|
if (res.status === 'rejected' && res.reason) reason = res.reason.message;
|
||||||
|
else reason = 'no editable fields returned';
|
||||||
|
perSource.push({src:src, ok:false, count:0, reason:reason});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
perSource.push({src:src, ok:true, count:res.value.length, reason:''});
|
||||||
res.value.forEach(function(f) {
|
res.value.forEach(function(f) {
|
||||||
var val = esc(src.fieldValue(f));
|
var val = esc(src.fieldValue(f));
|
||||||
var ro = src.readOnly(f);
|
var ro = src.readOnly(f);
|
||||||
@@ -526,16 +556,17 @@ window.fruAllRead = function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (totalFields === 0 && failedSources.length > 0) {
|
renderSrcStatus(perSource);
|
||||||
status.textContent = 'No sources available: ' + failedSources.join('; ');
|
|
||||||
|
if (totalFields === 0) {
|
||||||
|
status.textContent = 'No editable fields available — see per-source status below.';
|
||||||
status.style.color = 'var(--crit-fg,#9f3a38)';
|
status.style.color = 'var(--crit-fg,#9f3a38)';
|
||||||
|
table.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.innerHTML = '<table style="width:100%;border-collapse:collapse">'+rows+'</table>';
|
table.innerHTML = '<table style="width:100%;border-collapse:collapse">'+rows+'</table>';
|
||||||
var msg = totalFields + ' field(s) loaded';
|
status.textContent = totalFields + ' field(s) loaded';
|
||||||
if (failedSources.length > 0) msg += ' (skipped: ' + failedSources.join(', ') + ')';
|
|
||||||
status.textContent = msg;
|
|
||||||
status.style.color = 'var(--muted)';
|
status.style.color = 'var(--muted)';
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1227,7 +1227,8 @@ func TestDashboardRendersRuntimeHealthTable(t *testing.T) {
|
|||||||
],
|
],
|
||||||
"services":[
|
"services":[
|
||||||
{"name":"bee-web","status":"active"},
|
{"name":"bee-web","status":"active"},
|
||||||
{"name":"bee-nvidia","status":"inactive"}
|
{"name":"bee-audit","status":"inactive"},
|
||||||
|
{"name":"bee-nvidia","status":"failed"}
|
||||||
]
|
]
|
||||||
}`
|
}`
|
||||||
if err := os.WriteFile(filepath.Join(exportDir, "runtime-health.json"), []byte(health), 0644); err != nil {
|
if err := os.WriteFile(filepath.Join(exportDir, "runtime-health.json"), []byte(health), 0644); err != nil {
|
||||||
@@ -1281,7 +1282,7 @@ func TestDashboardRendersRuntimeHealthTable(t *testing.T) {
|
|||||||
`Bee Services`,
|
`Bee Services`,
|
||||||
`CUDA runtime is not ready for GPU SAT.`,
|
`CUDA runtime is not ready for GPU SAT.`,
|
||||||
`Missing: nvidia-smi`,
|
`Missing: nvidia-smi`,
|
||||||
`bee-nvidia=inactive`,
|
`bee-nvidia=failed`,
|
||||||
// Hardware Summary card — component health badges
|
// Hardware Summary card — component health badges
|
||||||
`Hardware Summary`,
|
`Hardware Summary`,
|
||||||
`>CPU<`,
|
`>CPU<`,
|
||||||
|
|||||||
Reference in New Issue
Block a user