Add iDRAC Console wall, power control, and proxy embeds.

Operators can power nodes via OME jobs and view multiple live iDRAC
consoles in-panel (4/8 wall) through a same-origin reverse proxy;
Present decks updated for the new remote-ops story.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-07-19 00:16:19 +02:00
parent 795707e47b
commit 03e207b46d
9 changed files with 2066 additions and 31 deletions
+185 -7
View File
@@ -1194,12 +1194,18 @@
</div>
<div class="kpi-actions">
<button type="button" class="btn primary" data-kpi-act="focus">Focus on map</button>
<div class="power-actions">
<button type="button" class="btn power-on" data-kpi-act="power-on" title="Power on via OME → iDRAC">⏻ Power on</button>
<button type="button" class="btn power-off" data-kpi-act="power-off" title="Graceful shutdown via OME → iDRAC">Power off</button>
<button type="button" class="btn ghost" data-kpi-act="power-cycle" title="Power cycle via OME → iDRAC">Cycle</button>
</div>
<button type="button" class="btn idrac-console" data-kpi-act="idrac-console" ${node.ip || node.idrac_ip ? "" : "disabled"} title="Open iDRAC HTML5 console in Cockpit">▣ iDRAC console</button>
${node.idrac_url ? `<a class="btn conn-link" href="${escapeAttr(node.idrac_url)}" target="_blank" rel="noopener">iDRAC Web ↗</a>` : ""}
<button type="button" class="btn conn-link" data-kpi-act="ssh" ${node.ip ? "" : "disabled"}>SSH terminal</button>
<button type="button" class="btn conn-link" data-kpi-act="rdp" ${node.ip ? "" : "disabled"}>RDP popout</button>
<button type="button" class="btn" data-kpi-act="connect">Quick Connect</button>
<button type="button" class="btn" data-kpi-act="inventory">Inspector + inventory</button>
<button type="button" class="btn" data-kpi-act="chat">Ask Cockpit chat</button>
${node.idrac_url ? `<a class="btn conn-link" href="${escapeAttr(node.idrac_url)}" target="_blank" rel="noopener">iDRAC Web</a>` : ""}
</div>
<div class="inv-section">
<h3>Related alerts</h3>
@@ -1845,6 +1851,105 @@
showToast._t = setTimeout(() => el.classList.remove("show"), ms);
}
async function requestDevicePower(node, action) {
if (!node?.id) return;
const labels = {
on: "Power ON",
off: "graceful Power OFF",
cycle: "Power CYCLE",
force_off: "FORCE Power OFF",
};
const label = labels[action] || action;
if (action !== "on") {
const ok = window.confirm(
`${label} for ${node.name || "device"} (${node.service_tag || node.id}) via OME → iDRAC?\n\nThis runs a remote power job.`
);
if (!ok) return;
}
try {
showToast(`Sending ${label}`);
const res = await fetch(`/api/devices/${node.id}/power`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, confirm: action !== "on" }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const detail = data.detail;
const msg = Array.isArray(detail)
? detail.map((d) => d.msg || JSON.stringify(d)).join("; ")
: detail || data.message || res.statusText;
throw new Error(msg);
}
showToast(`${label} accepted · OME job #${data.job_id ?? "—"}`, { ms: 5000 });
// Optimistic badge: on → show powered, off → not powered
if (action === "on") node.powered_on = true;
if (action === "off" || action === "force_off") node.powered_on = false;
if (kpiPopup.key) renderKpiPopup();
} catch (e) {
showToast(`Power failed: ${e.message || e}`, { alert: true, ms: 8000 });
}
}
async function openIdracConsole(node) {
if (!node?.id) return;
const modal = $("#idrac-console-modal");
const frame = $("#idrac-console-frame");
const title = $("#idrac-console-title");
const sub = $("#idrac-console-sub");
const note = $("#idrac-console-note");
const scrim = $("#scrim");
if (!modal || !frame) {
const url = node.idrac_url || (node.ip ? `https://${node.ip}` : null);
if (url) window.open(url, "_blank", "noopener");
return;
}
title.textContent = node.name || "iDRAC console";
sub.textContent = `${node.model || "—"} · ${node.service_tag || "no tag"} · ${node.idrac_ip || node.ip || ""}`;
note.textContent = "Loading console URLs…";
frame.removeAttribute("src");
modal.dataset.deviceId = String(node.id);
modal.classList.remove("hidden");
modal.setAttribute("aria-hidden", "false");
scrim?.classList.add("open");
if (scrim) scrim.dataset.mode = "idrac-console";
try {
const res = await fetch(`/api/devices/${node.id}/idrac-console`);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || res.statusText);
// Prefer same-origin proxy embed (strips X-Frame-Options)
const primary = data.primary || data.urls?.embed_path || data.urls?.html5 || data.urls?.web;
modal.dataset.primary = primary || "";
modal.dataset.web = data.urls?.web || "";
modal.dataset.popout = data.urls?.web || data.urls?.html5 || primary || "";
note.textContent = data.note || "Log in with your iDRAC credentials inside the panel.";
if (primary) {
frame.src = primary;
} else {
note.textContent = "No console URL available.";
}
} catch (e) {
note.textContent = `Console lookup failed: ${e.message || e}`;
const fallback = `/api/idrac-proxy/${node.id}/restgui/start.html?console`;
modal.dataset.primary = fallback;
modal.dataset.popout = node.idrac_url || (node.ip ? `https://${node.ip}/` : fallback);
frame.src = fallback;
}
}
function closeIdracConsole() {
const modal = $("#idrac-console-modal");
const frame = $("#idrac-console-frame");
const scrim = $("#scrim");
if (frame) frame.removeAttribute("src");
modal?.classList.add("hidden");
modal?.setAttribute("aria-hidden", "true");
if (scrim?.dataset.mode === "idrac-console") {
scrim.classList.remove("open");
delete scrim.dataset.mode;
}
}
function pushNotifyCard(ev) {
const host = $("#notify-stack");
if (!host) return;
@@ -1952,11 +2057,26 @@
const ip = node.ip;
const idrac = node.idrac_url || (ip ? `https://${ip}` : null);
$("#connect-grid").innerHTML = `
${idrac ? `<a class="connect-tile primary conn-link" href="${escapeAttr(idrac)}" target="_blank" rel="noopener">
<button type="button" class="connect-tile primary" id="btn-modal-power-on">
<span class="ct-ico">⏻</span>
<span class="ct-t">Power on</span>
<span class="ct-s">OME → iDRAC · boot server</span>
</button>
<button type="button" class="connect-tile danger" id="btn-modal-power-off">
<span class="ct-ico">⏻</span>
<span class="ct-t">Power off</span>
<span class="ct-s">Graceful shutdown via OME</span>
</button>
${idrac ? `<button type="button" class="connect-tile idrac-console" id="btn-modal-idrac-console">
<span class="ct-ico">▣</span>
<span class="ct-t">iDRAC console</span>
<span class="ct-s">Embed in Cockpit · ${escapeHtml(ip)}</span>
</button>` : `<div class="connect-tile" style="opacity:.45"><span class="ct-t">iDRAC console</span><span class="ct-s">No management IP</span></div>`}
${idrac ? `<a class="connect-tile conn-link" href="${escapeAttr(idrac)}" target="_blank" rel="noopener">
<span class="ct-ico">↗</span>
<span class="ct-t">iDRAC Web</span>
<span class="ct-s">BMC console · ${escapeHtml(ip)}</span>
</a>` : `<div class="connect-tile" style="opacity:.45"><span class="ct-t">iDRAC Web</span><span class="ct-s">No management IP</span></div>`}
<span class="ct-s">Open in new tab · ${escapeHtml(ip)}</span>
</a>` : ""}
${ip ? `<button type="button" class="connect-tile conn-link" id="btn-modal-ssh">
<span class="ct-ico">〉_</span>
<span class="ct-t">SSH terminal</span>
@@ -1995,6 +2115,19 @@
}
return;
}
if (e.target.closest("#btn-modal-power-on")) {
await requestDevicePower(node, "on");
return;
}
if (e.target.closest("#btn-modal-power-off")) {
await requestDevicePower(node, "off");
return;
}
if (e.target.closest("#btn-modal-idrac-console")) {
closeConnect();
openIdracConsole(node);
return;
}
if (e.target.closest("#btn-modal-ssh")) {
closeConnect();
window.cockpitSsh?.open(node);
@@ -2227,7 +2360,13 @@
<div id="warranty-compliance-mount" class="wc-mount"><p class="hint">Loading warranty &amp; 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>` : ""}
<div class="power-actions">
<button type="button" class="btn power-on" id="btn-power-on">⏻ Power on</button>
<button type="button" class="btn power-off" id="btn-power-off">Power off</button>
<button type="button" class="btn ghost" id="btn-power-cycle">Cycle</button>
</div>
${node.ip || node.idrac_ip ? `<button type="button" class="btn idrac-console" id="btn-idrac-console">▣ iDRAC console</button>` : ""}
${node.idrac_url ? `<a class="btn conn-link" href="${escapeAttr(node.idrac_url)}" target="_blank" rel="noopener">iDRAC Web ↗</a>` : ""}
${node.ip ? `<button type="button" class="btn conn-link" id="btn-ssh-term">SSH terminal · ${escapeHtml(node.ip)}</button>` : ""}
${node.ip ? `<button type="button" class="btn conn-link" id="btn-rdp-term">RDP popout · ${escapeHtml(node.rdp_host || node.os_hostname || node.ip)}</button>` : ""}
<button type="button" class="btn" id="btn-detail">Load full inventory + app landscape</button>
@@ -2263,6 +2402,10 @@
});
updateFocusContext();
$("#btn-quick-connect")?.addEventListener("click", () => openConnect(node));
$("#btn-power-on")?.addEventListener("click", () => requestDevicePower(node, "on"));
$("#btn-power-off")?.addEventListener("click", () => requestDevicePower(node, "off"));
$("#btn-power-cycle")?.addEventListener("click", () => requestDevicePower(node, "cycle"));
$("#btn-idrac-console")?.addEventListener("click", () => openIdracConsole(node));
$("#btn-ssh-term")?.addEventListener("click", () => window.cockpitSsh?.open(node));
$("#btn-rdp-term")?.addEventListener("click", () => window.cockpitRdp?.open(node));
$("#btn-ask-ai")?.addEventListener("click", () => openAi(node));
@@ -2617,6 +2760,14 @@
closeKpiPopup();
focusDeviceId(node.id);
showToast("Focused " + (node.name || ""));
} else if (a === "power-on") {
requestDevicePower(node, "on");
} else if (a === "power-off") {
requestDevicePower(node, "off");
} else if (a === "power-cycle") {
requestDevicePower(node, "cycle");
} else if (a === "idrac-console") {
openIdracConsole(node);
} else if (a === "ssh") {
window.cockpitSsh?.open(node);
} else if (a === "rdp") {
@@ -2857,6 +3008,9 @@
openConnect,
showInspector,
openAi,
requestDevicePower,
openIdracConsole,
closeIdracConsole,
relTime,
escapeHtml,
openTriage: null,
@@ -2924,20 +3078,44 @@
$("#btn-ai")?.addEventListener("click", () => openAi(null));
$("#btn-ai-close").addEventListener("click", closeAi);
$("#btn-connect-close")?.addEventListener("click", closeConnect);
$("#btn-idrac-console-close")?.addEventListener("click", closeIdracConsole);
$("#btn-idrac-popout")?.addEventListener("click", () => {
const modal = $("#idrac-console-modal");
const url = modal?.dataset.popout || modal?.dataset.web || modal?.dataset.primary;
if (url) window.open(url, "_blank", "noopener,noreferrer");
});
$("#btn-idrac-web")?.addEventListener("click", () => {
const modal = $("#idrac-console-modal");
const url = modal?.dataset.web || modal?.dataset.popout || modal?.dataset.primary;
if (url) window.open(url, "_blank", "noopener,noreferrer");
});
$("#btn-idrac-reload")?.addEventListener("click", () => {
const frame = $("#idrac-console-frame");
const modal = $("#idrac-console-modal");
const url = modal?.dataset.primary;
if (frame && url) {
frame.src = "about:blank";
setTimeout(() => {
frame.src = url;
}, 30);
}
});
$("#scrim").addEventListener("click", () => {
const mode = $("#scrim")?.dataset.mode;
if (mode === "triage") document.getElementById("btn-triage-close")?.click();
else if (mode === "kpi") closeKpiPopup();
else if (mode === "connect") closeConnect();
else if (mode === "idrac-console") closeIdracConsole();
else if (
mode === "chat-drawer" ||
mode === "ops-drawer" ||
mode === "ai-drawer" ||
mode === "reports-drawer" ||
mode === "an-context" ||
mode === "network-drawer"
mode === "network-drawer" ||
mode === "console-drawer"
) {
/* reports.js / ops.js / network.js also listen */
/* reports.js / ops.js / network.js / console.js also listen */
} else closeAi();
});
$("#btn-ome-console").addEventListener("click", () => {