b4714c70d1
Ship team/DM chat with files, avatars, unread alerts, and a first-open identity gate (including Guest); fold Team into Ops desk; clarify Datacenter Engineer vs "Data Plumbers" FDE roles; refresh Present story/tech slides to match. Co-authored-by: Cursor <cursoragent@cursor.com>
630 lines
22 KiB
JavaScript
630 lines
22 KiB
JavaScript
/**
|
||
* Console workspace — live iDRAC wall (4 or 8 draggable tiles).
|
||
* Only real iDRAC management endpoints (not OS host IPs).
|
||
*/
|
||
(() => {
|
||
const $ = (sel, root = document) => root.querySelector(sel);
|
||
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
|
||
|
||
const STORE_KEY = "cockpit_console_slots_v2";
|
||
const DENSITY_KEY = "cockpit_console_density_v1";
|
||
const SLOTS_KEY = "cockpit_console_slotcount_v1";
|
||
|
||
const state = {
|
||
slotCount: Number(localStorage.getItem(SLOTS_KEY) || 4) === 8 ? 8 : 4,
|
||
slots: [],
|
||
density: localStorage.getItem(DENSITY_KEY) || "medium",
|
||
filter: "",
|
||
subnet: "all",
|
||
fsDeviceId: null,
|
||
dragDeviceId: null,
|
||
dragFromSlot: null,
|
||
};
|
||
|
||
function esc(s) {
|
||
return String(s ?? "")
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function fleetDevices() {
|
||
return window.cockpit?.getState?.()?.data?.devices || [];
|
||
}
|
||
|
||
function fleetSubnets() {
|
||
return window.cockpit?.getState?.()?.data?.subnets || [];
|
||
}
|
||
|
||
/** Real iDRAC / BMC endpoints only — never bare OS inventory hosts. */
|
||
function isIdracHost(n) {
|
||
if (!n || n.id == null) return false;
|
||
if (n.is_idrac) return !!(n.idrac_ip || n.ip);
|
||
if (n.idrac_ip) return true;
|
||
if (n.idrac_url && /idrac/i.test(String(n.name || "") + String(n.idrac_url))) return true;
|
||
const name = String(n.name || "").toLowerCase();
|
||
if (name.includes("idrac") && (n.ip || n.idrac_ip)) return true;
|
||
return false;
|
||
}
|
||
|
||
function mgmtIp(n) {
|
||
return n?.idrac_ip || (n?.is_idrac ? n.ip : null) || null;
|
||
}
|
||
|
||
function idracPool() {
|
||
return fleetDevices()
|
||
.filter(isIdracHost)
|
||
.filter((n) => mgmtIp(n))
|
||
.slice()
|
||
.sort((a, b) => {
|
||
const sa = String(a.subnet || "");
|
||
const sb = String(b.subnet || "");
|
||
if (sa !== sb) return sa.localeCompare(sb);
|
||
return String(a.name || "").localeCompare(String(b.name || ""));
|
||
});
|
||
}
|
||
|
||
function deviceById(id) {
|
||
if (id == null) return null;
|
||
return fleetDevices().find((d) => String(d.id) === String(id)) || null;
|
||
}
|
||
|
||
function subnetMeta(cidr) {
|
||
const meta = fleetSubnets().find((s) => s.cidr === cidr) || {};
|
||
const colors = window.cockpit?.getState?.()?.subnetColors;
|
||
let color = meta.vlan_color;
|
||
if (!color && colors?.get) color = colors.get(cidr || "?");
|
||
if (!color) {
|
||
let h = 0;
|
||
for (const c of String(cidr || "?")) h = (h * 31 + c.charCodeAt(0)) >>> 0;
|
||
color = `hsl(${h % 360} 72% 52%)`;
|
||
}
|
||
return {
|
||
cidr: cidr || "—",
|
||
vlan: meta.vlan_id,
|
||
name: meta.name || meta.label || "",
|
||
color,
|
||
};
|
||
}
|
||
|
||
function embedUrl(deviceId) {
|
||
return `/api/idrac-proxy/${deviceId}/restgui/start.html?console`;
|
||
}
|
||
|
||
function resizeSlots(count) {
|
||
const next = Number(count) === 8 ? 8 : 4;
|
||
const prev = state.slots.slice();
|
||
state.slotCount = next;
|
||
state.slots = Array.from({ length: next }, (_, i) => prev[i] ?? null);
|
||
localStorage.setItem(SLOTS_KEY, String(next));
|
||
saveSlots();
|
||
}
|
||
|
||
function loadSlots() {
|
||
try {
|
||
const raw = JSON.parse(localStorage.getItem(STORE_KEY) || "[]");
|
||
if (Array.isArray(raw) && raw.length) {
|
||
state.slots = Array.from({ length: state.slotCount }, (_, i) => {
|
||
const id = raw[i] ?? null;
|
||
const n = deviceById(id);
|
||
return n && isIdracHost(n) ? id : null;
|
||
});
|
||
return;
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
state.slots = Array(state.slotCount).fill(null);
|
||
}
|
||
|
||
function saveSlots() {
|
||
try {
|
||
localStorage.setItem(STORE_KEY, JSON.stringify(state.slots));
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
|
||
function closeOtherDrawers() {
|
||
["#chat-drawer", "#ops-drawer", "#ai-drawer", "#reports-drawer", "#network-drawer", "#team-drawer"].forEach((id) => {
|
||
const el = $(id);
|
||
if (el) {
|
||
el.classList.remove("open");
|
||
el.setAttribute("aria-hidden", "true");
|
||
}
|
||
});
|
||
window.cockpitNetwork?.close?.();
|
||
}
|
||
|
||
function openConsole() {
|
||
const drawer = $("#console-drawer");
|
||
const scrim = $("#scrim");
|
||
if (!drawer) return;
|
||
closeOtherDrawers();
|
||
loadSlots();
|
||
drawer.classList.add("open");
|
||
drawer.setAttribute("aria-hidden", "false");
|
||
if (scrim) {
|
||
scrim.classList.add("open");
|
||
scrim.dataset.mode = "console-drawer";
|
||
}
|
||
renderAll();
|
||
requestAnimationFrame(() => drawer.classList.add("console-ready"));
|
||
}
|
||
|
||
function closeConsole() {
|
||
closeFullscreen();
|
||
const drawer = $("#console-drawer");
|
||
const scrim = $("#scrim");
|
||
drawer?.classList.remove("open", "console-ready", "is-dragging");
|
||
drawer?.setAttribute("aria-hidden", "true");
|
||
if (scrim?.dataset.mode === "console-drawer") {
|
||
scrim.classList.remove("open");
|
||
delete scrim.dataset.mode;
|
||
}
|
||
}
|
||
|
||
function setDensity(mode) {
|
||
state.density = mode === "small" ? "small" : "medium";
|
||
localStorage.setItem(DENSITY_KEY, state.density);
|
||
applyStageClasses();
|
||
$$("[data-console-density]").forEach((b) => {
|
||
b.classList.toggle("active", b.dataset.consoleDensity === state.density);
|
||
});
|
||
}
|
||
|
||
function setSlotCount(count) {
|
||
resizeSlots(count);
|
||
applyStageClasses();
|
||
$$("[data-console-slots]").forEach((b) => {
|
||
b.classList.toggle("active", Number(b.dataset.consoleSlots) === state.slotCount);
|
||
});
|
||
renderAll();
|
||
}
|
||
|
||
function applyStageClasses() {
|
||
const stage = $("#console-stage");
|
||
if (!stage) return;
|
||
stage.classList.toggle("density-small", state.density === "small");
|
||
stage.classList.toggle("density-medium", state.density === "medium");
|
||
stage.classList.toggle("slots-4", state.slotCount === 4);
|
||
stage.classList.toggle("slots-8", state.slotCount === 8);
|
||
}
|
||
|
||
function toast(msg) {
|
||
if (typeof window.showToast === "function") window.showToast(msg);
|
||
else if (window.cockpit?.showToast) window.cockpit.showToast(msg);
|
||
}
|
||
|
||
function assignSlot(index, deviceId) {
|
||
if (index < 0 || index >= state.slotCount) return;
|
||
if (deviceId != null) {
|
||
const n = deviceById(deviceId);
|
||
if (!n || !isIdracHost(n)) {
|
||
toast("Kies een iDRAC-endpoint (geen OS-host)");
|
||
return;
|
||
}
|
||
state.slots = state.slots.map((id, i) => (i !== index && String(id) === String(deviceId) ? null : id));
|
||
}
|
||
state.slots[index] = deviceId;
|
||
saveSlots();
|
||
renderStage();
|
||
renderFleet();
|
||
renderStats();
|
||
}
|
||
|
||
function clearSlot(index) {
|
||
assignSlot(index, null);
|
||
}
|
||
|
||
function swapSlots(a, b) {
|
||
if (a === b || a < 0 || b < 0 || a >= state.slotCount || b >= state.slotCount) return;
|
||
const tmp = state.slots[a];
|
||
state.slots[a] = state.slots[b];
|
||
state.slots[b] = tmp;
|
||
saveSlots();
|
||
renderStage();
|
||
}
|
||
|
||
function openFullscreen(deviceId) {
|
||
const node = deviceById(deviceId);
|
||
if (!node) return;
|
||
state.fsDeviceId = deviceId;
|
||
const modal = $("#console-fs-modal");
|
||
const frame = $("#console-fs-frame");
|
||
const title = $("#console-fs-title");
|
||
const sub = $("#console-fs-sub");
|
||
const net = $("#console-fs-net");
|
||
if (!modal || !frame) return;
|
||
const sm = subnetMeta(node.subnet);
|
||
title.textContent = node.name || "iDRAC";
|
||
sub.textContent = `${node.model || "—"} · ${node.service_tag || "no tag"} · ${mgmtIp(node) || ""}`;
|
||
net.innerHTML = netBadgeHtml(sm, node);
|
||
$$(`.console-tile[data-device-id="${deviceId}"] iframe`).forEach((f) => {
|
||
f.dataset.pausedSrc = f.src;
|
||
f.removeAttribute("src");
|
||
});
|
||
frame.src = embedUrl(deviceId);
|
||
modal.classList.remove("hidden");
|
||
modal.setAttribute("aria-hidden", "false");
|
||
}
|
||
|
||
function closeFullscreen() {
|
||
const modal = $("#console-fs-modal");
|
||
const frame = $("#console-fs-frame");
|
||
const id = state.fsDeviceId;
|
||
if (frame) frame.removeAttribute("src");
|
||
modal?.classList.add("hidden");
|
||
modal?.setAttribute("aria-hidden", "true");
|
||
if (id != null) {
|
||
$$(`.console-tile[data-device-id="${id}"] iframe`).forEach((f) => {
|
||
if (f.dataset.pausedSrc) {
|
||
f.src = f.dataset.pausedSrc;
|
||
delete f.dataset.pausedSrc;
|
||
} else {
|
||
f.src = embedUrl(id);
|
||
}
|
||
});
|
||
}
|
||
state.fsDeviceId = null;
|
||
}
|
||
|
||
function netBadgeHtml(sm, node) {
|
||
const vlan = sm.vlan != null ? `VLAN ${esc(sm.vlan)}` : "";
|
||
const name = sm.name ? esc(sm.name) : "";
|
||
const bits = [vlan, name, esc(sm.cidr)].filter(Boolean);
|
||
return `<span class="console-net-badge" style="--net:${esc(sm.color)}">
|
||
<i class="console-net-dot"></i>
|
||
<span>${bits.join(" · ") || "network —"}</span>
|
||
${node?.connected ? `<em class="on">live</em>` : `<em class="off">offline</em>`}
|
||
</span>`;
|
||
}
|
||
|
||
function renderStats() {
|
||
const pool = idracPool();
|
||
const live = pool.filter((n) => n.connected).length;
|
||
const filled = state.slots.filter(Boolean).length;
|
||
const nets = new Set(pool.map((n) => n.subnet).filter(Boolean)).size;
|
||
const el = $("#console-stats");
|
||
if (el) {
|
||
el.innerHTML = `
|
||
<span class="cs-pill"><strong>${pool.length}</strong> iDRACs</span>
|
||
<span class="cs-pill live"><strong>${live}</strong> connected</span>
|
||
<span class="cs-pill"><strong>${nets}</strong> networks</span>
|
||
<span class="cs-pill"><strong>${filled}/${state.slotCount}</strong> wall</span>`;
|
||
}
|
||
}
|
||
|
||
function filteredPool() {
|
||
const q = state.filter.trim().toLowerCase();
|
||
return idracPool().filter((n) => {
|
||
if (state.subnet !== "all" && n.subnet !== state.subnet) return false;
|
||
if (!q) return true;
|
||
const hay = [n.name, n.ip, n.idrac_ip, n.model, n.service_tag, n.subnet]
|
||
.join(" ")
|
||
.toLowerCase();
|
||
return hay.includes(q);
|
||
});
|
||
}
|
||
|
||
function renderSubnetChips() {
|
||
const host = $("#console-subnet-chips");
|
||
if (!host) return;
|
||
const pool = idracPool();
|
||
const counts = new Map();
|
||
pool.forEach((n) => {
|
||
const c = n.subnet || "unknown";
|
||
counts.set(c, (counts.get(c) || 0) + 1);
|
||
});
|
||
const chips = [
|
||
`<button type="button" class="chip ${state.subnet === "all" ? "active" : ""}" data-console-subnet="all">All nets · ${pool.length}</button>`,
|
||
];
|
||
[...counts.entries()]
|
||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||
.forEach(([cidr, n]) => {
|
||
const sm = subnetMeta(cidr);
|
||
const label = sm.vlan != null ? `VLAN ${sm.vlan}` : cidr;
|
||
chips.push(
|
||
`<button type="button" class="chip ${state.subnet === cidr ? "active" : ""}" data-console-subnet="${esc(cidr)}" style="--chip:${esc(sm.color)}">
|
||
<i class="console-chip-dot"></i>${esc(label)} · ${n}
|
||
</button>`
|
||
);
|
||
});
|
||
host.innerHTML = chips.join("");
|
||
}
|
||
|
||
function renderFleet() {
|
||
const host = $("#console-fleet-list");
|
||
if (!host) return;
|
||
const used = new Set(state.slots.filter(Boolean).map(String));
|
||
const list = filteredPool();
|
||
if (!list.length) {
|
||
host.innerHTML = `<p class="hint">No iDRAC endpoints match. OS hosts are hidden — only BMC/iDRAC IPs.</p>`;
|
||
return;
|
||
}
|
||
let lastSubnet = null;
|
||
const parts = [];
|
||
list.forEach((n) => {
|
||
if (n.subnet !== lastSubnet) {
|
||
lastSubnet = n.subnet;
|
||
const sm = subnetMeta(n.subnet);
|
||
parts.push(`<div class="console-fleet-group" style="--net:${esc(sm.color)}">
|
||
<span class="cfg-label">${sm.vlan != null ? `VLAN ${esc(sm.vlan)}` : ""}${sm.name ? ` · ${esc(sm.name)}` : ""} · <code>${esc(sm.cidr)}</code></span>
|
||
</div>`);
|
||
}
|
||
const inWall = used.has(String(n.id));
|
||
const ip = mgmtIp(n);
|
||
parts.push(`<div class="console-fleet-card ${n.connected ? "is-live" : "is-off"} ${inWall ? "on-wall" : ""}"
|
||
draggable="true" data-device-id="${esc(n.id)}" title="Drag onto a wall slot">
|
||
<span class="cfc-top">
|
||
<strong>${esc(n.name || "device")}</strong>
|
||
<span class="cfc-status">${n.connected ? "LIVE" : "OFF"}</span>
|
||
</span>
|
||
<span class="cfc-meta">${esc(n.model || "—")} · ${esc(n.service_tag || "no tag")}</span>
|
||
<span class="cfc-ip">${esc(ip || "—")}</span>
|
||
<span class="cfc-actions">
|
||
<button type="button" class="btn compact primary" data-fleet-act="add">${inWall ? "On wall" : "+ Wall"}</button>
|
||
</span>
|
||
</div>`);
|
||
});
|
||
host.innerHTML = parts.join("");
|
||
}
|
||
|
||
function tileHtml(index, deviceId) {
|
||
const node = deviceById(deviceId);
|
||
if (!node || !isIdracHost(node)) {
|
||
return `<div class="console-tile is-empty" data-slot="${index}" data-device-id="">
|
||
<div class="console-dropzone">
|
||
<span class="cd-kicker">Slot ${index + 1}</span>
|
||
<strong>Drop an iDRAC here</strong>
|
||
<p>Only BMC/iDRAC IPs · drag from fleet or click + Wall</p>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
const sm = subnetMeta(node.subnet);
|
||
const ip = mgmtIp(node);
|
||
return `<div class="console-tile is-filled ${node.connected ? "is-live" : "is-off"}" data-slot="${index}" data-device-id="${esc(node.id)}" draggable="true" style="--net:${esc(sm.color)}">
|
||
<header class="console-tile-chrome">
|
||
<div class="ctc-id">
|
||
<span class="ctc-slot">#${index + 1}</span>
|
||
<strong class="ctc-name" title="${esc(node.name)}">${esc(node.name || "iDRAC")}</strong>
|
||
${netBadgeHtml(sm, node)}
|
||
</div>
|
||
<div class="ctc-actions">
|
||
<button type="button" class="btn ghost compact" data-tile-act="reload" title="Reload">↻</button>
|
||
<button type="button" class="btn ghost compact" data-tile-act="pop" title="Open direct iDRAC">↗</button>
|
||
<button type="button" class="btn idrac-console compact" data-tile-act="fs" title="Fullscreen popup">Fullscreen</button>
|
||
<button type="button" class="btn ghost compact" data-tile-act="clear" title="Clear slot">×</button>
|
||
</div>
|
||
</header>
|
||
<div class="console-tile-meta">
|
||
<span>${esc(node.model || "—")}</span>
|
||
<span>${esc(node.service_tag || "—")}</span>
|
||
<span class="mono">${esc(ip || "")}</span>
|
||
<span class="${node.powered_on ? "on" : "off"}">${node.powered_on ? "POWERED" : "POWER N/A"}</span>
|
||
</div>
|
||
<div class="console-frame-wrap">
|
||
<div class="console-drop-shield" aria-hidden="true"></div>
|
||
<iframe title="iDRAC ${esc(node.name)}" src="${esc(embedUrl(node.id))}" allow="fullscreen; clipboard-read; clipboard-write" referrerpolicy="no-referrer-when-downgrade"></iframe>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderStage() {
|
||
const stage = $("#console-stage");
|
||
if (!stage) return;
|
||
applyStageClasses();
|
||
const prev = new Map();
|
||
$$(".console-tile iframe", stage).forEach((f) => {
|
||
const tile = f.closest(".console-tile");
|
||
if (!tile) return;
|
||
prev.set(`${tile.dataset.slot}:${tile.dataset.deviceId}`, f);
|
||
});
|
||
stage.innerHTML = state.slots.map((id, i) => tileHtml(i, id)).join("");
|
||
$$(".console-tile.is-filled", stage).forEach((tile) => {
|
||
const key = `${tile.dataset.slot}:${tile.dataset.deviceId}`;
|
||
const old = prev.get(key);
|
||
const frame = $("iframe", tile);
|
||
if (old && frame && old !== frame && old.src) frame.replaceWith(old);
|
||
});
|
||
}
|
||
|
||
function renderAll() {
|
||
renderStats();
|
||
renderSubnetChips();
|
||
renderFleet();
|
||
renderStage();
|
||
setDensity(state.density);
|
||
$$("[data-console-slots]").forEach((b) => {
|
||
b.classList.toggle("active", Number(b.dataset.consoleSlots) === state.slotCount);
|
||
});
|
||
}
|
||
|
||
function fillNextEmpty(deviceId) {
|
||
const idx = state.slots.findIndex((id) => id == null);
|
||
if (idx === -1) {
|
||
assignSlot(state.slotCount - 1, deviceId);
|
||
return;
|
||
}
|
||
assignSlot(idx, deviceId);
|
||
}
|
||
|
||
function autoFillLive() {
|
||
const live = idracPool().filter((n) => n.connected);
|
||
const picks = (live.length ? live : idracPool()).slice(0, state.slotCount);
|
||
state.slots = Array.from({ length: state.slotCount }, (_, i) => (picks[i] ? picks[i].id : null));
|
||
saveSlots();
|
||
renderAll();
|
||
}
|
||
|
||
function bindDrag() {
|
||
const drawer = $("#console-drawer");
|
||
if (!drawer || drawer.dataset.dragBound) return;
|
||
drawer.dataset.dragBound = "1";
|
||
|
||
drawer.addEventListener("dragstart", (e) => {
|
||
const card = e.target.closest(".console-fleet-card");
|
||
const tile = e.target.closest(".console-tile.is-filled");
|
||
// don't start drag from action buttons
|
||
if (e.target.closest("button")) return;
|
||
if (card) {
|
||
state.dragDeviceId = card.dataset.deviceId;
|
||
state.dragFromSlot = null;
|
||
e.dataTransfer.setData("text/plain", String(state.dragDeviceId));
|
||
e.dataTransfer.effectAllowed = "copyMove";
|
||
card.classList.add("dragging");
|
||
drawer.classList.add("is-dragging");
|
||
} else if (tile) {
|
||
state.dragDeviceId = tile.dataset.deviceId;
|
||
state.dragFromSlot = Number(tile.dataset.slot);
|
||
e.dataTransfer.setData("text/plain", String(state.dragDeviceId));
|
||
e.dataTransfer.effectAllowed = "move";
|
||
tile.classList.add("dragging");
|
||
drawer.classList.add("is-dragging");
|
||
} else {
|
||
return;
|
||
}
|
||
});
|
||
|
||
drawer.addEventListener("dragend", () => {
|
||
$$(".dragging", drawer).forEach((t) => t.classList.remove("dragging"));
|
||
$$(".console-tile.drag-over", drawer).forEach((t) => t.classList.remove("drag-over"));
|
||
drawer.classList.remove("is-dragging");
|
||
state.dragDeviceId = null;
|
||
state.dragFromSlot = null;
|
||
});
|
||
|
||
drawer.addEventListener("dragover", (e) => {
|
||
const tile = e.target.closest(".console-tile");
|
||
if (!tile) return;
|
||
e.preventDefault();
|
||
tile.classList.add("drag-over");
|
||
e.dataTransfer.dropEffect = state.dragFromSlot != null ? "move" : "copy";
|
||
});
|
||
|
||
drawer.addEventListener("dragleave", (e) => {
|
||
const tile = e.target.closest(".console-tile");
|
||
if (tile && !tile.contains(e.relatedTarget)) tile.classList.remove("drag-over");
|
||
});
|
||
|
||
drawer.addEventListener("drop", (e) => {
|
||
const tile = e.target.closest(".console-tile");
|
||
if (!tile) return;
|
||
e.preventDefault();
|
||
tile.classList.remove("drag-over");
|
||
const to = Number(tile.dataset.slot);
|
||
const deviceId = e.dataTransfer.getData("text/plain") || state.dragDeviceId;
|
||
if (deviceId == null || deviceId === "") return;
|
||
if (state.dragFromSlot != null) swapSlots(state.dragFromSlot, to);
|
||
else assignSlot(to, Number(deviceId) || deviceId);
|
||
drawer.classList.remove("is-dragging");
|
||
});
|
||
}
|
||
|
||
function bind() {
|
||
$("#btn-console")?.addEventListener("click", openConsole);
|
||
$("#btn-console-close")?.addEventListener("click", closeConsole);
|
||
$("#btn-console-autofill")?.addEventListener("click", autoFillLive);
|
||
$("#btn-console-clear")?.addEventListener("click", () => {
|
||
state.slots = Array(state.slotCount).fill(null);
|
||
saveSlots();
|
||
renderAll();
|
||
});
|
||
$("#console-search")?.addEventListener("input", (e) => {
|
||
state.filter = e.target.value || "";
|
||
renderFleet();
|
||
});
|
||
$("#console-subnet-chips")?.addEventListener("click", (e) => {
|
||
const chip = e.target.closest("[data-console-subnet]");
|
||
if (!chip) return;
|
||
state.subnet = chip.dataset.consoleSubnet || "all";
|
||
renderSubnetChips();
|
||
renderFleet();
|
||
});
|
||
$$("[data-console-density]").forEach((b) => {
|
||
b.addEventListener("click", () => setDensity(b.dataset.consoleDensity));
|
||
});
|
||
$$("[data-console-slots]").forEach((b) => {
|
||
b.addEventListener("click", () => setSlotCount(b.dataset.consoleSlots));
|
||
});
|
||
$("#console-fleet-list")?.addEventListener("click", (e) => {
|
||
const addBtn = e.target.closest("[data-fleet-act='add']");
|
||
const card = e.target.closest(".console-fleet-card");
|
||
if (addBtn && card) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
fillNextEmpty(Number(card.dataset.deviceId) || card.dataset.deviceId);
|
||
return;
|
||
}
|
||
if (!card) return;
|
||
$$(".console-fleet-card.selected").forEach((c) => c.classList.remove("selected"));
|
||
card.classList.add("selected");
|
||
});
|
||
$("#console-fleet-list")?.addEventListener("dblclick", (e) => {
|
||
const card = e.target.closest(".console-fleet-card");
|
||
if (!card || e.target.closest("button")) return;
|
||
fillNextEmpty(Number(card.dataset.deviceId) || card.dataset.deviceId);
|
||
});
|
||
$("#console-stage")?.addEventListener("click", (e) => {
|
||
const btn = e.target.closest("[data-tile-act]");
|
||
if (!btn) return;
|
||
const tile = btn.closest(".console-tile");
|
||
if (!tile) return;
|
||
const slot = Number(tile.dataset.slot);
|
||
const id = tile.dataset.deviceId;
|
||
const act = btn.dataset.tileAct;
|
||
if (act === "clear") clearSlot(slot);
|
||
else if (act === "reload") {
|
||
const frame = $("iframe", tile);
|
||
if (frame?.src) {
|
||
const u = frame.src;
|
||
frame.src = "about:blank";
|
||
setTimeout(() => {
|
||
frame.src = u;
|
||
}, 40);
|
||
}
|
||
} else if (act === "fs" && id) openFullscreen(id);
|
||
else if (act === "pop" && id) {
|
||
const n = deviceById(id);
|
||
const url = n?.idrac_url || (mgmtIp(n) ? `https://${mgmtIp(n)}/` : null);
|
||
if (url) window.open(url, "_blank", "noopener");
|
||
}
|
||
});
|
||
$("#btn-console-fs-close")?.addEventListener("click", closeFullscreen);
|
||
$("#console-fs-modal")?.addEventListener("click", (e) => {
|
||
if (e.target.id === "console-fs-modal") closeFullscreen();
|
||
});
|
||
$("#scrim")?.addEventListener("click", () => {
|
||
if ($("#scrim")?.dataset.mode === "console-drawer") closeConsole();
|
||
});
|
||
document.addEventListener("keydown", (e) => {
|
||
if (e.key === "Escape") {
|
||
if (!$("#console-fs-modal")?.classList.contains("hidden")) closeFullscreen();
|
||
else if ($("#console-drawer")?.classList.contains("open")) closeConsole();
|
||
}
|
||
});
|
||
bindDrag();
|
||
|
||
setInterval(() => {
|
||
if (!$("#console-drawer")?.classList.contains("open")) return;
|
||
renderStats();
|
||
renderSubnetChips();
|
||
renderFleet();
|
||
$$(".console-tile.is-filled").forEach((tile) => {
|
||
const n = deviceById(tile.dataset.deviceId);
|
||
if (!n) return;
|
||
tile.classList.toggle("is-live", !!n.connected);
|
||
tile.classList.toggle("is-off", !n.connected);
|
||
const badge = $(".console-net-badge", tile);
|
||
if (badge) badge.outerHTML = netBadgeHtml(subnetMeta(n.subnet), n);
|
||
});
|
||
}, 8000);
|
||
}
|
||
|
||
state.slots = Array(state.slotCount).fill(null);
|
||
loadSlots();
|
||
bind();
|
||
window.cockpitConsole = { open: openConsole, close: closeConsole, refresh: renderAll };
|
||
})();
|