Files
ome-cockpit/ui/reports.js
T

1832 lines
78 KiB
JavaScript
Raw Normal View History

/* OME Cockpit — Reports / export / compliance drawer */
(function () {
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const state = {
tab: "analytics",
fleet: null,
compliance: null,
warranties: null,
reportDefs: null,
jobs: null,
catalogs: null,
brief: null,
outdatedOnly: true,
search: "",
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.",
};
function escapeHtml(s) {
return String(s ?? "")
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function escapeAttr(s) {
return escapeHtml(s).replace(/'/g, "&#39;");
}
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;
["#chat-drawer", "#ops-drawer", "#ai-drawer", "#network-drawer", "#console-drawer"].forEach((id) => {
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 {
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") {
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>`;
}
}
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] || "";
return `
<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" : ""}>3190 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("")}
</div>
</div>`;
}
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 || []) });
}
function matchSearch(row, fields) {
const q = state.search.trim().toLowerCase();
if (!q) return true;
return fields.some((f) => String(row[f] ?? "").toLowerCase().includes(q));
}
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]);
}
function barRows(entries, maxN, fillClass, kind) {
const top = entries.slice(0, maxN);
const max = Math.max(1, ...top.map(([, n]) => n));
return top
.map(
([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>
<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>
</button>`
)
.join("");
}
function donut(slices, chartId) {
const total = slices.reduce((s, x) => s + x.value, 0) || 1;
let acc = 0;
const parts = [];
const meta = [];
slices.forEach((s) => {
const start = (acc / total) * 100;
acc += s.value;
const end = (acc / total) * 100;
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,
});
});
const legend = meta
.map(
(s) =>
`<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>`
)
.join("");
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>
<ul class="an-legend">${legend}</ul>
</div>`;
}
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];
}
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>
${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>
</section>
<section class="an-card">
<h3 title="Which firmware families need the most upgrades">Outdated by component</h3>
<div class="an-bars">${barRows(byComponent, 8, "crit", "component") || '<p class="an-note">No upgrade components</p>'}</div>
</section>
<section class="an-card">
<h3 title="Which server models appear most often in the non-compliant set">Outdated by model</h3>
<div class="an-bars">${barRows(byModel, 8, "warn", "model") || '<p class="an-note">No outdated models</p>'}</div>
</section>
<section class="an-card">
<h3 title="OME ConnectionState across the fleet snapshot">Connection state</h3>
${donut(
[
{ key: "connected", label: "Connected", value: connected, color: "#3dffa0" },
{ key: "offline", label: "Offline", value: offline, color: "#e53935" },
],
"connection"
)}
<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>
${donut(
[
{ key: "expired", label: "Expired", value: expired, color: "#ff5c5c" },
{ key: "d30", label: "≤ 30 days", value: d30, color: "#ffb84d" },
{ key: "d90", label: "3190 days", value: d90, color: "#ff9a3c" },
{ key: "ok", label: "> 90 days", value: okW, color: "#3dffa0" },
],
"warranty"
)}
<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");
});
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: "3190 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);
});
});
mount.querySelectorAll("[data-jump]").forEach((el) => {
el.addEventListener("click", (evt) => {
evt.preventDefault();
const jump = el.dataset.jump;
// 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;
}
}
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")
);
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"] });
mount.innerHTML =
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>
<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(
(r) => `<tr data-id="${r.id}" class="an-row-click" title="Open Hardware / Expansion" data-open-hw="${r.id}">
<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>`;
wireFilters(mount, () => renderFleet(mount));
mount.querySelectorAll("[data-open-hw]").forEach((tr) => {
tr.addEventListener("click", () => {
state.hwDeviceId = Number(tr.dataset.openHw);
loadTab("hardware");
});
});
}
function renderFirmware(mount) {
const sum = state.compliance?.summary || {};
let comps = state.compliance?.components || [];
const compOpts = [...new Set(comps.map((c) => shortComponent(c.component)))].sort();
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"
);
}
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"]));
mount.innerHTML =
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" },
],
}) +
`<div class="rpt-meta">
Baseline <strong>${escapeHtml(sum.baseline_name || "—")}</strong>
· showing ${comps.length} rows · outdated devices ${sum.outdated_devices ?? "—"}
</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>
<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>
</tr>`
)
.join("")}
</tbody>
</table></div>`;
wireFilters(mount, () => renderFirmware(mount));
}
function renderWarranty(mount) {
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"]));
mount.innerHTML =
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>
<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>`;
wireFilters(mount, () => renderWarranty(mount));
}
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"]));
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;
mount.innerHTML = `
${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,
})),
})}
<div class="rpt-grid-2">
<section class="rpt-panel">
<h3 title="Dell update catalogs registered in OME">Dell catalogs</h3>
<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>
<h3 title="Firmware baselines compared against Dell catalogs">Baselines</h3>
<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">
<h3 title="Recent JobService entries (read-only)">Recent OME jobs</h3>
<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>
<h3 class="rpt-h">OME ReportDefs (${shown.length})</h3>
<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>
${shown
.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>
<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>
</tr>`
)
.join("")}
</tbody>
</table></div>
<pre class="rpt-results hidden" id="rpt-ome-results"></pre>`;
wireFilters(mount, () => renderOme(mount, baselines));
$("#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}`);
} catch (err) {
alert("Run failed: " + err.message);
} 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);
} catch (err) {
if (out) out.textContent = String(err.message || err);
}
});
});
}
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) {
// legacy no-op kept for brief; prefer wireFilters
$("#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", () => {
const mode = $("#scrim")?.dataset.mode;
if (mode === "an-context") closeAnContext();
else if (mode === "reports-drawer") closeReports();
});
// Extend ops closeDrawers awareness via custom event
window.addEventListener("cockpit-close-drawers", closeReports);
}
window.cockpitReports = { open: openReports, close: closeReports, loadTab, closeAnContext };
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
else init();
})();