2026-07-17 15:15:57 +02:00
/* OME Cockpit — Reports / export / compliance drawer */
( function () {
const $ = ( sel , root = document ) => root . querySelector ( sel );
const $$ = ( sel , root = document ) => [... root . querySelectorAll ( sel )];
const state = {
2026-07-18 01:18:14 +02:00
tab : "analytics" ,
2026-07-17 15:15:57 +02:00
fleet : null ,
compliance : null ,
warranties : null ,
reportDefs : null ,
jobs : null ,
catalogs : null ,
brief : null ,
outdatedOnly : true ,
search : "" ,
2026-07-18 01:18:14 +02:00
hwDeviceId : null ,
filters : {
model : "" ,
connected : "all" , // all | yes | no
severity : "all" , // all | critical | warning
component : "all" , // all | BIOS | iDRAC | ...
warranty : "all" , // all | expired | d30 | d90 | ok
category : "" ,
},
};
const TAB_HELP = {
analytics : "Live KPIs for fleet health, Dell catalog compliance and warranty. Click a KPI to jump to a filtered view." ,
hardware : "Current physical inventory (OME) vs separate Dell max-expansion recommendations. RDP IPs resolved from OS hostname when available." ,
fleet : "Full inventory matrix with Service Tags. Filter by model and connection, then export CSV/JSON." ,
firmware : "Current vs Dell catalog versions (BIOS/iDRAC/…). Filter by component and severity; open Dell driver links." ,
warranty : "OME warranty records. Filter expired / expiring soon for customer attention." ,
ome : "Native OME ReportDefs, catalogs, baselines and recent jobs. Run a report or fetch results." ,
brief : "Printable customer one-pager with Service Tag appendix. Use Print → PDF for handoff." ,
2026-07-17 15:15:57 +02:00
};
function escapeHtml ( s ) {
return String ( s ?? "" )
. replace ( /&/g , "&" )
. replace ( /</g , "<" )
. replace ( />/g , ">" )
. replace ( /"/g , """ );
}
2026-07-18 01:18:14 +02:00
function escapeAttr ( s ) {
return escapeHtml ( s ). replace ( /'/g , "'" );
}
2026-07-17 15:15:57 +02:00
function fmtDate ( v ) {
if ( ! v ) return "—" ;
return String ( v ). replace ( /\.\d+$/ , "" ). slice ( 0 , 19 );
}
function badge ( status ) {
const s = String ( status || "" ). toLowerCase ();
let cls = "unk" ;
if ( s . includes ( "critical" ) || s === "outdated" || s . includes ( "non-compliant" )) cls = "bad" ;
else if ( s . includes ( "warning" )) cls = "warn" ;
else if ( s . includes ( "normal" ) || s === "current" || s . includes ( "compliant" ) || s === "ok" ) cls = "ok" ;
return `<span class="rpt-badge ${ cls } "> ${ escapeHtml ( status || "—" ) } </span>` ;
}
function downloadUrl ( kind , fmt ) {
const q = new URLSearchParams ({ fmt });
if ( state . outdatedOnly && kind === "firmware" ) q . set ( "force" , "0" );
return `/api/export/ ${ kind } ? ${ q . toString () } ` ;
}
async function apiGet ( url ) {
const r = await fetch ( url );
if ( ! r . ok ) throw new Error ( ` ${ r . status } ${ await r . text () } ` );
return r . json ();
}
function openReports () {
const drawer = $ ( "#reports-drawer" );
const scrim = $ ( "#scrim" );
if ( ! drawer ) return ;
2026-07-19 01:42:00 +02:00
[ "#chat-drawer" , "#ops-drawer" , "#ai-drawer" , "#network-drawer" , "#console-drawer" , "#team-drawer" ]. forEach (( id ) => {
2026-07-17 15:15:57 +02:00
const el = $ ( id );
if ( el ) {
el . classList . remove ( "open" );
el . setAttribute ( "aria-hidden" , "true" );
}
});
drawer . classList . add ( "open" );
drawer . setAttribute ( "aria-hidden" , "false" );
if ( scrim ) {
scrim . classList . add ( "open" );
scrim . dataset . mode = "reports-drawer" ;
}
loadTab ( state . tab );
}
function closeReports () {
const drawer = $ ( "#reports-drawer" );
const scrim = $ ( "#scrim" );
drawer ? . classList . remove ( "open" );
drawer ? . setAttribute ( "aria-hidden" , "true" );
if ( scrim ? . dataset . mode === "reports-drawer" ) {
scrim . classList . remove ( "open" );
delete scrim . dataset . mode ;
}
}
async function loadTab ( tab ) {
state . tab = tab ;
$$ ( "#reports-tabs .chip" ). forEach (( c ) => c . classList . toggle ( "active" , c . dataset . tab === tab ));
const mount = $ ( "#reports-mount" );
if ( ! mount ) return ;
mount . innerHTML = `<p class="hint">Loading ${ escapeHtml ( tab ) } …</p>` ;
try {
2026-07-18 01:18:14 +02:00
if ( tab === "analytics" ) {
const [ fleet , compliance , warranties , brief ] = await Promise . all ([
apiGet ( "/api/reports/fleet" ),
apiGet ( "/api/compliance" ),
apiGet ( "/api/warranties" ),
apiGet ( "/api/reports/brief" ),
]);
state . fleet = fleet ;
state . compliance = compliance ;
state . warranties = warranties ;
state . brief = brief ;
renderAnalytics ( mount );
} else if ( tab === "hardware" ) {
if ( ! state . fleet ) state . fleet = await apiGet ( "/api/reports/fleet" );
renderHardware ( mount );
} else if ( tab === "fleet" ) {
2026-07-17 15:15:57 +02:00
state . fleet = await apiGet ( "/api/reports/fleet" );
renderFleet ( mount );
} else if ( tab === "firmware" ) {
state . compliance = await apiGet ( "/api/compliance" );
renderFirmware ( mount );
} else if ( tab === "warranty" ) {
state . warranties = await apiGet ( "/api/warranties" );
renderWarranty ( mount );
} else if ( tab === "ome" ) {
const [ defs , jobs , catalogs , baselines ] = await Promise . all ([
apiGet ( "/api/ome/report-defs" ),
apiGet ( "/api/ome/jobs" ),
apiGet ( "/api/catalogs" ),
apiGet ( "/api/baselines" ),
]);
state . reportDefs = defs ;
state . jobs = jobs ;
state . catalogs = catalogs ;
renderOme ( mount , baselines );
} else if ( tab === "brief" ) {
state . brief = await apiGet ( "/api/reports/brief" );
renderBrief ( mount );
}
} catch ( err ) {
mount . innerHTML = `<p class="hint">Failed: ${ escapeHtml ( err . message ) } </p>` ;
}
}
2026-07-18 01:18:14 +02:00
function uniqueModels ( rows ) {
return [... new Set (( rows || []). map (( r ) => r . model ). filter ( Boolean ))]. sort (( a , b ) => a . localeCompare ( b ));
}
function applyBaseFilters ( rows , opts = {}) {
const f = state . filters ;
return ( rows || []). filter (( r ) => {
if ( f . model && r . model !== f . model ) return false ;
if ( f . connected === "yes" && ! r . connected ) return false ;
if ( f . connected === "no" && r . connected ) return false ;
if ( opts . searchFields && ! matchSearch ( r , opts . searchFields )) return false ;
return true ;
});
}
function filterBar ( opts = {}) {
const models = opts . models || [];
const showConnected = opts . connected !== false ;
const showSeverity = !! opts . severity ;
const showComponent = !! opts . componentOptions ;
const showWarranty = !! opts . warranty ;
const showDeviceSelect = !! opts . deviceSelect ;
const devices = opts . deviceSelect || [];
const help = TAB_HELP [ state . tab ] || "" ;
2026-07-17 15:15:57 +02:00
return `
2026-07-18 01:18:14 +02:00
<div class="rpt-sticky">
<div class="rpt-context"> ${ escapeHtml ( help ) } </div>
<div class="rpt-toolbar">
<input type="search" id="rpt-search" class="search" placeholder=" ${ escapeHtml ( opts . searchPlaceholder || "Filter name, service tag, IP, model…" ) } " value=" ${ escapeHtml ( state . search ) } " title="Type to filter the list instantly" />
<div class="rpt-exports">
${ opts . exports || "" }
<button type="button" class="btn ghost compact" id="rpt-print" title="Open browser print dialog — choose Save as PDF for a customer handout">Print / PDF</button>
<button type="button" class="btn ghost compact" id="rpt-clear-filters" title="Reset search and all filters on this tab">Clear filters</button>
</div>
</div>
<div class="rpt-filters" role="group" aria-label="Report filters">
${
showDeviceSelect
? `<label class="rpt-f" title="Jump directly to a server without scrolling the list">
Server
<select id="flt-device">
<option value="">Select server…</option>
${ devices
. map (
( d ) =>
`<option value=" ${ d . id } " ${ Number ( state . hwDeviceId ) === Number ( d . id ) ? "selected" : "" } > ${ escapeHtml (( d . service_tag || "—" ) + " · " + ( d . name || "" ). slice ( 0 , 40 )) } </option>`
)
. join ( "" ) }
</select>
</label>`
: ""
}
<label class="rpt-f" title="Limit results to one PowerEdge / chassis model">
Model
<select id="flt-model">
<option value="">All models</option>
${ models . map (( m ) => `<option value=" ${ escapeHtml ( m ) } " ${ state . filters . model === m ? "selected" : "" } > ${ escapeHtml ( m ) } </option>` ). join ( "" ) }
</select>
</label>
${
showConnected
? `<label class="rpt-f" title="Show only OME-connected or offline nodes">
Connection
<select id="flt-connected">
<option value="all" ${ state . filters . connected === "all" ? "selected" : "" } >All</option>
<option value="yes" ${ state . filters . connected === "yes" ? "selected" : "" } >Connected</option>
<option value="no" ${ state . filters . connected === "no" ? "selected" : "" } >Offline</option>
</select>
</label>`
: ""
}
${
showSeverity
? `<label class="rpt-f" title="Filter Dell catalog compliance severity">
Severity
<select id="flt-severity">
<option value="all" ${ state . filters . severity === "all" ? "selected" : "" } >All</option>
<option value="critical" ${ state . filters . severity === "critical" ? "selected" : "" } >Critical</option>
<option value="warning" ${ state . filters . severity === "warning" ? "selected" : "" } >Warning</option>
</select>
</label>`
: ""
}
${
showComponent
? `<label class="rpt-f" title="Show only one firmware family (BIOS, iDRAC, NIC, …)">
Component
<select id="flt-component">
<option value="all">All components</option>
${ ( opts . componentOptions || [])
. map (
( c ) =>
`<option value=" ${ escapeHtml ( c ) } " ${ state . filters . component === c ? "selected" : "" } > ${ escapeHtml ( c ) } </option>`
)
. join ( "" ) }
</select>
</label>`
: ""
}
${
showWarranty
? `<label class="rpt-f" title="Focus warranties that need attention">
Warranty
<select id="flt-warranty">
<option value="all" ${ state . filters . warranty === "all" ? "selected" : "" } >All</option>
<option value="expired" ${ state . filters . warranty === "expired" ? "selected" : "" } >Expired</option>
<option value="d30" ${ state . filters . warranty === "d30" ? "selected" : "" } >≤ 30 days</option>
<option value="d90" ${ state . filters . warranty === "d90" ? "selected" : "" } >31– 90 days</option>
<option value="ok" ${ state . filters . warranty === "ok" ? "selected" : "" } >> 90 days</option>
</select>
</label>`
: ""
}
${
opts . outdatedToggle
? `<label class="rpt-check" title="Hide components that already match the Dell catalog">
<input type="checkbox" id="rpt-outdated-only" ${ state . outdatedOnly ? "checked" : "" } /> Outdated only
</label>`
: ""
}
</div>
<div class="rpt-quick" id="rpt-quick">
${ ( opts . quick || [])
. map (
( q ) =>
`<button type="button" class="chip ${ q . active ? "active" : "" } " data-quick=" ${ escapeHtml ( q . id ) } " title=" ${ escapeHtml ( q . title || q . label ) } "> ${ escapeHtml ( q . label ) } </button>`
)
. join ( "" ) }
2026-07-17 15:15:57 +02:00
</div>
</div>` ;
}
2026-07-18 01:18:14 +02:00
function wireFilters ( mount , rerender ) {
const sync = () => {
state . search = $ ( "#rpt-search" ) ? . value || "" ;
state . filters . model = $ ( "#flt-model" ) ? . value || "" ;
state . filters . connected = $ ( "#flt-connected" ) ? . value || "all" ;
state . filters . severity = $ ( "#flt-severity" ) ? . value || "all" ;
state . filters . component = $ ( "#flt-component" ) ? . value || "all" ;
state . filters . warranty = $ ( "#flt-warranty" ) ? . value || "all" ;
const od = $ ( "#rpt-outdated-only" );
if ( od ) state . outdatedOnly = od . checked ;
rerender ();
};
$ ( "#rpt-search" ) ? . addEventListener ( "input" , sync );
[ "flt-model" , "flt-connected" , "flt-severity" , "flt-component" , "flt-warranty" , "rpt-outdated-only" ]. forEach (( id ) => {
$ ( `# ${ id } ` ) ? . addEventListener ( "change" , sync );
});
$ ( "#flt-device" ) ? . addEventListener ( "change" , ( e ) => {
const id = Number ( e . target . value );
if ( ! id ) return ;
state . hwDeviceId = id ;
rerender ();
});
$ ( "#rpt-clear-filters" ) ? . addEventListener ( "click" , () => {
state . search = "" ;
state . filters = { model : "" , connected : "all" , severity : "all" , component : "all" , warranty : "all" , category : "" };
state . outdatedOnly = state . tab === "firmware" ;
rerender ();
});
$ ( "#rpt-print" ) ? . addEventListener ( "click" , () => window . print ());
$ ( "#rpt-quick" ) ? . addEventListener ( "click" , ( e ) => {
const btn = e . target . closest ( "[data-quick]" );
if ( ! btn ) return ;
const id = btn . dataset . quick ;
if ( id && id . startsWith ( "cat:" )) {
state . filters . category = id . slice ( 4 );
rerender ();
return ;
}
if ( id === "connected" ) state . filters . connected = "yes" ;
if ( id === "offline" ) state . filters . connected = "no" ;
if ( id === "outdated" ) {
state . outdatedOnly = true ;
state . filters . severity = "critical" ;
if ( state . tab !== "firmware" ) loadTab ( "firmware" );
else rerender ();
return ;
}
if ( id === "warranty-risk" ) {
state . filters . warranty = "expired" ;
if ( state . tab !== "warranty" ) loadTab ( "warranty" );
else rerender ();
return ;
}
if ( id === "all" ) {
state . filters . connected = "all" ;
state . filters . severity = "all" ;
state . filters . warranty = "all" ;
state . filters . model = "" ;
state . filters . category = "" ;
state . filters . component = "all" ;
state . search = "" ;
}
if ( id === "bios" ) state . filters . component = "BIOS" ;
if ( id === "idrac" ) state . filters . component = "iDRAC" ;
rerender ();
});
}
function toolbar ( exports ) {
return filterBar ({ exports , models : uniqueModels ( state . fleet ? . rows || state . compliance ? . devices || []) });
}
2026-07-17 15:15:57 +02:00
function matchSearch ( row , fields ) {
const q = state . search . trim (). toLowerCase ();
if ( ! q ) return true ;
return fields . some (( f ) => String ( row [ f ] ?? "" ). toLowerCase (). includes ( q ));
}
2026-07-18 01:18:14 +02:00
function countBy ( items , keyFn ) {
const map = new Map ();
for ( const item of items ) {
const k = keyFn ( item ) || "Unknown" ;
map . set ( k , ( map . get ( k ) || 0 ) + 1 );
}
return [... map . entries ()]. sort (( a , b ) => b [ 1 ] - a [ 1 ]);
}
2026-07-18 21:56:30 +02:00
function barRows ( entries , maxN , fillClass , kind ) {
2026-07-18 01:18:14 +02:00
const top = entries . slice ( 0 , maxN );
const max = Math . max ( 1 , ... top . map (([, n ]) => n ));
return top
. map (
2026-07-18 21:56:30 +02:00
([ label , n ]) => `<button type="button" class="an-bar-row an-clickable" data-an-kind=" ${ escapeHtml ( kind || "bar" ) } " data-an-key=" ${ escapeHtml ( label ) } " data-an-count=" ${ n } " title="Open context for ${ escapeHtml ( label ) } ">
<div class="an-bar-label"> ${ escapeHtml ( label ) } </div>
2026-07-18 01:18:14 +02:00
<div class="an-bar-track"><div class="an-bar-fill ${ fillClass || "" } " style="width: ${ Math . round (( n / max ) * 100 ) } %"></div></div>
<div class="an-bar-val"> ${ n } </div>
2026-07-18 21:56:30 +02:00
</button>`
2026-07-18 01:18:14 +02:00
)
. join ( "" );
}
2026-07-18 21:56:30 +02:00
function donut ( slices , chartId ) {
2026-07-18 01:18:14 +02:00
const total = slices . reduce (( s , x ) => s + x . value , 0 ) || 1 ;
let acc = 0 ;
2026-07-18 21:56:30 +02:00
const parts = [];
const meta = [];
slices . forEach (( s ) => {
2026-07-18 01:18:14 +02:00
const start = ( acc / total ) * 100 ;
acc += s . value ;
const end = ( acc / total ) * 100 ;
2026-07-18 21:56:30 +02:00
parts . push ( ` ${ s . color } ${ start } % ${ end } %` );
meta . push ({
key : s . key || s . label ,
label : s . label ,
value : s . value ,
color : s . color ,
startPct : start ,
endPct : end ,
});
2026-07-18 01:18:14 +02:00
});
2026-07-18 21:56:30 +02:00
const legend = meta
2026-07-18 01:18:14 +02:00
. map (
( s ) =>
2026-07-18 21:56:30 +02:00
`<li><button type="button" class="an-legend-btn an-clickable" data-an-kind=" ${ escapeHtml ( chartId || "donut" ) } " data-an-key=" ${ escapeHtml ( s . key ) } " data-an-count=" ${ s . value } " title="Open context: ${ escapeHtml ( s . label ) } ">
<span class="an-swatch" style="background: ${ s . color } "></span>
<span class="an-legend-text"> ${ escapeHtml ( s . label ) } </span>
<strong> ${ s . value } </strong>
</button></li>`
2026-07-18 01:18:14 +02:00
)
. join ( "" );
2026-07-18 21:56:30 +02:00
return `<div class="an-donut-wrap" data-an-chart=" ${ escapeHtml ( chartId || "" ) } " data-an-meta=" ${ encodeURIComponent ( JSON . stringify ( meta )) } ">
<button type="button" class="an-donut an-donut-hit an-clickable" data-an-kind=" ${ escapeHtml ( chartId || "donut" ) } " data-an-donut="1" style="--slices: ${ parts . join ( ", " ) } " title="Click a colored segment for context" aria-label="Chart ${ escapeHtml ( chartId || "" ) } "></button>
2026-07-18 01:18:14 +02:00
<ul class="an-legend"> ${ legend } </ul>
</div>` ;
}
2026-07-18 21:56:30 +02:00
function closeAnContext () {
const modal = $ ( "#an-context-modal" );
if ( ! modal ) return ;
modal . classList . add ( "hidden" );
modal . setAttribute ( "aria-hidden" , "true" );
const scrim = $ ( "#scrim" );
if ( scrim ? . dataset . mode === "an-context" ) {
// keep reports drawer open underneath
if ( $ ( "#reports-drawer" ) ? . classList . contains ( "open" )) {
scrim . dataset . mode = "reports-drawer" ;
scrim . classList . add ( "open" );
} else {
scrim . classList . remove ( "open" );
delete scrim . dataset . mode ;
}
}
}
function openAnContext ({ title , sub , pills , rows , actions }) {
const modal = $ ( "#an-context-modal" );
if ( ! modal ) return ;
$ ( "#an-ctx-title" ). textContent = title || "Context" ;
$ ( "#an-ctx-sub" ). textContent = sub || "" ;
$ ( "#an-ctx-pills" ). innerHTML = ( pills || [])
. map (( p ) => `<span class="pill"> ${ escapeHtml ( p ) } </span>` )
. join ( "" );
const list = $ ( "#an-ctx-list" );
if ( ! rows ? . length ) {
list . innerHTML = `<p class="hint">No matching records in the current OME snapshot.</p>` ;
} else {
list . innerHTML = `
<table class="an-ctx-table">
<thead><tr> ${ ( rows [ 0 ]. cols || []). map (( c ) => `<th> ${ escapeHtml ( c ) } </th>` ). join ( "" ) } </tr></thead>
<tbody>
${ rows
. map (( r ) => {
const cells = ( r . cells || []). map (( c ) => `<td> ${ c } </td>` ). join ( "" );
const attr = r . jumpHw ? ` class="an-row-click" data-jump-hw=" ${ r . jumpHw } "` : "" ;
return `<tr ${ attr } > ${ cells } </tr>` ;
} )
.join("")}
</tbody>
</table>
${ rows . length >= 80 ? `<p class="an-note">Showing first ${ rows . length } rows.</p>` : "" } ` ;
}
$ ( "#an-ctx-actions" ). innerHTML = ( actions || [])
. map (
( a ) =>
`<button type="button" class="btn ${ a . primary ? "primary" : "compact" } " data-an-action=" ${ escapeHtml ( a . id ) } "> ${ escapeHtml ( a . label ) } </button>`
)
. join ( "" );
modal . classList . remove ( "hidden" );
modal . setAttribute ( "aria-hidden" , "false" );
const scrim = $ ( "#scrim" );
if ( scrim ) {
scrim . classList . add ( "open" );
scrim . dataset . mode = "an-context" ;
}
$ ( "#an-ctx-close" ) ? . addEventListener ( "click" , closeAnContext , { once : true });
$ ( "#an-ctx-actions" ) ? . querySelectorAll ( "[data-an-action]" ). forEach (( btn ) => {
btn . addEventListener ( "click" , () => {
const id = btn . dataset . anAction ;
const act = ( actions || []). find (( x ) => x . id === id );
if ( act ? . run ) act . run ();
closeAnContext ();
});
});
list . querySelectorAll ( "[data-jump-hw]" ). forEach (( el ) => {
el . addEventListener ( "click" , () => {
const id = Number ( el . dataset . jumpHw );
if ( ! id ) return ;
closeAnContext ();
state . hwDeviceId = id ;
loadTab ( "hardware" );
});
});
}
function sliceFromDonutClick ( btn , evt ) {
const wrap = btn . closest ( ".an-donut-wrap" );
let meta = [];
try {
meta = JSON . parse ( decodeURIComponent ( wrap ? . dataset . anMeta || "%5B%5D" ));
} catch ( _ ) {
meta = [];
}
if ( ! meta . length ) return null ;
const rect = btn . getBoundingClientRect ();
const cx = rect . left + rect . width / 2 ;
const cy = rect . top + rect . height / 2 ;
const dx = evt . clientX - cx ;
const dy = evt . clientY - cy ;
let deg = ( Math . atan2 ( dx , - dy ) * 180 ) / Math . PI ;
if ( deg < 0 ) deg += 360 ;
const pct = ( deg / 360 ) * 100 ;
return meta . find (( s ) => pct >= s . startPct && pct < s . endPct ) || meta [ meta . length - 1 ];
}
2026-07-18 01:18:14 +02:00
function renderAnalytics ( mount ) {
const fleet = state . fleet ? . rows || [];
const sum = state . fleet ? . summary || state . brief ? . summary || {};
const cs = state . compliance ? . summary || {};
const comps = state . compliance ? . components || [];
const devices = state . compliance ? . devices || [];
const warranties = state . warranties ? . items || [];
const connected = fleet . filter (( d ) => d . connected ). length ;
const offline = Math . max ( 0 , fleet . length - connected );
const outdatedDevs = cs . outdated_devices ?? devices . filter (( d ) => String ( d . compliance_status || "" ). toUpperCase () === "CRITICAL" ). length ;
const critComps = comps . filter (( c ) => String ( c . compliance_status || "" ). toUpperCase () === "CRITICAL" ). length ;
const warnComps = comps . filter (( c ) => String ( c . compliance_status || "" ). toUpperCase () === "WARNING" ). length ;
const upgradeComps = comps . filter (( c ) => String ( c . update_action || "" ). toUpperCase () === "UPGRADE" ). length ;
const expired = warranties . filter (( w ) => intOr ( w . days_remaining ) <= 0 ). length ;
const d30 = warranties . filter (( w ) => {
const d = intOr ( w . days_remaining );
return d > 0 && d <= 30 ;
}). length ;
const d90 = warranties . filter (( w ) => {
const d = intOr ( w . days_remaining );
return d > 30 && d <= 90 ;
}). length ;
const okW = warranties . filter (( w ) => intOr ( w . days_remaining ) > 90 ). length ;
const byComponent = countBy (
comps . filter (( c ) => String ( c . update_action || "" ). toUpperCase () === "UPGRADE" ),
( c ) => shortComponent ( c . component )
);
const byModel = countBy (
devices . filter (
( d ) =>
String ( d . compliance_status || "" ). toUpperCase () === "CRITICAL" ||
String ( d . firmware_status || "" ). toLowerCase (). includes ( "non-compliant" )
),
( d ) => d . model || "Unknown"
);
const byStatus = countBy ( comps , ( c ) => String ( c . compliance_status || "UNKNOWN" ). toUpperCase ());
const topOutdated = devices
. filter (
( d ) =>
String ( d . compliance_status || "" ). toUpperCase () === "CRITICAL" ||
String ( d . firmware_status || "" ). toLowerCase (). includes ( "non-compliant" )
)
. slice ( 0 , 8 );
mount . innerHTML = `
${ filterBar ({
models : uniqueModels ( fleet ),
connected : true ,
exports : `
<a class="btn compact" href=" ${ downloadUrl ( "firmware" , "csv" ) } " title="Download outdated/current firmware matrix as CSV">Firmware CSV</a>
<a class="btn compact" href=" ${ downloadUrl ( "fleet" , "csv" ) } " title="Download full fleet inventory CSV with Service Tags">Fleet CSV</a>
<button type="button" class="btn primary compact" id="an-refresh" title="Reload analytics from OME caches">Refresh</button>
` ,
quick : [
{ id : "all" , label : "All" , title : "Clear connection focus" } ,
{ id: "connected", label: "Connected", title: "Jump to fleet · connected only", active: state.filters.connected === "yes" },
{ id: "offline", label: "Offline", title: "Jump to fleet · offline only", active: state.filters.connected === "no" },
{ id: "outdated", label: "Outdated firmware", title: "Open Firmware tab filtered to critical upgrades" },
{ id: "warranty-risk", label: "Warranty risk", title: "Open Warranty tab · expired" },
],
})}
<div class="rpt-meta">Live OME analytics · baseline <strong> ${ escapeHtml ( cs . baseline_name || "—" ) } </strong> · last run ${ escapeHtml ( fmtDate ( cs . last_run )) } </div>
<div class="analytics-dash">
<div class="an-kpis">
<button type="button" class="an-kpi" data-jump="fleet" data-conn="all" title="Open Fleet inventory for all devices">
<em> ${ fleet . length || sum . total || 0 } </em><span>Fleet devices</span>
</button>
<button type="button" class="an-kpi" data-jump="fleet" data-conn="yes" title="Show only OME-connected systems">
<em> ${ connected } </em><span>Connected</span>
</button>
<button type="button" class="an-kpi" data-jump="fleet" data-conn="no" title="Show offline / not connected systems">
<em> ${ offline } </em><span>Offline</span>
</button>
<button type="button" class="an-kpi danger" data-jump="firmware" data-sev="critical" title="Open Firmware / BIOS · critical Dell catalog gaps">
<em> ${ outdatedDevs } </em><span>Outdated vs Dell</span>
</button>
<button type="button" class="an-kpi danger" data-jump="firmware" data-sev="critical" title="Critical component upgrades against Dell Online Baseline">
<em> ${ critComps } </em><span>Critical components</span>
</button>
<button type="button" class="an-kpi warn" data-jump="warranty" data-war="expired" title="Warranties expired or ≤ 30 days">
<em> ${ d30 + expired } </em><span>Warranty ≤30d / expired</span>
</button>
</div>
<div class="an-grid">
<section class="an-card">
<h3 title="Share of component compliance statuses from the Dell Online Baseline">Compliance mix</h3>
2026-07-18 21:56:30 +02:00
${ donut (
[
{ key : "CRITICAL" , label : "Critical" , value : critComps || Number ( byStatus . find (([ k ]) => k === "CRITICAL" ) ? .[ 1 ] || 0 ), color : "#ff5c5c" } ,
{ key: "WARNING", label: "Warning", value: warnComps || Number(byStatus.find(([k]) => k === "WARNING")?.[1] || 0), color: "#ffb84d" },
{
key: "OK",
label: "Other / OK",
value: Math.max(0, comps.length - critComps - warnComps),
color: "#3dffa0",
},
],
"compliance"
)}
<p class="an-note an-clickable" data-an-kind="compliance" data-an-key="UPGRADE" data-an-count=" ${ upgradeComps } " title="Show all UPGRADE components"> ${ upgradeComps } components marked UPGRADE against Dell catalog — click for list</p>
2026-07-18 01:18:14 +02:00
</section>
<section class="an-card">
<h3 title="Which firmware families need the most upgrades">Outdated by component</h3>
2026-07-18 21:56:30 +02:00
<div class="an-bars"> ${ barRows ( byComponent , 8 , "crit" , "component" ) || '<p class="an-note">No upgrade components</p>' } </div>
2026-07-18 01:18:14 +02:00
</section>
<section class="an-card">
<h3 title="Which server models appear most often in the non-compliant set">Outdated by model</h3>
2026-07-18 21:56:30 +02:00
<div class="an-bars"> ${ barRows ( byModel , 8 , "warn" , "model" ) || '<p class="an-note">No outdated models</p>' } </div>
2026-07-18 01:18:14 +02:00
</section>
<section class="an-card">
<h3 title="OME ConnectionState across the fleet snapshot">Connection state</h3>
2026-07-18 21:56:30 +02:00
${ donut (
[
{ key : "connected" , label : "Connected" , value : connected , color : "#3dffa0" } ,
{ key: "offline", label: "Offline", value: offline, color: "#e53935" },
],
"connection"
)}
2026-07-18 01:18:14 +02:00
<p class="an-note"> ${ escapeHtml ( String ( sum . total_watts != null ? Math . round ( sum . total_watts ) + " W sampled" : "Power samples from live poll" )) } </p>
</section>
<section class="an-card">
<h3 title="Days remaining buckets from OME WarrantyService">Warranty health</h3>
2026-07-18 21:56:30 +02:00
${ donut (
[
{ key : "expired" , label : "Expired" , value : expired , color : "#ff5c5c" } ,
{ key: "d30", label: "≤ 30 days", value: d30, color: "#ffb84d" },
{ key: "d90", label: "31– 90 days", value: d90, color: "#ff9a3c" },
{ key: "ok", label: "> 90 days", value: okW, color: "#3dffa0" },
],
"warranty"
)}
2026-07-18 01:18:14 +02:00
<p class="an-note"> ${ warranties . length } warranty records from OME</p>
</section>
<section class="an-card">
<h3 title="Highest-priority systems from the Dell baseline report">Top outdated systems</h3>
<table class="an-table-mini">
<thead><tr><th>Service Tag</th><th>Device</th><th>Status</th></tr></thead>
<tbody>
${
topOutdated
. map (
( d ) => `<tr class="an-row-click" data-jump-hw=" ${ d . device_id || "" } " title="Open Hardware / Expansion for this Service Tag">
<td><code class="st"> ${ escapeHtml ( d . service_tag || "—" ) } </code></td>
<td> ${ escapeHtml ( d . name || "—" ) } <div class="sub"> ${ escapeHtml ( d . model || "" ) } </div></td>
<td> ${ badge ( d . compliance_status || d . firmware_status ) } </td>
</tr>`
)
. join ( "" ) || `<tr><td colspan="3">No outdated devices in baseline</td></tr>`
}
</tbody>
</table>
<p class="an-note"><button type="button" class="btn compact" id="an-goto-firmware" title="Open full firmware compliance table">Open Firmware / BIOS</button></p>
</section>
</div>
</div>` ;
wireFilters ( mount , () => {
if ( state . filters . connected !== "all" ) {
loadTab ( "fleet" );
return ;
}
renderAnalytics ( mount );
});
$ ( "#an-refresh" ) ? . addEventListener ( "click" , () => loadTab ( "analytics" ));
$ ( "#an-goto-firmware" ) ? . addEventListener ( "click" , () => {
state . outdatedOnly = true ;
state . filters . severity = "critical" ;
loadTab ( "firmware" );
});
2026-07-18 21:56:30 +02:00
function ctxRowsFromComps ( list ) {
return list . slice ( 0 , 80 ). map (( c ) => ({
cols : [ "ST" , "Device" , "Component" , "Current → Catalog" , "Status" ],
cells : [
`<code class="st"> ${ escapeHtml ( c . service_tag || "—" ) } </code>` ,
escapeHtml ( c . device_name || "—" ),
escapeHtml ( shortComponent ( c . component )),
`<span class="mono"> ${ escapeHtml ( c . current_version || "?" ) } → ${ escapeHtml ( c . catalog_version || "?" ) } </span>` ,
badge ( c . compliance_status || c . update_action ),
],
jumpHw : c . device_id || null ,
}));
}
function ctxRowsFromDevices ( list ) {
return list . slice ( 0 , 80 ). map (( d ) => ({
cols : [ "ST" , "Name" , "Model" , "Status" ],
cells : [
`<code class="st"> ${ escapeHtml ( d . service_tag || "—" ) } </code>` ,
escapeHtml ( d . name || d . device_name || "—" ),
escapeHtml ( d . model || "—" ),
badge ( d . compliance_status || d . firmware_status || ( d . connected ? "connected" : "offline" )),
],
jumpHw : d . device_id || d . id || null ,
}));
}
function ctxRowsFromFleet ( list ) {
return list . slice ( 0 , 80 ). map (( d ) => ({
cols : [ "ST" , "Name" , "Model" , "iDRAC" , "State" ],
cells : [
`<code class="st"> ${ escapeHtml ( d . service_tag || "—" ) } </code>` ,
escapeHtml ( d . name || "—" ),
escapeHtml ( d . model || "—" ),
`<span class="mono"> ${ escapeHtml ( d . idrac_ip || d . ip || "—" ) } </span>` ,
d . connected
? `<span class="rpt-badge ok">online</span>`
: `<span class="rpt-badge bad">offline</span>` ,
],
jumpHw : d . id || d . device_id || null ,
}));
}
function ctxRowsFromWarranty ( list ) {
return list . slice ( 0 , 80 ). map (( w ) => ({
cols : [ "ST" , "Device" , "Level" , "Ends" , "Days left" ],
cells : [
`<code class="st"> ${ escapeHtml ( w . service_tag || "—" ) } </code>` ,
escapeHtml ( w . device_name || "—" ),
escapeHtml ( w . service_level || "—" ),
escapeHtml ( w . end_date || "—" ),
String ( intOr ( w . days_remaining )),
],
jumpHw : w . device_id || null ,
}));
}
function handleAnContext ( kind , key , count , evt ) {
let title = key ;
let sub = "" ;
let pills = [ ` ${ count ?? "?" } items` ];
let rows = [];
let actions = [];
if ( kind === "compliance" ) {
if ( key === "UPGRADE" ) {
const list = comps . filter (( c ) => String ( c . update_action || "" ). toUpperCase () === "UPGRADE" );
title = "UPGRADE components" ;
sub = "Dell Online Baseline · update_action = UPGRADE" ;
rows = ctxRowsFromComps ( list );
pills = [ ` ${ list . length } components` , `baseline ${ cs . baseline_name || "—" } ` ];
actions = [
{
id : "fw" ,
label : "Open Firmware / BIOS" ,
primary : true ,
run : () => {
state . outdatedOnly = true ;
loadTab ( "firmware" );
},
},
];
} else {
const list =
key === "OK"
? comps . filter (( c ) => {
const s = String ( c . compliance_status || "" ). toUpperCase ();
return s !== "CRITICAL" && s !== "WARNING" ;
})
: comps . filter (( c ) => String ( c . compliance_status || "" ). toUpperCase () === key );
title = `Compliance · ${ key } ` ;
sub = "Component compliance from Dell catalog baseline" ;
rows = ctxRowsFromComps ( list );
pills = [ ` ${ list . length } components` , key ];
actions = [
{
id : "fw" ,
label : "Open Firmware tab" ,
primary : true ,
run : () => {
state . outdatedOnly = key !== "OK" ;
state . filters . severity = key === "CRITICAL" ? "critical" : key === "WARNING" ? "warning" : "all" ;
loadTab ( "firmware" );
},
},
];
}
} else if ( kind === "component" ) {
const list = comps . filter (
( c ) =>
String ( c . update_action || "" ). toUpperCase () === "UPGRADE" && shortComponent ( c . component ) === key
);
title = `Outdated · ${ key } ` ;
sub = "Components marked UPGRADE in this firmware family" ;
rows = ctxRowsFromComps ( list );
pills = [ ` ${ list . length } components` , key ];
actions = [
{
id : "fw" ,
label : "Open Firmware filtered" ,
primary : true ,
run : () => {
state . outdatedOnly = true ;
state . filters . component = key ;
loadTab ( "firmware" );
},
},
];
} else if ( kind === "model" ) {
const list = devices . filter (
( d ) =>
( d . model || "Unknown" ) === key &&
( String ( d . compliance_status || "" ). toUpperCase () === "CRITICAL" ||
String ( d . firmware_status || "" ). toLowerCase (). includes ( "non-compliant" ))
);
title = `Outdated · ${ key } ` ;
sub = "Non-compliant systems for this model" ;
rows = ctxRowsFromDevices ( list );
pills = [ ` ${ list . length } systems` , key ];
actions = [
{
id : "fw" ,
label : "Open Firmware / BIOS" ,
primary : true ,
run : () => {
state . outdatedOnly = true ;
state . filters . model = key ;
loadTab ( "firmware" );
},
},
];
} else if ( kind === "connection" ) {
const list = fleet . filter (( d ) => ( key === "connected" ? d . connected : ! d . connected ));
title = key === "connected" ? "Connected systems" : "Offline systems" ;
sub = "OME ConnectionState from live fleet snapshot" ;
rows = ctxRowsFromFleet ( list );
pills = [ ` ${ list . length } devices` , key === "connected" ? "online" : "offline" ];
actions = [
{
id : "fleet" ,
label : "Open Fleet inventory" ,
primary : true ,
run : () => {
state . filters . connected = key === "connected" ? "yes" : "no" ;
loadTab ( "fleet" );
},
},
];
} else if ( kind === "warranty" ) {
const list = warranties . filter (( w ) => {
const d = intOr ( w . days_remaining );
if ( key === "expired" ) return d <= 0 ;
if ( key === "d30" ) return d > 0 && d <= 30 ;
if ( key === "d90" ) return d > 30 && d <= 90 ;
return d > 90 ;
});
const labels = { expired : "Expired" , d30 : "≤ 30 days" , d90 : "31– 90 days" , ok : "> 90 days" };
title = `Warranty · ${ labels [ key ] || key } ` ;
sub = "OME WarrantyService days_remaining buckets" ;
rows = ctxRowsFromWarranty ( list );
pills = [ ` ${ list . length } records` , labels [ key ] || key ];
actions = [
{
id : "war" ,
label : "Open Warranty tab" ,
primary : true ,
run : () => {
state . filters . warranty =
key === "expired" ? "expired" : key === "d30" ? "d30" : key === "d90" ? "d90" : "ok" ;
loadTab ( "warranty" );
},
},
];
} else if ( kind === "kpi" ) {
// handled separately
return ;
} else {
title = ` ${ kind } · ${ key } ` ;
sub = "Analytics selection" ;
}
openAnContext ({ title , sub , pills , rows , actions });
}
mount . querySelectorAll ( "[data-an-kind]" ). forEach (( el ) => {
el . addEventListener ( "click" , ( evt ) => {
evt . preventDefault ();
evt . stopPropagation ();
let kind = el . dataset . anKind ;
let key = el . dataset . anKey ;
let count = el . dataset . anCount ;
if ( el . dataset . anDonut === "1" ) {
const hit = sliceFromDonutClick ( el , evt );
if ( ! hit ) return ;
key = hit . key ;
count = hit . value ;
}
if ( ! key ) return ;
handleAnContext ( kind , key , count , evt );
});
});
2026-07-18 01:18:14 +02:00
mount . querySelectorAll ( "[data-jump]" ). forEach (( el ) => {
2026-07-18 21:56:30 +02:00
el . addEventListener ( "click" , ( evt ) => {
evt . preventDefault ();
2026-07-18 01:18:14 +02:00
const jump = el . dataset . jump ;
2026-07-18 21:56:30 +02:00
// KPI → context popup first
if ( el . classList . contains ( "an-kpi" )) {
let kind = "connection" ;
let key = "connected" ;
if ( el . dataset . conn === "all" ) {
openAnContext ({
title : "Fleet devices" ,
sub : "Full OME fleet snapshot" ,
pills : [ ` ${ fleet . length } devices` , ` ${ connected } online` , ` ${ offline } offline` ],
rows : ctxRowsFromFleet ( fleet ),
actions : [
{
id : "fleet" ,
label : "Open Fleet inventory" ,
primary : true ,
run : () => {
state . filters . connected = "all" ;
loadTab ( "fleet" );
},
},
],
});
return ;
}
if ( el . dataset . conn === "yes" ) {
handleAnContext ( "connection" , "connected" , connected );
return ;
}
if ( el . dataset . conn === "no" ) {
handleAnContext ( "connection" , "offline" , offline );
return ;
}
if ( el . dataset . sev === "critical" ) {
const label = ( el . querySelector ( "span" ) ? . textContent || "" ). toLowerCase ();
if ( label . includes ( "component" )) {
handleAnContext ( "compliance" , "CRITICAL" , critComps );
} else {
const list = devices . filter (
( d ) =>
String ( d . compliance_status || "" ). toUpperCase () === "CRITICAL" ||
String ( d . firmware_status || "" ). toLowerCase (). includes ( "non-compliant" )
);
openAnContext ({
title : "Outdated vs Dell" ,
sub : "Systems non-compliant against Dell Online Baseline" ,
pills : [ ` ${ list . length } systems` , `baseline ${ cs . baseline_name || "—" } ` ],
rows : ctxRowsFromDevices ( list ),
actions : [
{
id : "fw" ,
label : "Open Firmware / BIOS" ,
primary : true ,
run : () => {
state . outdatedOnly = true ;
state . filters . severity = "critical" ;
loadTab ( "firmware" );
},
},
],
});
}
return ;
}
if ( el . dataset . war ) {
const list = warranties . filter (( w ) => {
const d = intOr ( w . days_remaining );
return d <= 30 ;
});
openAnContext ({
title : "Warranty risk (≤30d / expired)" ,
sub : "OME warranty records needing attention" ,
pills : [ ` ${ list . length } records` , ` ${ expired } expired` , ` ${ d30 } ≤30d` ],
rows : ctxRowsFromWarranty ( list ),
actions : [
{
id : "war" ,
label : "Open Warranty tab" ,
primary : true ,
run : () => {
state . filters . warranty = "expired" ;
loadTab ( "warranty" );
},
},
],
});
return ;
}
}
2026-07-18 01:18:14 +02:00
if ( el . dataset . conn ) state . filters . connected = el . dataset . conn ;
if ( el . dataset . sev ) {
state . filters . severity = el . dataset . sev ;
state . outdatedOnly = true ;
}
if ( el . dataset . war ) state . filters . warranty = el . dataset . war ;
loadTab ( jump );
});
});
mount . querySelectorAll ( "[data-jump-hw]" ). forEach (( el ) => {
el . addEventListener ( "click" , () => {
const id = Number ( el . dataset . jumpHw );
if ( ! id ) return ;
state . hwDeviceId = id ;
loadTab ( "hardware" );
});
});
}
function intOr ( v ) {
const n = Number ( v );
return Number . isFinite ( n ) ? n : 0 ;
}
function shortComponent ( name ) {
const s = String ( name || "component" );
if ( /bios/i . test ( s )) return "BIOS" ;
if ( /idrac/i . test ( s )) return "iDRAC" ;
if ( /boss/i . test ( s )) return "BOSS" ;
if ( /cpld/i . test ( s )) return "CPLD" ;
if ( /nic|network|broadcom|mellanox|intel/i . test ( s )) return "NIC / Network" ;
if ( /perc|raid|storage/i . test ( s )) return "RAID / Storage" ;
if ( /openmanage|ome/i . test ( s )) return "OME / Modular" ;
return s . length > 28 ? s . slice ( 0 , 26 ) + "…" : s ;
}
function renderHardware ( mount ) {
const all = ( state . fleet ? . rows || []). filter (
( r ) => r . is_server || String ( r . model || "" ). toLowerCase (). includes ( "poweredge" )
2026-07-17 15:15:57 +02:00
);
2026-07-18 01:18:14 +02:00
const rows = applyBaseFilters ( all , { searchFields : [ "name" , "service_tag" , "ip" , "model" ] });
const selectedId = state . hwDeviceId || rows [ 0 ] ? . id || all [ 0 ] ? . id ;
if ( selectedId ) state . hwDeviceId = selectedId ;
mount . innerHTML = `
${ filterBar ({
models : uniqueModels ( all ),
connected : true ,
deviceSelect : rows . length ? rows : all ,
searchPlaceholder : "Filter server list…" ,
exports : `<button type="button" class="btn compact" id="hw-reload" title="Reload DIMM/disk/PCIe inventory from OME for the selected server">Reload hardware</button>` ,
quick : [
{ id : "all" , label : "All servers" , title : "Clear filters" } ,
{ id: "connected", label: "Connected", title: "Only connected", active: state.filters.connected === "yes" },
{ id: "offline", label: "Offline", title: "Only offline", active: state.filters.connected === "no" },
],
})}
<div class="hw-layout">
<aside class="hw-list">
<h3> ${ rows . length } servers</h3>
<div class="hw-list-scroll">
${ rows
. map (
( r ) => `<button type="button" class="hw-item ${ Number ( r . id ) === Number ( selectedId ) ? "active" : "" } " data-hw-id=" ${ r . id } " title="Show DIMM map & expansion for ${ escapeHtml ( r . service_tag || r . name || "" ) } ">
<strong> ${ escapeHtml ( r . name || "—" ) } </strong>
<span><code class="st"> ${ escapeHtml ( r . service_tag || "—" ) } </code> · ${ escapeHtml ( r . model || "" ) } </span>
</button>`
)
. join ( "" ) || "<p class='hint'>No servers match filters — Clear filters or pick another model.</p>" }
</div>
</aside>
<section class="hw-detail" id="hw-detail">
<p class="hint">Select a server above or use the Server dropdown…</p>
</section>
</div>` ;
wireFilters ( mount , () => renderHardware ( mount ));
$ ( "#hw-reload" ) ? . addEventListener ( "click" , () => {
if ( state . hwDeviceId ) loadHardwareDetail ( state . hwDeviceId , true );
});
mount . querySelectorAll ( "[data-hw-id]" ). forEach (( btn ) => {
btn . addEventListener ( "click" , () => {
state . hwDeviceId = Number ( btn . dataset . hwId );
mount . querySelectorAll ( ".hw-item" ). forEach (( el ) => el . classList . toggle ( "active" , Number ( el . dataset . hwId ) === state . hwDeviceId ));
const sel = $ ( "#flt-device" );
if ( sel ) sel . value = String ( state . hwDeviceId );
loadHardwareDetail ( state . hwDeviceId );
});
});
if ( selectedId ) loadHardwareDetail ( selectedId );
}
async function loadHardwareDetail ( deviceId , force ) {
const el = $ ( "#hw-detail" );
if ( ! el ) return ;
const liveHint = ( state . fleet ? . rows || []). find (( r ) => Number ( r . id ) === Number ( deviceId ));
const label = liveHint
? ` ${ liveHint . service_tag || "—" } · ${ liveHint . name || "" } `
: `OME # ${ deviceId } ` ;
const steps = [
"Connecting to OpenManage Enterprise…" ,
"Reading processors & memory map…" ,
"Reading disks, RAID & adapters…" ,
"Reading OS / network inventory…" ,
"Building current state vs Dell catalog…" ,
];
let stepIdx = 0 ;
const t0 = performance . now ();
let timer = null ;
let stepTimer = null ;
function paintLoading () {
const sec = Math . max ( 0 , Math . floor (( performance . now () - t0 ) / 1000 ));
el . innerHTML = `
<div class="hw-wait" role="status" aria-live="polite" aria-busy="true">
<div class="hw-wait-visual">
<div class="hw-wait-orbit" aria-hidden="true">
<span class="hw-wait-core">OME</span>
<span class="hw-wait-ring"></span>
<span class="hw-wait-dot"></span>
</div>
<div class="hw-wait-bars" aria-hidden="true">
<i></i><i></i><i></i><i></i><i></i>
</div>
</div>
<div class="hw-wait-copy">
<h3>Fetching hardware from OME</h3>
<p class="hw-wait-target"> ${ escapeHtml ( label ) } </p>
<p class="hw-wait-step" id="hw-wait-step"> ${ escapeHtml ( steps [ stepIdx ]) } </p>
<p class="hw-wait-time">Waiting <strong id="hw-wait-sec"> ${ sec } </strong>s — full inventory can take up to a minute</p>
<ul class="hw-wait-checklist">
<li class=" ${ stepIdx >= 0 ? "on" : "" } ">Session</li>
<li class=" ${ stepIdx >= 1 ? "on" : "" } ">CPU / RAM</li>
<li class=" ${ stepIdx >= 2 ? "on" : "" } ">Disks</li>
<li class=" ${ stepIdx >= 3 ? "on" : "" } ">OS / NIC</li>
<li class=" ${ stepIdx >= 4 ? "on" : "" } ">Catalog</li>
</ul>
</div>
</div>` ;
}
paintLoading ();
timer = setInterval (() => {
const secEl = el . querySelector ( "#hw-wait-sec" );
if ( secEl ) secEl . textContent = String ( Math . max ( 0 , Math . floor (( performance . now () - t0 ) / 1000 )));
}, 250 );
stepTimer = setInterval (() => {
stepIdx = Math . min ( steps . length - 1 , stepIdx + 1 );
const stepEl = el . querySelector ( "#hw-wait-step" );
if ( stepEl ) stepEl . textContent = steps [ stepIdx ];
el . querySelectorAll ( ".hw-wait-checklist li" ). forEach (( li , i ) => {
li . classList . toggle ( "on" , i <= stepIdx );
});
}, 4500 );
try {
const data = await apiGet ( `/api/devices/ ${ deviceId } /expansion ${ force ? "?force=true" : "" } ` );
// Prefer live fleet node (has DNS-resolved RDP IPs) over detail snapshot
const live = ( state . fleet ? . rows || []). find (( r ) => Number ( r . id ) === Number ( deviceId ));
if ( live && data . device ) {
data . device = {
... data . device ,
rdp_host : live . rdp_host || data . device . rdp_host ,
rdp_ips : live . rdp_ips || data . device . rdp_ips || [],
os_hostname : live . os_hostname || data . device . os_hostname ,
idrac_ip : live . idrac_ip || data . device . idrac_ip || data . device . ip ,
is_windows : live . is_windows || data . device . is_windows ,
};
}
el . innerHTML = renderExpansionHtml ( data );
el . querySelector ( "#hw-rdp-btn" ) ? . addEventListener ( "click" , () => {
const host = el . querySelector ( "#hw-rdp-btn" ) ? . dataset ? . rdpHost ;
window . cockpitRdp ? . open ({
...( data . device || {}),
ip : host || data . device ? . rdp_host || data . device ? . ip ,
host : host || data . device ? . rdp_host || data . device ? . ip ,
});
});
} catch ( err ) {
el . innerHTML = `
<div class="hw-wait hw-wait-error" role="alert">
<h3>Could not load hardware from OME</h3>
<p> ${ escapeHtml ( err . message ) } </p>
<button type="button" class="btn" id="hw-wait-retry">Retry</button>
</div>` ;
el . querySelector ( "#hw-wait-retry" ) ? . addEventListener ( "click" , () => loadHardwareDetail ( deviceId , true ));
} finally {
clearInterval ( timer );
clearInterval ( stepTimer );
}
}
function renderExpansionHtml ( data ) {
const d = data . device || {};
const x = data . expansion || {};
const cur = x . current || {};
const exp = x . expansion || {};
const findings = x . findings || [];
const memNow = cur . memory || {};
const cpuNow = cur . cpu || {};
const diskNow = cur . disks || {};
const memExp = exp . memory || {};
const cpuExp = exp . cpu || {};
const diskExp = exp . disks || {};
const pcieExp = exp . pcie || {};
const modules = memNow . modules || [];
const processors = cpuNow . processors || [];
const disks = diskNow . items || [];
const adapters = cur . adapters || [];
const osRows = cur . os || [];
const slotMap = memExp . slot_map || [];
const banks = [... new Set ( slotMap . map (( s ) => s . cpu_bank ). filter ( Boolean ))];
const rdpHost = d . rdp_host || ( d . rdp_ips && d . rdp_ips [ 0 ]) || "" ;
const rdpIps = d . rdp_ips || [];
const idracIp = d . idrac_ip || d . ip || "" ;
const bankHtml = banks
. map (( bank ) => {
const bankSlots = slotMap . filter (( s ) => s . cpu_bank === bank ). sort (( a , b ) => ( a . slot_num || 0 ) - ( b . slot_num || 0 ));
const filled = bankSlots . filter (( s ) => s . populated ). length ;
return `<div class="dimm-bank">
<h4>CPU ${ escapeHtml ( bank ) } · ${ filled } / ${ bankSlots . length } populated (catalog map)</h4>
<div class="dimm-grid">
${ bankSlots
. map (( s ) => {
const title = s . populated
? ` ${ s . slot } : ${ s . size_gb || "?" } GB @ ${ s . operating_mts || s . rated_mts || "?" } MT/s · ${ s . part_number || "" } `
: ` ${ s . slot } : EMPTY (catalog)` ;
return `<div class="dimm-slot ${ s . populated ? "full" : "empty" } ${ s . rank_color || "" } " title=" ${ escapeHtml ( title ) } ">
<span class="dimm-lab"> ${ escapeHtml ( s . slot ) } </span>
<span class="dimm-val"> ${ s . populated ? escapeHtml ( String ( s . size_gb ?? "?" )) + "G" : "—" } </span>
<span class="dimm-meta"> ${ s . populated ? escapeHtml ( String ( s . operating_mts || s . rated_mts || "" )) : "empty" } </span>
</div>` ;
} )
.join("")}
</div>
</div>` ;
})
. join ( "" );
const dash = ( v ) => ( v == null || v === "" ? "—" : v );
const catalogBadge = exp . catalog_known
? `<span class="badge on">Catalog: ${ escapeHtml ( exp . catalog_model || "" ) } </span>`
: `<span class="badge off">No catalog max for this model</span>` ;
return `
<header class="hw-head">
<div>
<h2> ${ escapeHtml ( d . name || "Device" ) } </h2>
<p><code class="st"> ${ escapeHtml ( d . service_tag || "—" ) } </code> · ${ escapeHtml ( d . model || "—" ) } </p>
<p class="hw-net">
<span>iDRAC <code> ${ escapeHtml ( idracIp || "—" ) } </code></span>
${ d . os_hostname ? `<span>OS host <code> ${ escapeHtml ( d . os_hostname ) } </code></span>` : "" }
${ rdpIps . length ? `<span>RDP IP(s) <code> ${ escapeHtml ( rdpIps . join ( ", " )) } </code></span>` : `<span>RDP IP <em>not resolved</em></span>` }
${ rdpHost ? `<button type="button" class="btn compact" id="hw-rdp-btn" data-rdp-host=" ${ escapeAttr ( rdpHost ) } ">RDP popout</button>` : "" }
</p>
${ osRows
. map (
( o ) =>
`<p class="hw-os"> ${ escapeHtml ([ o . os_name , o . os_version , o . hostname ]. filter ( Boolean ). join ( " · " ) || "OS inventory empty" ) } </p>`
)
. join ( "" ) }
</div>
${ catalogBadge }
</header>
<section class="hw-section hw-current">
<h3>Current state · physical (OME live)</h3>
<p class="an-note">Only what OME reports as installed — not Dell maximums.</p>
<div class="hw-limits hw-limits-current">
<div><em> ${ dash ( memNow . installed_gb ) } GB</em><span>RAM installed</span></div>
<div><em> ${ dash ( memNow . dimm_count ) } </em><span>DIMM modules</span></div>
<div><em> ${ dash ( cpuNow . sockets_populated ) } </em><span>CPU sockets filled</span></div>
<div><em> ${ dash ( cpuNow . cores_total ) } </em><span>Cores total</span></div>
<div><em> ${ dash ( diskNow . count ) } </em><span>Disks installed</span></div>
<div><em> ${ dash ( diskNow . capacity_gb ) } GB</em><span>Disk capacity sum</span></div>
</div>
<h4>Memory modules (installed)</h4>
<p class="an-note">Rated MT/s: ${ escapeHtml (( memNow . rated_mts || []). join ( ", " ) || "—" ) } · Operating MT/s: ${ escapeHtml (( memNow . operating_mts || []). join ( ", " ) || "—" ) } </p>
<div class="rpt-table-wrap" style="max-height:240px">
<table class="rpt-table">
<thead><tr><th>Slot</th><th>Size</th><th>Rated</th><th>Operating</th><th>Type</th><th>Part</th><th>Serial</th></tr></thead>
<tbody>
${ modules
. map (
( s ) => `<tr>
<td> ${ escapeHtml ( s . slot ) } </td>
<td> ${ s . size_gb ?? "—" } GB</td>
<td> ${ escapeHtml ( String ( s . rated_mts ?? "—" )) } </td>
<td> ${ escapeHtml ( String ( s . operating_mts ?? "—" )) } </td>
<td> ${ escapeHtml ( s . type || "—" ) } </td>
<td> ${ escapeHtml ( s . part_number || "—" ) } </td>
<td> ${ escapeHtml ( s . serial || "—" ) } </td>
</tr>`
)
. join ( "" ) || "<tr><td colspan='7'>No DIMMs in OME inventory</td></tr>" }
</tbody>
</table>
</div>
<h4>Processors (installed)</h4>
<ul class="rpt-list">
${ processors
. map (
( p ) =>
`<li><strong> ${ escapeHtml ( p . slot || "CPU" ) } </strong> — ${ escapeHtml ( p . model || "—" ) } · ${ p . cores ?? "?" } cores · ${ p . current_mhz ?? "?" } / ${ p . max_mhz ?? "?" } MHz</li>`
)
. join ( "" ) || "<li>No CPU inventory</li>" }
</ul>
<h4>Disks (installed)</h4>
<div class="rpt-table-wrap" style="max-height:260px">
<table class="rpt-table">
<thead><tr><th>Bay</th><th>Model</th><th>Media</th><th>Size (GB)</th><th>Bus</th><th>Status</th><th>Serial</th></tr></thead>
<tbody>
${ disks
. map (
( disk ) => `<tr>
<td> ${ escapeHtml ( String ( disk . bay ?? "—" )) } </td>
<td> ${ escapeHtml ( disk . model || "—" ) } </td>
<td> ${ escapeHtml ( disk . media || "—" ) } </td>
<td> ${ escapeHtml ( String ( disk . size_gb ?? "—" )) } </td>
<td> ${ escapeHtml ( disk . bus || "—" ) } </td>
<td> ${ escapeHtml ( String ( disk . status || "—" )) } </td>
<td> ${ escapeHtml ( disk . serial || "—" ) } </td>
</tr>`
)
. join ( "" ) || "<tr><td colspan='7'>No disks in inventory</td></tr>" }
</tbody>
</table>
</div>
<h4>Adapters seen by OME</h4>
<div class="rpt-table-wrap" style="max-height:180px">
<table class="rpt-table">
<thead><tr><th>Slot</th><th>Description</th><th>Type</th></tr></thead>
<tbody>
${ adapters
. slice ( 0 , 40 )
. map (
( c ) => `<tr>
<td> ${ escapeHtml ( c . slot || "—" ) } </td>
<td> ${ escapeHtml ( c . description || "—" ) } </td>
<td> ${ escapeHtml ( c . slot_type || "—" ) } </td>
</tr>`
)
. join ( "" ) || "<tr><td colspan='3'>No adapters</td></tr>" }
</tbody>
</table>
</div>
</section>
<section class="hw-section hw-expand">
<h3>Max expansion · recommendations (Dell catalog)</h3>
<p class="an-note"> ${ escapeHtml ( exp . source || "" ) } — separate from live inventory. Confirm against Tech Specs for this Service Tag BOM.</p>
<div class="hw-limits">
<div><em> ${ memExp . max_capacity_tb != null ? memExp . max_capacity_tb + " TB" : "—" } </em><span>Max memory</span></div>
<div><em> ${ memExp . headroom_gb != null ? memExp . headroom_gb + " GB" : "—" } </em><span>Headroom vs installed</span></div>
<div><em> ${ memExp . dimms_installed ?? "—" } / ${ memExp . dimm_slots_max ?? "—" } </em><span>DIMMs used / max slots</span></div>
<div><em> ${ cpuExp . sockets_populated ?? "—" } / ${ cpuExp . sockets_max ?? "—" } </em><span>CPU sockets used / max</span></div>
<div><em> ${ diskExp . drives_installed ?? "—" } / ${ diskExp . bays_max ?? "—" } </em><span>Disks / max bays</span></div>
<div><em> ${ pcieExp . slots_max ?? "—" } </em><span>Max PCIe (catalog)</span></div>
</div>
<div class="hw-exp-notes">
<p><strong>Memory:</strong> ${ escapeHtml ( memExp . note || "—" ) } </p>
<p><strong>CPU:</strong> ${ escapeHtml ( cpuExp . note || "—" ) } </p>
<p><strong>Disks:</strong> ${ escapeHtml ( diskExp . note || "—" ) } </p>
<p><strong>PCIe:</strong> ${ escapeHtml ( pcieExp . note || "—" ) } </p>
<p class="an-note">Supported memory: ${ escapeHtml (( memExp . supported_types || []). join ( ", " ) || "—" ) } · catalog max rated ${ dash ( memExp . max_rated_mts ) } MT/s</p>
</div>
<div class="hw-docs-bar">
<span class="hw-docs-label">Dell docs</span>
${ ( exp . docs || [])
. map (( doc ) => {
const kind = doc . kind || ( String ( doc . title || "" ). toLowerCase (). includes ( "spec" ) ? "spec" : "doc" );
return `<a class="hw-doc-btn ${ escapeAttr ( kind ) } " href=" ${ escapeAttr ( doc . url ) } " target="_blank" rel="noopener" title=" ${ escapeAttr ( doc . title ) } "> ${ escapeHtml ( doc . title ) } </a>` ;
} )
.join("")}
<a class="hw-doc-btn support" href="https://www.dell.com/support/home/en-us/product-support/servicetag/ ${ encodeURIComponent ( d . service_tag || "" ) } /overview" target="_blank" rel="noopener">Support · ST ${ escapeHtml ( d . service_tag || "—" ) } </a>
</div>
${
exp . catalog_known
? `<h4>Catalog DIMM map (empty = expansion candidate)</h4> ${ bankHtml || "<p class='hint'>No slot map</p>" } `
: `<p class="hw-finding warning">No Cockpit catalog for ${ escapeHtml ( d . model || "this model" ) } — maxima above are blank on purpose (not guessed).</p>`
}
</section>
<div class="hw-findings">
${ findings
. map (( f ) => `<div class="hw-finding ${ f . severity || "" } "> ${ escapeHtml ( f . text ) } </div>` )
. join ( "" ) || `<div class="hw-finding ok">No population warnings from live inventory vs catalog rules.</div>` }
</div>
<section class="hw-section dell-guidance">
<h3>Dell guidance (not current inventory)</h3>
<div class="hw-cols">
<div>
<h4>Installation / population</h4>
<ul> ${ ( exp . best_practices || []). map (( t ) => `<li> ${ escapeHtml ( t ) } </li>` ). join ( "" ) || "<li>—</li>" } </ul>
</div>
<div>
<h4>Performance</h4>
<ul> ${ ( exp . performance_notes || []). map (( t ) => `<li> ${ escapeHtml ( t ) } </li>` ). join ( "" ) || "<li>—</li>" } </ul>
</div>
</div>
</section>` ;
}
function renderFleet ( mount ) {
const all = state . fleet ? . rows || [];
const rows = applyBaseFilters ( all , { searchFields : [ "name" , "service_tag" , "ip" , "model" , "subnet" ] });
2026-07-17 15:15:57 +02:00
mount . innerHTML =
2026-07-18 01:18:14 +02:00
filterBar ({
models : uniqueModels ( all ),
connected : true ,
exports : `
<a class="btn compact" href=" ${ downloadUrl ( "fleet" , "csv" ) } " title="Download filtered-capable fleet CSV (full export)">CSV</a>
<a class="btn compact" href=" ${ downloadUrl ( "fleet" , "json" ) } " title="Download fleet JSON">JSON</a>
` ,
quick : [
{ id : "all" , label : "All" , title : "Clear filters" },
{ id : "connected" , label : "Connected" , active : state . filters . connected === "yes" },
{ id : "offline" , label : "Offline" , active : state . filters . connected === "no" },
],
}) +
`<div class="rpt-meta"> ${ rows . length } / ${ all . length } devices shown · Service Tags + warranty joined</div>
2026-07-17 15:15:57 +02:00
<div class="rpt-table-wrap"><table class="rpt-table">
<thead><tr>
<th>Name</th><th>Service Tag</th><th>Model</th><th>IP</th><th>Connected</th><th>Power</th><th>Warranty end</th><th>Days left</th>
</tr></thead>
<tbody>
${ rows
. map (
2026-07-18 01:18:14 +02:00
( r ) => `<tr data-id=" ${ r . id } " class="an-row-click" title="Open Hardware / Expansion" data-open-hw=" ${ r . id } ">
2026-07-17 15:15:57 +02:00
<td> ${ escapeHtml ( r . name ) } </td>
<td><code class="st"> ${ escapeHtml ( r . service_tag || "—" ) } </code></td>
<td> ${ escapeHtml ( r . model || "—" ) } </td>
<td> ${ escapeHtml ( r . ip || "—" ) } </td>
<td> ${ r . connected ? badge ( "CONNECTED" ) : badge ( "OFFLINE" ) } </td>
<td> ${ r . watts != null ? Math . round ( r . watts ) + " W" : "—" } </td>
<td> ${ escapeHtml ( fmtDate ( r . warranty_end )) } </td>
<td> ${ r . warranty_days_remaining ?? "—" } </td>
</tr>`
)
. join ( "" ) }
</tbody>
</table></div>` ;
2026-07-18 01:18:14 +02:00
wireFilters ( mount , () => renderFleet ( mount ));
mount . querySelectorAll ( "[data-open-hw]" ). forEach (( tr ) => {
tr . addEventListener ( "click" , () => {
state . hwDeviceId = Number ( tr . dataset . openHw );
loadTab ( "hardware" );
});
});
2026-07-17 15:15:57 +02:00
}
function renderFirmware ( mount ) {
const sum = state . compliance ? . summary || {};
let comps = state . compliance ? . components || [];
2026-07-18 01:18:14 +02:00
const compOpts = [... new Set ( comps . map (( c ) => shortComponent ( c . component )))]. sort ();
2026-07-17 15:15:57 +02:00
if ( state . outdatedOnly ) {
comps = comps . filter (
( c ) =>
String ( c . update_action || "" ). toUpperCase () === "UPGRADE" ||
String ( c . compliance_status || "" ). toUpperCase () === "CRITICAL" ||
String ( c . compliance_status || "" ). toUpperCase () === "WARNING"
);
}
2026-07-18 01:18:14 +02:00
if ( state . filters . severity === "critical" ) {
comps = comps . filter (( c ) => String ( c . compliance_status || "" ). toUpperCase () === "CRITICAL" );
} else if ( state . filters . severity === "warning" ) {
comps = comps . filter (( c ) => String ( c . compliance_status || "" ). toUpperCase () === "WARNING" );
}
if ( state . filters . component && state . filters . component !== "all" ) {
comps = comps . filter (( c ) => shortComponent ( c . component ) === state . filters . component );
}
if ( state . filters . model ) {
comps = comps . filter (( c ) => c . model === state . filters . model );
}
comps = comps . filter (( c ) => matchSearch ( c , [ "device_name" , "service_tag" , "model" , "component" , "ip" ]));
2026-07-17 15:15:57 +02:00
mount . innerHTML =
2026-07-18 01:18:14 +02:00
filterBar ({
models : uniqueModels ( state . compliance ? . devices || []),
connected : false ,
severity : true ,
componentOptions : compOpts ,
outdatedToggle : true ,
exports : `
<a class="btn compact" href=" ${ downloadUrl ( "firmware" , "csv" ) } " title="Export firmware compliance CSV">CSV</a>
<a class="btn compact" href=" ${ downloadUrl ( "firmware" , "json" ) } " title="Export firmware compliance JSON">JSON</a>
` ,
quick : [
{ id : "all" , label : "All" , title : "Clear severity/component filters" },
{ id : "bios" , label : "BIOS" , title : "Only PowerEdge BIOS rows" , active : state . filters . component === "BIOS" },
{ id : "idrac" , label : "iDRAC" , title : "Only iDRAC / Lifecycle Controller" , active : state . filters . component === "iDRAC" },
{ id : "outdated" , label : "Critical" , title : "Critical only" , active : state . filters . severity === "critical" },
],
}) +
2026-07-17 15:15:57 +02:00
`<div class="rpt-meta">
Baseline <strong> ${ escapeHtml ( sum . baseline_name || "—" ) } </strong>
2026-07-18 01:18:14 +02:00
· showing ${ comps . length } rows · outdated devices ${ sum . outdated_devices ?? "—" }
2026-07-17 15:15:57 +02:00
</div>
<div class="rpt-table-wrap"><table class="rpt-table">
<thead><tr>
<th>Service Tag</th><th>Device</th><th>Component</th><th>Current</th><th>Dell catalog</th><th>Action</th><th>Status</th><th>Dell.com</th>
</tr></thead>
<tbody>
${ comps
. map (
( c ) => `<tr>
<td><code class="st"> ${ escapeHtml ( c . service_tag || "—" ) } </code></td>
<td> ${ escapeHtml ( c . device_name || "—" ) } <div class="sub"> ${ escapeHtml ( c . model || "" ) } </div></td>
<td> ${ escapeHtml ( c . component || "—" ) } </td>
<td> ${ escapeHtml ( c . current_version || "—" ) } </td>
<td> ${ escapeHtml ( c . catalog_version || "—" ) } </td>
<td> ${ escapeHtml ( c . update_action || "—" ) } </td>
<td> ${ badge ( c . compliance_status || c . status_badge ) } </td>
2026-07-18 01:18:14 +02:00
<td> ${ c . dell_uri ? `<a class="rpt-link" href=" ${ escapeHtml ( c . dell_uri ) } " target="_blank" rel="noopener" title="Open Dell driver page for this package">Driver</a>` : "—" } </td>
2026-07-17 15:15:57 +02:00
</tr>`
)
. join ( "" ) }
</tbody>
</table></div>` ;
2026-07-18 01:18:14 +02:00
wireFilters ( mount , () => renderFirmware ( mount ));
2026-07-17 15:15:57 +02:00
}
function renderWarranty ( mount ) {
2026-07-18 01:18:14 +02:00
let items = state . warranties ? . items || [];
if ( state . filters . warranty === "expired" ) items = items . filter (( w ) => intOr ( w . days_remaining ) <= 0 );
else if ( state . filters . warranty === "d30" )
items = items . filter (( w ) => {
const d = intOr ( w . days_remaining );
return d > 0 && d <= 30 ;
});
else if ( state . filters . warranty === "d90" )
items = items . filter (( w ) => {
const d = intOr ( w . days_remaining );
return d > 30 && d <= 90 ;
});
else if ( state . filters . warranty === "ok" ) items = items . filter (( w ) => intOr ( w . days_remaining ) > 90 );
if ( state . filters . model ) items = items . filter (( w ) => w . model === state . filters . model );
items = items . filter (( w ) => matchSearch ( w , [ "device_name" , "service_tag" , "model" , "service_level" ]));
2026-07-17 15:15:57 +02:00
mount . innerHTML =
2026-07-18 01:18:14 +02:00
filterBar ({
models : uniqueModels ( state . warranties ? . items || []),
connected : false ,
warranty : true ,
exports : `
<a class="btn compact" href=" ${ downloadUrl ( "warranty" , "csv" ) } " title="Export warranties CSV">CSV</a>
<a class="btn compact" href=" ${ downloadUrl ( "warranty" , "json" ) } " title="Export warranties JSON">JSON</a>
` ,
quick : [
{ id : "all" , label : "All" , title : "Clear warranty filter" },
{ id : "warranty-risk" , label : "Expired" , title : "Expired only" , active : state . filters . warranty === "expired" },
],
}) +
`<div class="rpt-meta"> ${ items . length } warranty records shown</div>
2026-07-17 15:15:57 +02:00
<div class="rpt-table-wrap"><table class="rpt-table">
<thead><tr>
<th>Service Tag</th><th>Device</th><th>Model</th><th>Service level</th><th>Start</th><th>End</th><th>Days left</th>
</tr></thead>
<tbody>
${ items
. map (
( w ) => `<tr>
<td><code class="st"> ${ escapeHtml ( w . service_tag || "—" ) } </code></td>
<td> ${ escapeHtml ( w . device_name || "—" ) } </td>
<td> ${ escapeHtml ( w . model || "—" ) } </td>
<td> ${ escapeHtml ( w . service_level || "—" ) } </td>
<td> ${ escapeHtml ( fmtDate ( w . start_date )) } </td>
<td> ${ escapeHtml ( fmtDate ( w . end_date )) } </td>
<td> ${ w . days_remaining ?? "—" } </td>
</tr>`
)
. join ( "" ) }
</tbody>
</table></div>` ;
2026-07-18 01:18:14 +02:00
wireFilters ( mount , () => renderWarranty ( mount ));
2026-07-17 15:15:57 +02:00
}
function renderOme ( mount , baselines ) {
const defs = state . reportDefs ? . items || [];
const jobs = state . jobs ? . items || [];
const cats = state . catalogs ? . items || [];
const filtered = defs . filter (( d ) => matchSearch ( d , [ "name" , "description" , "category" ]));
2026-07-18 01:18:14 +02:00
const categories = [... new Set ( defs . map (( d ) => d . category ). filter ( Boolean ))]. sort ();
const shown = state . filters . category
? filtered . filter (( d ) => d . category === state . filters . category )
: filtered ;
2026-07-17 15:15:57 +02:00
mount . innerHTML = `
2026-07-18 01:18:14 +02:00
${ filterBar ({
models : [],
connected : false ,
searchPlaceholder : "Filter OME report definitions…" ,
exports : `<button type="button" class="btn ghost compact" id="rpt-refresh-ome" title="Reload ReportDefs, catalogs, baselines and jobs from OME">Refresh OME data</button>` ,
quick : categories . slice ( 0 , 8 ). map (( c ) => ({
id : `cat: ${ c } ` ,
label : c ,
title : `Show ReportDefs in category ${ c } ` ,
active : state . filters . category === c ,
} )),
})}
2026-07-17 15:15:57 +02:00
<div class="rpt-grid-2">
<section class="rpt-panel">
2026-07-18 01:18:14 +02:00
<h3 title="Dell update catalogs registered in OME">Dell catalogs</h3>
2026-07-17 15:15:57 +02:00
<ul class="rpt-list">
${ cats
. map (
( c ) =>
`<li><strong> ${ escapeHtml ( c . repository_name || c . filename || "#" + c . id ) } </strong>
<span class="sub"> ${ escapeHtml ( c . repository_type || "" ) } · ${ escapeHtml ( fmtDate ( c . last_updated )) } </span></li>`
)
. join ( "" ) || "<li class='hint'>No catalogs</li>" }
</ul>
2026-07-18 01:18:14 +02:00
<h3 title="Firmware baselines compared against Dell catalogs">Baselines</h3>
2026-07-17 15:15:57 +02:00
<ul class="rpt-list">
${ ( baselines ? . items || [])
. map (
( b ) =>
`<li><strong> ${ escapeHtml ( b . name ) } </strong> ${ badge ( b . compliance_status ) }
<span class="sub">critical ${ b . critical ?? 0 } · last ${ escapeHtml ( fmtDate ( b . last_run )) } </span></li>`
)
. join ( "" ) }
</ul>
</section>
<section class="rpt-panel">
2026-07-18 01:18:14 +02:00
<h3 title="Recent JobService entries (read-only)">Recent OME jobs</h3>
2026-07-17 15:15:57 +02:00
<ul class="rpt-list">
${ jobs
. slice ( 0 , 20 )
. map (
( j ) =>
`<li><strong> ${ escapeHtml ( j . name || "#" + j . id ) } </strong> ${ badge ( j . status ) }
<span class="sub"> ${ escapeHtml ( j . job_type || "" ) } </span></li>`
)
. join ( "" ) || "<li class='hint'>No jobs returned</li>" }
</ul>
</section>
</div>
2026-07-18 01:18:14 +02:00
<h3 class="rpt-h">OME ReportDefs ( ${ shown . length } )</h3>
2026-07-17 15:15:57 +02:00
<div class="rpt-table-wrap"><table class="rpt-table">
<thead><tr><th>Name</th><th>Category</th><th>Columns</th><th>Last run</th><th></th></tr></thead>
<tbody>
2026-07-18 01:18:14 +02:00
${ shown
2026-07-17 15:15:57 +02:00
. map (
( d ) => `<tr>
<td><strong> ${ escapeHtml ( d . name ) } </strong><div class="sub"> ${ escapeHtml ( d . description || "" ) } </div></td>
<td> ${ escapeHtml ( d . category || "—" ) } </td>
<td> ${ escapeHtml (( d . columns || []). slice ( 0 , 4 ). join ( ", " )) }${ ( d . columns || []). length > 4 ? "…" : "" } </td>
<td> ${ escapeHtml ( fmtDate ( d . last_run )) } </td>
2026-07-18 01:18:14 +02:00
<td>
<button type="button" class="btn compact" data-run-report=" ${ d . id } " title="Start this report job inside OME (read/generate)">Run</button>
<button type="button" class="btn ghost compact" data-fetch-report=" ${ d . id } " title="Fetch last results JSON if available">Results</button>
</td>
2026-07-17 15:15:57 +02:00
</tr>`
)
. join ( "" ) }
</tbody>
</table></div>
<pre class="rpt-results hidden" id="rpt-ome-results"></pre>` ;
2026-07-18 01:18:14 +02:00
wireFilters ( mount , () => renderOme ( mount , baselines ));
2026-07-17 15:15:57 +02:00
$ ( "#rpt-refresh-ome" ) ? . addEventListener ( "click" , () => loadTab ( "ome" ));
mount . querySelectorAll ( "[data-run-report]" ). forEach (( btn ) => {
btn . addEventListener ( "click" , async () => {
const id = Number ( btn . dataset . runReport );
btn . disabled = true ;
try {
const res = await fetch ( "/api/ome/reports/run" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ report_def_id : id }),
});
const data = await res . json ();
if ( ! res . ok ) throw new Error ( data . detail || res . statusText );
alert ( `OME report job started: ${ data . job_id } ` );
2026-07-18 01:18:14 +02:00
} catch ( err ) {
alert ( "Run failed: " + err . message );
2026-07-17 15:15:57 +02:00
} finally {
btn . disabled = false ;
}
});
});
mount . querySelectorAll ( "[data-fetch-report]" ). forEach (( btn ) => {
btn . addEventListener ( "click" , async () => {
const id = Number ( btn . dataset . fetchReport );
const out = $ ( "#rpt-ome-results" );
out ? . classList . remove ( "hidden" );
if ( out ) out . textContent = "Loading…" ;
try {
const data = await apiGet ( `/api/ome/reports/ ${ id } /results` );
if ( out ) out . textContent = JSON . stringify ( data , null , 2 );
2026-07-18 01:18:14 +02:00
} catch ( err ) {
if ( out ) out . textContent = String ( err . message || err );
2026-07-17 15:15:57 +02:00
}
});
});
}
function renderBrief ( mount ) {
const b = state . brief || {};
const sum = b . summary || {};
const cs = b . compliance_summary || {};
const ome = b . ome || {};
mount . innerHTML = `
<div class="rpt-toolbar">
<a class="btn compact" href=" ${ downloadUrl ( "brief" , "json" ) } ">JSON brief</a>
<a class="btn compact" href=" ${ downloadUrl ( "fleet" , "csv" ) } ">Fleet CSV</a>
<a class="btn compact" href=" ${ downloadUrl ( "firmware" , "csv" ) } ">Firmware CSV</a>
<a class="btn compact" href=" ${ downloadUrl ( "warranty" , "csv" ) } ">Warranty CSV</a>
<button type="button" class="btn primary compact" id="rpt-print">Print customer PDF</button>
</div>
<article class="customer-brief" id="customer-brief">
<header class="brief-head">
<img src="/dell.png" alt="Dell" width="48" height="48" />
<div>
<p class="modal-kicker">Dell ATC · OpenManage Cockpit</p>
<h2>Customer fleet brief</h2>
<p> ${ escapeHtml ( ome . name || "OM Enterprise" ) } ${ escapeHtml ( ome . version || "" ) } · ${ escapeHtml ( ome . fqdn || "" ) } </p>
<p class="sub">Generated ${ escapeHtml ( new Date (( b . generated_at || 0 ) * 1000 ). toLocaleString ()) } </p>
</div>
</header>
<section>
<h3>Executive summary</h3>
<div class="brief-kpis">
<div><em> ${ sum . total ?? "—" } </em><span>Devices</span></div>
<div><em> ${ sum . connected ?? "—" } </em><span>Connected</span></div>
<div><em> ${ cs . outdated_devices ?? "—" } </em><span>Outdated vs Dell catalog</span></div>
<div><em> ${ sum . alerts_critical ?? "—" } </em><span>Critical alerts</span></div>
</div>
<p>Baseline: <strong> ${ escapeHtml ( cs . baseline_name || "—" ) } </strong>
( ${ escapeHtml ( cs . compliance_status || "—" ) } ) · last run ${ escapeHtml ( fmtDate ( cs . last_run )) } </p>
</section>
<section>
<h3>Critical alerts</h3>
<ul> ${ ( b . critical_alerts || [])
. map (
( a ) =>
`<li><strong> ${ escapeHtml ( a . device || "—" ) } </strong> ${ escapeHtml ( a . ip || "" ) } — ${ escapeHtml (( a . message || "" ). slice ( 0 , 160 )) } </li>`
)
. join ( "" ) || "<li>None in snapshot</li>" } </ul>
</section>
<section>
<h3>Systems out of date (Dell catalog)</h3>
<ul> ${ ( b . outdated_devices || [])
. map (
( d ) =>
`<li><code class="st"> ${ escapeHtml ( d . service_tag || "—" ) } </code> ${ escapeHtml ( d . name || "" ) } · ${ escapeHtml ( d . model || "" ) } · ${ escapeHtml ( d . compliance_status || d . firmware_status || "" ) } </li>`
)
. join ( "" ) || "<li>No outdated devices in baseline report</li>" } </ul>
</section>
<section>
<h3>Warranty attention (≤ 90 days)</h3>
<ul> ${ ( b . warranty_expiring || [])
. map (
( w ) =>
`<li><code class="st"> ${ escapeHtml ( w . service_tag || "—" ) } </code> ${ escapeHtml ( w . device_name || "" ) } · ends ${ escapeHtml ( fmtDate ( w . end_date )) } · ${ w . days_remaining } days</li>`
)
. join ( "" ) || "<li>No warranties expiring within 90 days</li>" } </ul>
</section>
<section>
<h3>Device appendix (Service Tags)</h3>
<div class="rpt-table-wrap"><table class="rpt-table">
<thead><tr><th>Service Tag</th><th>Name</th><th>Model</th><th>IP</th><th>Connected</th></tr></thead>
<tbody>
${ ( b . fleet || [])
. map (
( r ) => `<tr>
<td><code class="st"> ${ escapeHtml ( r . service_tag || "—" ) } </code></td>
<td> ${ escapeHtml ( r . name || "—" ) } </td>
<td> ${ escapeHtml ( r . model || "—" ) } </td>
<td> ${ escapeHtml ( r . ip || "—" ) } </td>
<td> ${ r . connected ? "Yes" : "No" } </td>
</tr>`
)
. join ( "" ) }
</tbody>
</table></div>
</section>
</article>` ;
wireCommon ( mount );
}
function wireCommon ( mount ) {
2026-07-18 01:18:14 +02:00
// legacy no-op kept for brief; prefer wireFilters
2026-07-17 15:15:57 +02:00
$ ( "#rpt-print" ) ? . addEventListener ( "click" , () => window . print ());
}
function init () {
$ ( "#btn-reports" ) ? . addEventListener ( "click" , openReports );
$ ( "#btn-reports-close" ) ? . addEventListener ( "click" , closeReports );
$ ( "#reports-tabs" ) ? . addEventListener ( "click" , ( e ) => {
const chip = e . target . closest ( "[data-tab]" );
if ( ! chip ) return ;
state . search = "" ;
loadTab ( chip . dataset . tab );
});
$ ( "#scrim" ) ? . addEventListener ( "click" , () => {
2026-07-18 21:56:30 +02:00
const mode = $ ( "#scrim" ) ? . dataset . mode ;
if ( mode === "an-context" ) closeAnContext ();
else if ( mode === "reports-drawer" ) closeReports ();
2026-07-17 15:15:57 +02:00
});
// Extend ops closeDrawers awareness via custom event
window . addEventListener ( "cockpit-close-drawers" , closeReports );
}
2026-07-18 21:56:30 +02:00
window . cockpitReports = { open : openReports , close : closeReports , loadTab , closeAnContext };
2026-07-17 15:15:57 +02:00
if ( document . readyState === "loading" ) document . addEventListener ( "DOMContentLoaded" , init );
else init ();
})();