Files
ome-cockpit/ui/reports.js
T

1399 lines
62 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"].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) {
const top = entries.slice(0, maxN);
const max = Math.max(1, ...top.map(([, n]) => n));
return top
.map(
([label, n]) => `<div class="an-bar-row">
<div class="an-bar-label" title="${escapeHtml(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>
</div>`
)
.join("");
}
function donut(slices) {
const total = slices.reduce((s, x) => s + x.value, 0) || 1;
let acc = 0;
const parts = slices.map((s) => {
const start = (acc / total) * 100;
acc += s.value;
const end = (acc / total) * 100;
return `${s.color} ${start}% ${end}%`;
});
const legend = slices
.map(
(s) =>
`<li><span class="an-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)} <strong>${s.value}</strong></li>`
)
.join("");
return `<div class="an-donut-wrap">
<div class="an-donut" style="--slices:${parts.join(", ")}"></div>
<ul class="an-legend">${legend}</ul>
</div>`;
}
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([
{ label: "Critical", value: critComps || Number(byStatus.find(([k]) => k === "CRITICAL")?.[1] || 0), color: "#ff5c5c" },
{ label: "Warning", value: warnComps || Number(byStatus.find(([k]) => k === "WARNING")?.[1] || 0), color: "#ffb84d" },
{
label: "Other / OK",
value: Math.max(0, comps.length - critComps - warnComps),
color: "#3dffe0",
},
])}
<p class="an-note">${upgradeComps} components marked UPGRADE against Dell catalog</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") || '<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") || '<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([
{ label: "Connected", value: connected, color: "#3dffe0" },
{ label: "Offline", value: offline, color: "#5a6a7a" },
])}
<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([
{ label: "Expired", value: expired, color: "#ff5c5c" },
{ label: "≤ 30 days", value: d30, color: "#ffb84d" },
{ label: "3190 days", value: d90, color: "#00a8e8" },
{ label: "> 90 days", value: okW, color: "#3dffe0" },
])}
<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");
});
mount.querySelectorAll("[data-jump]").forEach((el) => {
el.addEventListener("click", () => {
const jump = el.dataset.jump;
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", () => {
if ($("#scrim")?.dataset.mode === "reports-drawer") closeReports();
});
// Extend ops closeDrawers awareness via custom event
window.addEventListener("cockpit-close-drawers", closeReports);
}
window.cockpitReports = { open: openReports, close: closeReports, loadTab };
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
else init();
})();