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:
2026-06-24 17:30:50 +03:00
parent ea68318744
commit fc9b446d2e
2 changed files with 41 additions and 9 deletions

View File

@@ -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">
<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-src-status" style="display:none;margin-bottom:10px"></div>
<div id="fru-all-table"></div>
</div></div>
<style>
@@ -480,6 +481,31 @@ var SOURCES = [
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');}
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() {
var status = document.getElementById('fru-all-status');
var table = document.getElementById('fru-all-table');
@@ -494,14 +520,18 @@ window.fruAllRead = function() {
Promise.allSettled(fetches).then(function(results) {
var rows = '';
var totalFields = 0;
var failedSources = [];
var perSource = [];
results.forEach(function(res, i) {
var src = SOURCES[i];
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;
}
perSource.push({src:src, ok:true, count:res.value.length, reason:''});
res.value.forEach(function(f) {
var val = esc(src.fieldValue(f));
var ro = src.readOnly(f);
@@ -526,16 +556,17 @@ window.fruAllRead = function() {
});
});
if (totalFields === 0 && failedSources.length > 0) {
status.textContent = 'No sources available: ' + failedSources.join('; ');
renderSrcStatus(perSource);
if (totalFields === 0) {
status.textContent = 'No editable fields available — see per-source status below.';
status.style.color = 'var(--crit-fg,#9f3a38)';
table.innerHTML = '';
return;
}
table.innerHTML = '<table style="width:100%;border-collapse:collapse">'+rows+'</table>';
var msg = totalFields + ' field(s) loaded';
if (failedSources.length > 0) msg += ' (skipped: ' + failedSources.join(', ') + ')';
status.textContent = msg;
status.textContent = totalFields + ' field(s) loaded';
status.style.color = 'var(--muted)';
});
};

View File

@@ -1227,7 +1227,8 @@ func TestDashboardRendersRuntimeHealthTable(t *testing.T) {
],
"services":[
{"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 {
@@ -1281,7 +1282,7 @@ func TestDashboardRendersRuntimeHealthTable(t *testing.T) {
`Bee Services`,
`CUDA runtime is not ready for GPU SAT.`,
`Missing: nvidia-smi`,
`bee-nvidia=inactive`,
`bee-nvidia=failed`,
// Hardware Summary card — component health badges
`Hardware Summary`,
`>CPU<`,