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:
@@ -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 & 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", () => {
|
||||
|
||||
Reference in New Issue
Block a user