Add Reports drawer, Dell catalog compliance, and Service Tag chat context.
Exposes OME warranties, baselines, report defs, and CSV/JSON exports for customer-ready fleet briefs.
This commit is contained in:
@@ -747,6 +747,7 @@
|
||||
${node.is_server ? `<span class="badge">SERVER</span>` : ""}
|
||||
</div>
|
||||
<div class="kpi-kv">
|
||||
<div class="row"><span class="k">Service Tag</span><span class="v"><code>${escapeHtml(node.service_tag || "—")}</code></span></div>
|
||||
<div class="row"><span class="k">IP</span><span class="v">${escapeHtml(node.ip || "—")}</span></div>
|
||||
<div class="row"><span class="k">Subnet</span><span class="v">${escapeHtml(node.subnet || "—")}</span></div>
|
||||
<div class="row"><span class="k">Status</span><span class="v">${escapeHtml(node.status || "—")}</span></div>
|
||||
@@ -1424,7 +1425,7 @@
|
||||
const watts = node.watts != null ? `${Math.round(node.watts)} W` : "no power sample";
|
||||
tip.innerHTML = `
|
||||
<p class="nt-name">${escapeHtml(node.name || "node")}</p>
|
||||
<p class="nt-meta">${escapeHtml(node.model || "—")}<br/>
|
||||
<p class="nt-meta">${escapeHtml(node.model || "—")} · ST ${escapeHtml(node.service_tag || "—")}<br/>
|
||||
${node.connected ? "connected" : "offline"} · ${node.powered_on ? "powered on" : "power n/a"} · ${escapeHtml(watts)}<br/>
|
||||
${escapeHtml(node.ip || "no IP")} · ${escapeHtml(node.subnet || "")}</p>
|
||||
<p class="nt-hint">Double-click · Quick Connect</p>`;
|
||||
@@ -1582,7 +1583,62 @@
|
||||
updateFocusContext();
|
||||
}
|
||||
|
||||
async function showInspector(node) {
|
||||
async function loadWarrantyCompliance(deviceId) {
|
||||
const mount = $("#warranty-compliance-mount");
|
||||
if (!mount) return;
|
||||
try {
|
||||
const [wRes, cRes] = await Promise.all([
|
||||
fetch(`/api/devices/${deviceId}/warranty`),
|
||||
fetch(`/api/devices/${deviceId}/compliance`),
|
||||
]);
|
||||
const w = wRes.ok ? await wRes.json() : { items: [] };
|
||||
const c = cRes.ok ? await cRes.json() : { components: [] };
|
||||
const primary = w.primary;
|
||||
const comps = (c.components || []).filter(
|
||||
(x) =>
|
||||
String(x.update_action || "").toUpperCase() === "UPGRADE" ||
|
||||
String(x.compliance_status || "").toUpperCase() === "CRITICAL"
|
||||
);
|
||||
let html = `<div class="inv-section"><h3>Warranty</h3>`;
|
||||
if (primary) {
|
||||
html += `<div class="inv-line">ST ${escapeHtml(primary.service_tag || "—")} · ${escapeHtml(primary.service_level || "—")}</div>
|
||||
<div class="inv-line">Ends ${escapeHtml(primary.end_date || "—")} · ${primary.days_remaining ?? "—"} days left</div>`;
|
||||
} else if ((w.items || []).length) {
|
||||
html += (w.items || [])
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(x) =>
|
||||
`<div class="inv-line">${escapeHtml(x.service_level || "—")} · ends ${escapeHtml(x.end_date || "—")} · ${x.days_remaining ?? "—"}d</div>`
|
||||
)
|
||||
.join("");
|
||||
} else {
|
||||
html += `<div class="inv-line">No warranty records in OME</div>`;
|
||||
}
|
||||
html += `</div><div class="inv-section"><h3>Dell catalog compliance</h3>`;
|
||||
if (c.device) {
|
||||
html += `<div class="inv-line">${escapeHtml(c.device.compliance_status || c.device.firmware_status || "—")} · baseline ${escapeHtml((c.baseline || {}).name || "—")}</div>`;
|
||||
}
|
||||
if (comps.length) {
|
||||
html += comps
|
||||
.slice(0, 8)
|
||||
.map(
|
||||
(x) =>
|
||||
`<div class="inv-line">${escapeHtml(x.component || "component")}: ${escapeHtml(x.current_version || "?")} → ${escapeHtml(x.catalog_version || "?")} ${x.dell_uri ? `<a href="${escapeAttr(x.dell_uri)}" target="_blank" rel="noopener">dell.com</a>` : ""}</div>`
|
||||
)
|
||||
.join("");
|
||||
} else if (c.device) {
|
||||
html += `<div class="inv-line">No upgrade components in baseline report</div>`;
|
||||
} else {
|
||||
html += `<div class="inv-line">Not in active Dell baseline report</div>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
mount.innerHTML = html;
|
||||
} catch (err) {
|
||||
mount.innerHTML = `<p class="hint">Warranty/compliance unavailable</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function showInspector(node) {
|
||||
state.selectedId = node?.id ?? null;
|
||||
const empty = $("#inspector-empty");
|
||||
const body = $("#inspector-body");
|
||||
@@ -1596,7 +1652,7 @@
|
||||
body.innerHTML = `
|
||||
<div class="insp-head">
|
||||
<h2>${escapeHtml(node.name)}</h2>
|
||||
<p class="meta">${escapeHtml(node.model || "—")} · ${escapeHtml(node.service_tag || "no tag")}</p>
|
||||
<p class="meta">${escapeHtml(node.model || "—")} · id ${node.id}</p>
|
||||
</div>
|
||||
<div class="badge-row">
|
||||
<span class="badge ${node.connected ? "on" : "off"}">${node.connected ? "CONNECTED" : "OFFLINE"}</span>
|
||||
@@ -1606,8 +1662,12 @@
|
||||
${node.is_server ? `<span class="badge">SERVER</span>` : ""}
|
||||
</div>
|
||||
<div class="kv">
|
||||
<div class="kv-row st-row"><span class="k">Service Tag</span><span class="v st-value"><code>${escapeHtml(node.service_tag || "—")}</code>
|
||||
${node.service_tag ? `<button type="button" class="btn ghost compact" id="btn-copy-st" title="Copy Service Tag">Copy</button>` : ""}
|
||||
</span></div>
|
||||
<div class="kv-row"><span class="k">IP</span><span class="v">${escapeHtml(node.ip || "—")}</span></div>
|
||||
<div class="kv-row"><span class="k">Subnet</span><span class="v"><button type="button" class="linkish" data-jump-subnet="${escapeAttr(node.subnet)}">${escapeHtml(node.subnet)}</button></span></div>
|
||||
<div class="kv-row"><span class="k">Model</span><span class="v">${escapeHtml(node.model || "—")}</span></div>
|
||||
<div class="kv-row"><span class="k">Status</span><span class="v">${escapeHtml(node.status)}</span></div>
|
||||
<div class="kv-row"><span class="k">Avg W</span><span class="v">${node.avg_watts != null ? Math.round(node.avg_watts) + " W" : "—"}</span></div>
|
||||
<div class="kv-row"><span class="k">Peak W</span><span class="v">${node.peak_watts != null ? Math.round(node.peak_watts) + " W" : "—"}</span></div>
|
||||
@@ -1616,6 +1676,7 @@
|
||||
<div class="kv-row"><span class="k">Last status</span><span class="v">${escapeHtml(node.last_status_time || "—")}</span></div>
|
||||
<div class="kv-row"><span class="k">Inventory</span><span class="v">${escapeHtml(node.last_inventory_time || "—")}</span></div>
|
||||
</div>
|
||||
<div id="warranty-compliance-mount" class="wc-mount"><p class="hint">Loading warranty & Dell compliance…</p></div>
|
||||
<div class="action-stack">
|
||||
<button type="button" class="btn primary" id="btn-quick-connect">Quick Connect · iDRAC / SSH</button>
|
||||
${node.idrac_url ? `<a class="btn conn-link" href="${escapeAttr(node.idrac_url)}" target="_blank" rel="noopener">iDRAC Web</a>` : ""}
|
||||
@@ -1637,10 +1698,25 @@
|
||||
refreshLists();
|
||||
layout();
|
||||
});
|
||||
$("#btn-copy-st")?.addEventListener("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(node.service_tag);
|
||||
const btn = $("#btn-copy-st");
|
||||
if (btn) {
|
||||
btn.textContent = "Copied";
|
||||
setTimeout(() => {
|
||||
btn.textContent = "Copy";
|
||||
}, 1200);
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
});
|
||||
updateFocusContext();
|
||||
$("#btn-quick-connect")?.addEventListener("click", () => openConnect(node));
|
||||
$("#btn-ssh-term")?.addEventListener("click", () => window.cockpitSsh?.open(node));
|
||||
$("#btn-ask-ai")?.addEventListener("click", () => openAi(node));
|
||||
loadWarrantyCompliance(node.id);
|
||||
const cachedInv = state.inventoryCache[node.id];
|
||||
if (cachedInv) {
|
||||
const mount = $("#inv-mount");
|
||||
|
||||
Reference in New Issue
Block a user