/** * 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, """); } 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 ` ${bits.join(" · ") || "network —"} ${node?.connected ? `live` : `offline`} `; } 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 = ` ${pool.length} iDRACs ${live} connected ${nets} networks ${filled}/${state.slotCount} wall`; } } 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 = [ ``, ]; [...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( `` ); }); 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 = `
No iDRAC endpoints match. OS hosts are hidden — only BMC/iDRAC IPs.
`; return; } let lastSubnet = null; const parts = []; list.forEach((n) => { if (n.subnet !== lastSubnet) { lastSubnet = n.subnet; const sm = subnetMeta(n.subnet); parts.push(`${esc(sm.cidr)}
Only BMC/iDRAC IPs · drag from fleet or click + Wall