Files
ome-cockpit/ui/reports.js
T
root b4714c70d1 Add ATC Team chat, soft identity, and update Present decks.
Ship team/DM chat with files, avatars, unread alerts, and a first-open identity gate (including Guest); fold Team into Ops desk; clarify Datacenter Engineer vs "Data Plumbers" FDE roles; refresh Present story/tech slides to match.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 01:42:00 +02:00

1832 lines
78 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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, "&amp;")
.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", "#team-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();
})();