Add VLAN inventory, Present decks, and network fabric mapping.
Seed all ATC/FDE VLANs with live host inventory and editable notes, fix cluster membership on the map, and ship Present/network UI polish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+469
-36
@@ -375,41 +375,153 @@
|
||||
return [...map.entries()].sort((a, b) => b[1] - a[1]);
|
||||
}
|
||||
|
||||
function barRows(entries, maxN, fillClass) {
|
||||
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]) => `<div class="an-bar-row">
|
||||
<div class="an-bar-label" title="${escapeHtml(label)}">${escapeHtml(label)}</div>
|
||||
([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>
|
||||
</div>`
|
||||
</button>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function donut(slices) {
|
||||
function donut(slices, chartId) {
|
||||
const total = slices.reduce((s, x) => s + x.value, 0) || 1;
|
||||
let acc = 0;
|
||||
const parts = slices.map((s) => {
|
||||
const parts = [];
|
||||
const meta = [];
|
||||
slices.forEach((s) => {
|
||||
const start = (acc / total) * 100;
|
||||
acc += s.value;
|
||||
const end = (acc / total) * 100;
|
||||
return `${s.color} ${start}% ${end}%`;
|
||||
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 = slices
|
||||
const legend = meta
|
||||
.map(
|
||||
(s) =>
|
||||
`<li><span class="an-swatch" style="background:${s.color}"></span>${escapeHtml(s.label)} <strong>${s.value}</strong></li>`
|
||||
`<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">
|
||||
<div class="an-donut" style="--slices:${parts.join(", ")}"></div>
|
||||
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 || {};
|
||||
@@ -501,45 +613,55 @@
|
||||
<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>
|
||||
${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") || '<p class="an-note">No upgrade components</p>'}</div>
|
||||
<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") || '<p class="an-note">No outdated models</p>'}</div>
|
||||
<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([
|
||||
{ label: "Connected", value: connected, color: "#3dffe0" },
|
||||
{ label: "Offline", value: offline, color: "#5a6a7a" },
|
||||
])}
|
||||
${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([
|
||||
{ label: "Expired", value: expired, color: "#ff5c5c" },
|
||||
{ label: "≤ 30 days", value: d30, color: "#ffb84d" },
|
||||
{ label: "31–90 days", value: d90, color: "#00a8e8" },
|
||||
{ label: "> 90 days", value: okW, color: "#3dffe0" },
|
||||
])}
|
||||
${donut(
|
||||
[
|
||||
{ key: "expired", label: "Expired", value: expired, color: "#ff5c5c" },
|
||||
{ key: "d30", label: "≤ 30 days", value: d30, color: "#ffb84d" },
|
||||
{ key: "d90", label: "31–90 days", value: d90, color: "#ff9a3c" },
|
||||
{ key: "ok", label: "> 90 days", value: okW, color: "#3dffa0" },
|
||||
],
|
||||
"warranty"
|
||||
)}
|
||||
<p class="an-note">${warranties.length} warranty records from OME</p>
|
||||
</section>
|
||||
|
||||
@@ -579,9 +701,318 @@
|
||||
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: "31–90 days", ok: "> 90 days" };
|
||||
title = `Warranty · ${labels[key] || key}`;
|
||||
sub = "OME WarrantyService days_remaining buckets";
|
||||
rows = ctxRowsFromWarranty(list);
|
||||
pills = [`${list.length} records`, labels[key] || key];
|
||||
actions = [
|
||||
{
|
||||
id: "war",
|
||||
label: "Open Warranty tab",
|
||||
primary: true,
|
||||
run: () => {
|
||||
state.filters.warranty =
|
||||
key === "expired" ? "expired" : key === "d30" ? "d30" : key === "d90" ? "d90" : "ok";
|
||||
loadTab("warranty");
|
||||
},
|
||||
},
|
||||
];
|
||||
} else if (kind === "kpi") {
|
||||
// handled separately
|
||||
return;
|
||||
} else {
|
||||
title = `${kind} · ${key}`;
|
||||
sub = "Analytics selection";
|
||||
}
|
||||
|
||||
openAnContext({ title, sub, pills, rows, actions });
|
||||
}
|
||||
|
||||
mount.querySelectorAll("[data-an-kind]").forEach((el) => {
|
||||
el.addEventListener("click", (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
let kind = el.dataset.anKind;
|
||||
let key = el.dataset.anKey;
|
||||
let count = el.dataset.anCount;
|
||||
if (el.dataset.anDonut === "1") {
|
||||
const hit = sliceFromDonutClick(el, evt);
|
||||
if (!hit) return;
|
||||
key = hit.key;
|
||||
count = hit.value;
|
||||
}
|
||||
if (!key) return;
|
||||
handleAnContext(kind, key, count, evt);
|
||||
});
|
||||
});
|
||||
|
||||
mount.querySelectorAll("[data-jump]").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
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;
|
||||
@@ -1386,13 +1817,15 @@
|
||||
loadTab(chip.dataset.tab);
|
||||
});
|
||||
$("#scrim")?.addEventListener("click", () => {
|
||||
if ($("#scrim")?.dataset.mode === "reports-drawer") closeReports();
|
||||
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 };
|
||||
window.cockpitReports = { open: openReports, close: closeReports, loadTab, closeAnContext };
|
||||
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
|
||||
else init();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user