(() => { const $ = (sel) => document.querySelector(sel); const canvas = $("#topo"); const ctx = canvas.getContext("2d"); const state = { data: null, viewMode: "status", layoutMode: "orbit", clusterHubs: [], laneGuides: [], subnet: "all", model: null, groupHint: null, kpiFocus: null, selectedId: null, inventoryCache: {}, inventoryLoadingId: null, notifiedEventKeys: new Set(), lastPulse: 0, filters: { connected: false, powered: false, servers: true, idrac: false, hasPower: false, ghostLinks: false, pulseLinks: true, minWatts: 0, search: "", }, cam: { x: 0, y: 0, scale: 1 }, nodes: [], hub: null, dragging: false, last: null, hoverId: null, connectNode: null, pulseT: 0, modelColors: new Map(), subnetColors: new Map(), }; const PALETTE = [ "#00a8e8", "#3dffe0", "#ffb020", "#7ec8ff", "#3dffa0", "#ff7a59", "#c4a7ff", "#f0e68c", "#5eead4", "#f472b6", ]; function colorFor(map, key) { if (!map.has(key)) map.set(key, PALETTE[map.size % PALETTE.length]); return map.get(key); } function statusColor(status) { const s = String(status || ""); if (s === "1000" || s === "0") return "#3dffa0"; if (s === "2000" || s === "2") return "#ffb020"; if (s === "3000" || s === "3" || s === "4000") return "#ff5c5c"; return "#7a93a8"; } function powerColor(watts) { if (watts == null) return "#3a4a5a"; if (watts < 200) return "#3dffa0"; if (watts < 450) return "#00a8e8"; if (watts < 650) return "#ffb020"; return "#ff5c5c"; } function nodeColor(n) { switch (state.viewMode) { case "power": return powerColor(n.watts); case "connection": return n.connected ? "#3dffe0" : "#ff5c5c"; case "model": return colorFor(state.modelColors, n.model || "?"); case "subnet": return colorFor(state.subnetColors, n.subnet || "?"); default: if (!n.connected) return "#5a6a7a"; return statusColor(n.status); } } function visibleDevices() { const d = state.data; if (!d) return []; const f = state.filters; const q = (f.search || "").trim().toLowerCase(); // Type filters are INCLUDE toggles: // - Servers on => include servers // - iDRACs on => include iDRACs // - both off => show nothing (do not silently show all) const includeServers = !!f.servers; const includeIdrac = !!f.idrac; if (!includeServers && !includeIdrac) return []; return (d.devices || []).filter((n) => { if (state.subnet !== "all" && n.subnet !== state.subnet) return false; if (state.model && n.model !== state.model) return false; const isServer = !!n.is_server; const isIdrac = !!n.is_idrac; const typeOk = (includeServers && isServer) || (includeIdrac && isIdrac); if (!typeOk) return false; if (f.connected && !n.connected) return false; if (f.powered && !n.powered_on) return false; if (f.hasPower && n.watts == null) return false; if (f.minWatts > 0 && (n.watts == null || n.watts < f.minWatts)) return false; if (state.kpiFocus === "offline" && (n.connected || !n.is_server)) return false; if (state.kpiFocus === "connected" && !n.connected) return false; if (state.kpiFocus === "powered" && !n.powered_on) return false; if (state.kpiFocus === "idracs" && !n.is_idrac) return false; if (state.kpiFocus === "power" && n.watts == null) return false; if (q) { const hay = [n.name, n.model, n.service_tag, n.ip, n.subnet] .filter(Boolean) .join(" ") .toLowerCase(); if (!hay.includes(q)) return false; } return true; }); } function groupBySubnet(devices) { const bySubnet = new Map(); for (const n of devices) { const k = n.subnet || "unknown"; if (!bySubnet.has(k)) bySubnet.set(k, []); bySubnet.get(k).push(n); } return [...bySubnet.keys()].sort().map((k) => ({ cidr: k, members: bySubnet.get(k) })); } function nodeRadius(n) { return n.connected ? 8 : 5.5; } function setTargets(nodes) { // preserve current x/y for lerp; first time seed from target const prev = new Map(state.nodes.map((n) => [n.id, n])); for (const n of nodes) { const p = prev.get(n.id); if (p && Number.isFinite(p.x)) { n.x = p.x; n.y = p.y; } else { n.x = n.tx; n.y = n.ty; } } state.nodes = nodes; } function layoutOrbit(devices, cx, cy, w, h) { const groups = groupBySubnet(devices); const ringBase = Math.min(w, h) * 0.18; const nodes = []; groups.forEach((g, si) => { const ring = ringBase + si * Math.min(70, Math.max(42, 360 / Math.max(groups.length, 1))); g.members.forEach((n, i) => { const a = (i / Math.max(g.members.length, 1)) * Math.PI * 2 - Math.PI / 2 + si * 0.15; const jitter = (n.id % 7) * 2.2; nodes.push({ ...n, tx: cx + Math.cos(a) * (ring + jitter), ty: cy + Math.sin(a) * (ring + jitter), r: nodeRadius(n), depth: 1, }); }); }); state.clusterHubs = []; state.laneGuides = []; setTargets(nodes); } function layoutGalaxy(devices, cx, cy, w, h) { const groups = groupBySubnet(devices); const nodes = []; const arms = Math.max(groups.length, 1); const maxR = Math.min(w, h) * 0.42; groups.forEach((g, si) => { const armAngle = (si / arms) * Math.PI * 2; g.members.forEach((n, i) => { const t = (i + 1) / (g.members.length + 1); const r = 70 + t * maxR; const twist = t * 3.2 + armAngle; const wobble = Math.sin(i * 1.7 + si) * 12; nodes.push({ ...n, tx: cx + Math.cos(twist) * r + Math.cos(twist + Math.PI / 2) * wobble * 0.35, ty: cy + Math.sin(twist) * r + Math.sin(twist + Math.PI / 2) * wobble * 0.35, r: nodeRadius(n), depth: 0.6 + t * 0.8, arm: si, }); }); }); state.clusterHubs = []; state.laneGuides = []; setTargets(nodes); } function layoutClusters(devices, cx, cy, w, h) { const groups = groupBySubnet(devices); const nodes = []; const hubs = []; const R = Math.min(w, h) * 0.32; groups.forEach((g, si) => { const a = (si / Math.max(groups.length, 1)) * Math.PI * 2 - Math.PI / 2; const hx = cx + Math.cos(a) * R; const hy = cy + Math.sin(a) * R; hubs.push({ x: hx, y: hy, label: g.cidr, color: colorFor(state.subnetColors, g.cidr) }); const localR = 28 + Math.min(90, g.members.length * 4.5); g.members.forEach((n, i) => { const la = (i / Math.max(g.members.length, 1)) * Math.PI * 2; nodes.push({ ...n, tx: hx + Math.cos(la) * localR, ty: hy + Math.sin(la) * localR, r: nodeRadius(n), depth: 1, cluster: si, }); }); }); state.clusterHubs = hubs; state.laneGuides = []; setTargets(nodes); } function layoutLanes(devices, cx, cy, w, h) { const groups = groupBySubnet(devices); const nodes = []; const guides = []; const top = 70; const bottom = h - 50; const span = Math.max(bottom - top, 120); groups.forEach((g, si) => { const y = top + (groups.length <= 1 ? span / 2 : (si / (groups.length - 1 || 1)) * span); guides.push({ y, label: g.cidr, color: colorFor(state.subnetColors, g.cidr) }); g.members.forEach((n, i) => { const t = g.members.length <= 1 ? 0.5 : i / (g.members.length - 1); const x = 90 + t * (w - 160); const bob = Math.sin(i * 0.9 + si) * 10; nodes.push({ ...n, tx: x, ty: y + bob, r: nodeRadius(n), depth: 1, lane: si, }); }); }); state.clusterHubs = []; state.laneGuides = guides; // hub left side state.hub = { x: 48, y: cy, r: 26 }; setTargets(nodes); } function layoutHelix(devices, cx, cy, w, h) { const list = [...devices].sort((a, b) => Number(b.connected) - Number(a.connected) || (b.watts || 0) - (a.watts || 0)); const nodes = []; const turns = 2.4; list.forEach((n, i) => { const t = list.length <= 1 ? 0.5 : i / (list.length - 1); const angle = t * Math.PI * 2 * turns; const y = 60 + t * (h - 120); const amp = Math.min(w, h) * 0.28; // perspective: scale by "depth" const depth = 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(angle)); const x = cx + Math.cos(angle) * amp * depth; nodes.push({ ...n, tx: x, ty: y + Math.sin(angle * 2) * 8, r: (n.connected ? 7 : 4.5) * (0.75 + depth * 0.55), depth, helixT: t, }); }); state.clusterHubs = []; state.laneGuides = []; setTargets(nodes); } function layout() { const devices = visibleDevices(); const w = canvas.clientWidth; const h = canvas.clientHeight; const cx = w / 2; const cy = h / 2; if (state.layoutMode !== "lanes") { state.hub = { x: cx, y: cy, r: 28 }; } switch (state.layoutMode) { case "galaxy": layoutGalaxy(devices, cx, cy, w, h); break; case "clusters": layoutClusters(devices, cx, cy, w, h); break; case "lanes": layoutLanes(devices, cx, cy, w, h); break; case "helix": layoutHelix(devices, cx, cy, w, h); break; default: layoutOrbit(devices, cx, cy, w, h); } } function resize() { const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = canvas.clientWidth; const h = canvas.clientHeight; canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); layout(); } function drawHub(hub, t) { const hubGlow = 16 + 6 * Math.sin(t * 0.05); const g = ctx.createRadialGradient(hub.x, hub.y, 4, hub.x, hub.y, hubGlow + 20); g.addColorStop(0, "rgba(0,118,206,0.55)"); g.addColorStop(1, "rgba(0,118,206,0)"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(hub.x, hub.y, hubGlow + 20, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(hub.x, hub.y, hub.r, 0, Math.PI * 2); ctx.fillStyle = "#0076ce"; ctx.fill(); ctx.strokeStyle = "#3dffe0"; ctx.lineWidth = 2; ctx.stroke(); ctx.fillStyle = "#fff"; ctx.font = "700 11px IBM Plex Sans, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText("OME", hub.x, hub.y); } function drawPulseLink(x0, y0, x1, y1, connected, t, id, alphaScale) { if (!connected && !state.filters.ghostLinks) return; const animate = state.filters.pulseLinks !== false; const alpha = connected ? (animate ? (0.55 + 0.35 * Math.sin(t * 0.05 + id)) : 0.72) * alphaScale : 0.06 * alphaScale; // glow underlay for live OME links if (connected) { ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.strokeStyle = `rgba(0,168,232,${(animate ? 0.22 : 0.14) * alphaScale})`; ctx.lineWidth = animate ? 4.5 : 3.2; ctx.stroke(); } ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.strokeStyle = connected ? `rgba(61,255,224,${alpha})` : `rgba(90,106,122,${0.14 * alphaScale})`; ctx.lineWidth = connected ? 2.4 : 0.8; ctx.stroke(); if (connected && animate) { // dual packets along the link for (const off of [0, 0.5]) { const u = ((t * 0.018 + (id % 50) * 0.02 + off) % 1); const px = x0 + (x1 - x0) * u; const py = y0 + (y1 - y0) * u; ctx.beginPath(); ctx.arc(px, py, 3.2, 0, Math.PI * 2); ctx.fillStyle = "rgba(255,255,255,0.95)"; ctx.fill(); ctx.beginPath(); ctx.arc(px, py, 5.5, 0, Math.PI * 2); ctx.strokeStyle = "rgba(61,255,224,0.55)"; ctx.lineWidth = 1.5; ctx.stroke(); } } } function drawDecor(w, h, hub, t) { const mode = state.layoutMode; if (mode === "orbit" || mode === "galaxy") { for (let i = 1; i <= 5; i++) { ctx.beginPath(); ctx.arc(hub.x, hub.y, 70 * i * 0.5, 0, Math.PI * 2); ctx.strokeStyle = `rgba(0,168,232,${0.035 + i * 0.012})`; ctx.lineWidth = 1; ctx.stroke(); } if (mode === "galaxy") { // faint spiral guide ctx.beginPath(); for (let i = 0; i <= 120; i++) { const u = i / 120; const ang = u * Math.PI * 4; const r = 40 + u * Math.min(w, h) * 0.4; const x = hub.x + Math.cos(ang) * r; const y = hub.y + Math.sin(ang) * r; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle = "rgba(0,168,232,0.08)"; ctx.lineWidth = 1.2; ctx.stroke(); } } else if (mode === "clusters") { for (const ch of state.clusterHubs || []) { ctx.beginPath(); ctx.arc(ch.x, ch.y, 52, 0, Math.PI * 2); ctx.strokeStyle = (ch.color || "#00a8e8") + "33"; ctx.lineWidth = 1.2; ctx.stroke(); ctx.beginPath(); ctx.arc(ch.x, ch.y, 10, 0, Math.PI * 2); ctx.fillStyle = ch.color || "#00a8e8"; ctx.globalAlpha = 0.55; ctx.fill(); ctx.globalAlpha = 1; ctx.fillStyle = "rgba(232,244,255,0.7)"; ctx.font = "500 9px IBM Plex Mono, monospace"; ctx.textAlign = "center"; ctx.fillText((ch.label || "").slice(0, 18), ch.x, ch.y - 18); drawPulseLink(hub.x, hub.y, ch.x, ch.y, true, t, (ch.label || "").length * 17, 0.55); } } else if (mode === "lanes") { for (const g of state.laneGuides || []) { ctx.beginPath(); ctx.moveTo(70, g.y); ctx.lineTo(w - 30, g.y); ctx.strokeStyle = (g.color || "#00a8e8") + "44"; ctx.lineWidth = 2; ctx.stroke(); // moving dashes const dashX = ((t * 1.8) % (w - 100)) + 70; ctx.beginPath(); ctx.arc(dashX, g.y, 2.5, 0, Math.PI * 2); ctx.fillStyle = g.color || "#3dffe0"; ctx.fill(); ctx.fillStyle = "rgba(232,244,255,0.55)"; ctx.font = "500 9px IBM Plex Mono, monospace"; ctx.textAlign = "left"; ctx.fillText((g.label || "").slice(0, 16), 74, g.y - 8); } } else if (mode === "helix") { // depth rails for (let i = 0; i < 3; i++) { ctx.beginPath(); for (let s = 0; s <= 80; s++) { const u = s / 80; const ang = u * Math.PI * 2 * 2.4 + i * 0.9; const y = 60 + u * (h - 120); const amp = Math.min(w, h) * 0.28; const depth = 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(ang)); const x = hub.x + Math.cos(ang) * amp * depth; if (s === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.strokeStyle = `rgba(0,168,232,${0.06 + i * 0.03})`; ctx.lineWidth = 1; ctx.stroke(); } } } function drawLinks(hub, t) { const mode = state.layoutMode; if (mode === "clusters") { // node to nearest cluster hub for (const n of state.nodes) { let best = null; let bestD = Infinity; for (const ch of state.clusterHubs || []) { const dx = n.x - ch.x; const dy = n.y - ch.y; const d = dx * dx + dy * dy; if (d < bestD) { bestD = d; best = ch; } } if (best) drawPulseLink(best.x, best.y, n.x, n.y, n.connected, t, n.id, 0.85); } return; } if (mode === "lanes") { for (const n of state.nodes) { drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 0.55); } return; } if (mode === "helix") { // chain neighbors + faint hub links for connected const sorted = [...state.nodes].sort((a, b) => (a.helixT || 0) - (b.helixT || 0)); for (let i = 0; i < sorted.length - 1; i++) { const a = sorted[i]; const b = sorted[i + 1]; drawPulseLink(a.x, a.y, b.x, b.y, a.connected || b.connected, t, a.id, 0.7); } for (const n of state.nodes) { if (n.connected) drawPulseLink(hub.x, hub.y, n.x, n.y, true, t, n.id, 0.25); } return; } // orbit / galaxy for (const n of state.nodes) { drawPulseLink(hub.x, hub.y, n.x, n.y, n.connected, t, n.id, 1); } } function drawNodes(t) { // draw far (small depth) first for helix const list = [...state.nodes].sort((a, b) => (a.depth || 1) - (b.depth || 1)); for (const n of list) { // lerp toward target if (Number.isFinite(n.tx)) { n.x += (n.tx - n.x) * 0.14; n.y += (n.ty - n.y) * 0.14; } const col = nodeColor(n); const selected = n.id === state.selectedId; const depth = n.depth || 1; if (selected) { ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 6, 0, Math.PI * 2); ctx.strokeStyle = "#fff"; ctx.lineWidth = 2; ctx.stroke(); } if (n.watts != null && state.viewMode === "power") { ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 3 + Math.min(10, n.watts / 80), 0, Math.PI * 2); ctx.strokeStyle = col + "55"; ctx.lineWidth = 2; ctx.stroke(); } ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); ctx.fillStyle = col; ctx.globalAlpha = 0.55 + 0.45 * Math.min(depth, 1); ctx.fill(); ctx.globalAlpha = 1; if (state.cam.scale > 0.85) { ctx.fillStyle = "rgba(232,244,255,0.85)"; ctx.font = "500 9px IBM Plex Mono, monospace"; ctx.textAlign = "center"; ctx.textBaseline = "top"; const label = state.viewMode === "power" && n.watts != null ? `${Math.round(n.watts)}W` : (n.name || "").slice(0, 18); ctx.fillText(label, n.x, n.y + n.r + 3); } } } function draw() { const w = canvas.clientWidth; const h = canvas.clientHeight; // hard clear in device pixels (avoids trail artifacts with DPR transform) ctx.save(); ctx.setTransform(1, 0, 0, 1, 0, 0); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.restore(); ctx.save(); ctx.translate(state.cam.x, state.cam.y); ctx.scale(state.cam.scale, state.cam.scale); const hub = state.hub; if (!hub) { ctx.restore(); requestAnimationFrame(draw); return; } const t = state.pulseT; drawDecor(w, h, hub, t); drawLinks(hub, t); drawHub(hub, t); drawNodes(t); ctx.restore(); if (!state.nodes.length) { ctx.fillStyle = "rgba(232,244,255,0.82)"; ctx.font = "600 15px IBM Plex Sans, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; const f = state.filters; const tip = (!f.servers && !f.idrac) ? "No types selected — enable Servers and/or iDRACs in Filters" : "No devices match the current filters"; ctx.fillText(tip, w / 2, h / 2); ctx.font = "500 12px IBM Plex Mono, monospace"; ctx.fillStyle = "rgba(122,147,168,0.95)"; ctx.fillText("Adjust Filters or click Reset filters", w / 2, h / 2 + 28); } if (state.filters.pulseLinks !== false) state.pulseT += 1; requestAnimationFrame(draw); } function screenToWorld(sx, sy) { return { x: (sx - state.cam.x) / state.cam.scale, y: (sy - state.cam.y) / state.cam.scale, }; } function hitTest(sx, sy) { const p = screenToWorld(sx, sy); if (state.hub) { const dx = p.x - state.hub.x; const dy = p.y - state.hub.y; if (dx * dx + dy * dy <= (state.hub.r + 6) ** 2) return { type: "hub" }; } let best = null; let bestD = Infinity; for (const n of state.nodes) { const dx = p.x - n.x; const dy = p.y - n.y; const d = dx * dx + dy * dy; if (d <= (n.r + 8) ** 2 && d < bestD) { bestD = d; best = n; } } return best ? { type: "node", node: best } : null; } const KPI_META = { total: { title: "All nodes", blurb: "Full OME inventory currently in cockpit." }, servers: { title: "Servers", blurb: "Server-class devices (OME type 1000)." }, idracs: { title: "iDRACs", blurb: "iDRAC / BMC endpoints discovered in OME." }, connected: { title: "Connected", blurb: "Devices with live OME connection state." }, offline: { title: "Offline", blurb: "Devices currently not connected to OME." }, powered: { title: "Powered on", blurb: "Devices reporting powered-on state." }, power: { title: "Live power", blurb: "Nodes with a live watt reading — hottest first." }, avgw: { title: "Average watts / node", blurb: "Power samples contributing to fleet average." }, samples: { title: "Power samples", blurb: "Devices included in the live power sample set." }, }; let kpiPopup = { key: null, selectedId: null, q: "", sort: "name" }; function devicesForKpi(key) { const all = state.data?.devices || []; switch (key) { case "servers": return all.filter((d) => d.is_server); case "idracs": return all.filter((d) => d.is_idrac); case "connected": return all.filter((d) => d.connected); case "offline": return all.filter((d) => !d.connected); case "powered": return all.filter((d) => d.powered_on); case "power": case "avgw": case "samples": return all.filter((d) => d.watts != null); case "total": default: return all.slice(); } } function sortKpiDevices(list) { const sort = kpiPopup.sort || "name"; const arr = list.slice(); if (sort === "watts") arr.sort((a, b) => (b.watts || 0) - (a.watts || 0)); else if (sort === "status") arr.sort((a, b) => String(a.status || "").localeCompare(String(b.status || ""))); else if (sort === "subnet") arr.sort((a, b) => String(a.subnet || "").localeCompare(String(b.subnet || ""))); else arr.sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))); return arr; } function filterKpiDevices(list) { const q = (kpiPopup.q || "").trim().toLowerCase(); if (!q) return list; return list.filter((d) => [d.name, d.ip, d.model, d.service_tag, d.subnet, d.status] .filter(Boolean) .join(" ") .toLowerCase() .includes(q) ); } function kpiSummaryHtml(key, list) { const connected = list.filter((d) => d.connected).length; const offline = list.length - connected; const powered = list.filter((d) => d.powered_on).length; const withW = list.filter((d) => d.watts != null); const watts = withW.reduce((a, d) => a + (d.watts || 0), 0); const subnets = new Set(list.map((d) => d.subnet).filter(Boolean)).size; const pills = [ `${list.length} in view`, `${connected} connected`, `${offline} offline`, `${powered} powered`, withW.length ? `${Math.round(watts)} W sampled` : "no power samples", `${subnets} subnets`, ]; if (key === "power" || key === "avgw" || key === "samples") { const top = sortKpiDevices(withW).slice(0, 1)[0]; if (top) pills.push(`hottest ${top.name?.slice(0, 22)} ${Math.round(top.watts)}W`); } return pills.map((p) => `${escapeHtml(p)}`).join(""); } function renderKpiDetail(node) { const el = $("#kpi-detail"); if (!el) return; if (!node) { el.innerHTML = `

Select a system on the left for live context and actions.

`; return; } const alerts = (state.data?.alerts || []).filter((a) => a.device_id === node.id).slice(0, 5); el.innerHTML = `

${escapeHtml(node.name || "device")}

${escapeHtml(node.model || "—")} · ${escapeHtml(node.service_tag || "no tag")} · id ${node.id}

${node.connected ? "CONNECTED" : "OFFLINE"} ${node.powered_on ? "POWERED ON" : "POWER N/A"} ${node.watts != null ? `${Math.round(node.watts)} W` : ""} ${node.is_idrac ? `iDRAC` : ""} ${node.is_server ? `SERVER` : ""}
Service Tag${escapeHtml(node.service_tag || "—")}
IP${escapeHtml(node.ip || "—")}
Subnet${escapeHtml(node.subnet || "—")}
Status${escapeHtml(node.status || "—")}
Avg / peak${node.avg_watts != null ? Math.round(node.avg_watts) + " W" : "—"} / ${node.peak_watts != null ? Math.round(node.peak_watts) + " W" : "—"}
Last status${escapeHtml(node.last_status_time || "—")}
${node.idrac_url ? `iDRAC Web` : ""}

Related alerts

${ alerts.length ? alerts .map( (a) => `
${escapeHtml(a.severity)} · ${escapeHtml((a.message || "").slice(0, 120))}
` ) .join("") : `

No alerts for this node in the current feed.

` }
`; } function renderKpiPopup() { const key = kpiPopup.key; if (!key) return; const meta = KPI_META[key] || { title: key, blurb: "" }; const raw = devicesForKpi(key); const list = sortKpiDevices(filterKpiDevices(raw)); $("#kpi-title").textContent = meta.title; $("#kpi-sub").textContent = `${meta.blurb} · ${list.length} shown of ${raw.length}`; $("#kpi-summary").innerHTML = kpiSummaryHtml(key, raw); const listEl = $("#kpi-list"); if (!list.length) { listEl.innerHTML = `

No systems match this KPI / search.

`; renderKpiDetail(null); return; } if (kpiPopup.selectedId == null || !list.some((d) => d.id === kpiPopup.selectedId)) { kpiPopup.selectedId = list[0].id; } listEl.innerHTML = list .slice(0, 200) .map((d) => { const bits = [ d.ip || "no IP", d.connected ? "up" : "down", d.watts != null ? Math.round(d.watts) + "W" : null, d.subnet, ] .filter(Boolean) .join(" · "); return ``; }) .join(""); const selected = list.find((d) => d.id === kpiPopup.selectedId) || null; renderKpiDetail(selected); } function openKpiPopup(key) { kpiPopup.key = key; kpiPopup.selectedId = null; kpiPopup.q = ""; const search = $("#kpi-search"); if (search) search.value = ""; const modal = $("#kpi-modal"); const scrim = $("#scrim"); modal?.classList.remove("hidden"); modal?.setAttribute("aria-hidden", "false"); scrim?.classList.add("open"); if (scrim) scrim.dataset.mode = "kpi"; renderKpiPopup(); } function closeKpiPopup() { const modal = $("#kpi-modal"); modal?.classList.add("hidden"); modal?.setAttribute("aria-hidden", "true"); const scrim = $("#scrim"); if (scrim?.dataset.mode === "kpi") { scrim.classList.remove("open"); delete scrim.dataset.mode; } } function applyKpiAsFilter(key) { state.kpiFocus = key; // Align type filters so the map can show the KPI set if (key === "idracs") { state.filters.idrac = true; state.filters.servers = false; } else if (key === "servers" || key === "offline" || key === "powered" || key === "connected" || key === "total") { state.filters.servers = true; // keep idrac optional for total/connected/offline breadth if (key === "total" || key === "connected" || key === "offline") { state.filters.idrac = true; } } if (key === "connected") state.filters.connected = true; if (key === "powered") state.filters.powered = true; if (key === "power" || key === "avgw" || key === "samples") { state.filters.hasPower = true; state.viewMode = "power"; $("#view-modes")?.querySelectorAll(".chip").forEach((c) => c.classList.toggle("active", c.dataset.mode === "power") ); } syncFilterInputs(); refreshLists(); layout(); snapNodes(); fitCameraToNodes(); updateFocusContext(); showToast(`Map filter: ${KPI_META[key]?.title || key}`); } function renderKpis() { const s = state.data?.summary || {}; const items = [ { key: "total", label: "Nodes", v: s.total ?? "—", cls: "" }, { key: "servers", label: "Servers", v: s.servers ?? "—", cls: "" }, { key: "idracs", label: "iDRACs", v: s.idracs ?? "—", cls: "" }, { key: "connected", label: "Connected", v: s.connected ?? "—", cls: "" }, { key: "offline", label: "Offline", v: s.offline ?? "—", cls: "warn" }, { key: "powered", label: "Powered on", v: s.powered_on ?? "—", cls: "" }, { key: "power", label: "Live power", v: s.total_watts != null ? `${Math.round(s.total_watts)} W` : "—", cls: "power", }, { key: "avgw", label: "Avg / node", v: s.avg_node_watts != null ? `${Math.round(s.avg_node_watts)} W` : "—", cls: "power", }, { key: "samples", label: "Power samples", v: s.power_samples ?? "—", cls: "", }, ]; $("#kpi-strip").innerHTML = items .map( (it) => ` ` ) .join(""); } function renderSubnets() { const subs = state.data?.subnets || []; const html = [ ``, ...subs.map( (s) => `` ), ]; $("#subnet-list").innerHTML = html.join(""); } function renderModels() { const models = state.data?.models || []; $("#model-list").innerHTML = [ ``, ...models.map( (m) => `` ), ].join(""); } function renderGroups() { const groups = state.data?.groups || []; $("#group-list").innerHTML = groups.length ? groups .map( (g) => `` ) .join("") : `

No groups loaded

`; } function renderLegend() { const el = $("#legend"); if (state.viewMode === "power") { el.innerHTML = ` <200W 200–450W 450–650W >650W geen reading`; } else if (state.viewMode === "connection") { el.innerHTML = `connectedoffline`; } else if (state.viewMode === "model") { el.innerHTML = [...state.modelColors.entries()] .slice(0, 6) .map(([k, c]) => `${escapeHtml(k.slice(0, 22))}`) .join(""); } else if (state.viewMode === "subnet") { el.innerHTML = [...state.subnetColors.entries()] .slice(0, 6) .map(([k, c]) => `${escapeHtml(k)}`) .join(""); } else { el.innerHTML = ` healthy warning critical offline`; } } function renderTicker() { const d = state.data; if (!d) return; const ome = d.ome || {}; const s = d.summary || {}; const ctx = d.context || {}; const sev = ctx.alert_severity || {}; $("#ticker").innerHTML = ` · · · · · · updated ${d.updated_at ? new Date(d.updated_at * 1000).toLocaleTimeString() : "—"} `; } function escapeHtml(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function escapeAttr(s) { return escapeHtml(s).replace(/'/g, "'"); } function sevClass(sev) { const s = String(sev || "").toLowerCase(); if (s.includes("crit")) return "critical"; if (s.includes("warn")) return "warning"; return "info"; } function relTime(ts) { if (!ts) return "—"; let t; if (typeof ts === "number") t = ts * (ts > 1e12 ? 1 : 1000); else { // OME: "2026-07-16 23:36:33.858" const d = Date.parse(String(ts).replace(" ", "T") + "Z"); t = Number.isFinite(d) ? d : Date.parse(String(ts)); } if (!Number.isFinite(t)) return String(ts).slice(0, 19); const sec = Math.max(0, Math.round((Date.now() - t) / 1000)); if (sec < 60) return sec + "s ago"; if (sec < 3600) return Math.floor(sec / 60) + "m ago"; if (sec < 86400) return Math.floor(sec / 3600) + "h ago"; return Math.floor(sec / 86400) + "d ago"; } function updateFocusContext() { const el = $("#ctx-focus"); if (!el) return; const bits = []; bits.push("layout " + (state.layoutMode || "orbit")); bits.push("color " + (state.viewMode || "status")); bits.push(state.subnet === "all" ? "all networks" : state.subnet); if (state.model) bits.push(state.model); const types = []; if (state.filters.servers) types.push("servers"); if (state.filters.idrac) types.push("iDRACs"); bits.push(types.length ? types.join("+") : "no types"); if (state.filters.connected) bits.push("connected"); if (state.filters.powered) bits.push("powered"); if (state.filters.hasPower) bits.push("has W"); if (state.filters.minWatts > 0) bits.push("≥" + state.filters.minWatts + "W"); if (state.filters.pulseLinks === false) bits.push("pulse off"); if (state.filters.search) bits.push("“" + state.filters.search.slice(0, 18) + "”"); if (state.selectedId) { const n = (state.data?.devices || []).find((d) => d.id === state.selectedId); if (n) bits.push("node " + (n.name || "").slice(0, 22)); } bits.push(state.nodes.length + " visible"); el.textContent = "Focus: " + bits.join(" · "); } function renderContext() { const d = state.data; const ctx = d?.context || {}; const s = d?.summary || {}; const live = $("#ctx-live-text"); if (live) { const age = d?.updated_at ? relTime(d.updated_at) : "—"; live.textContent = (ctx.focus_hint || "Realtime context") + " · pulse #" + (d?.pulse ?? 0) + " · " + age; } const stats = $("#ctx-stats"); if (stats) { const sev = ctx.alert_severity || {}; stats.innerHTML = ` `; } // alerts in left rail const al = $("#alert-list"); if (al) { const alerts = (d?.alerts || []).slice(0, 12); if (!alerts.length) { al.innerHTML = `

No alerts in this snapshot

`; } else { al.innerHTML = alerts .map( (a) => `` ) .join(""); } } // live feed overlay: merge events + alerts const body = $("#ctx-feed-body"); const meta = $("#ctx-feed-meta"); if (meta) { meta.textContent = (ctx.alerts_total != null ? ctx.alerts_total + " alerts total" : "alerts…") + (ctx.events_new ? ` · +${ctx.events_new} delta` : ""); } if (body) { if ($("#ctx-feed")?.classList.contains("collapsed")) { body.innerHTML = ""; updateFocusContext(); return; } const items = []; for (const e of d?.events || []) { items.push({ cls: sevClass(e.severity), title: e.title || e.kind, msg: e.text, sub: (e.kind || "delta") + " · " + relTime(e.ts), deviceId: e.device_id, sort: e.ts || 0, }); } for (const a of (d?.alerts || []).slice(0, 15)) { let sort = 0; const parsed = Date.parse(String(a.time || "").replace(" ", "T") + "Z"); sort = Number.isFinite(parsed) ? parsed / 1000 : 0; items.push({ cls: sevClass(a.severity), title: (a.device || "OME") + " · " + (a.severity || ""), msg: a.message || "", sub: (a.category || "alert") + " · " + relTime(a.time), deviceId: a.device_id, sort, }); } // hottest quick context for (const h of ctx.hottest || []) { items.push({ cls: "info", title: "Hot · " + (h.name || "").slice(0, 24), msg: Math.round(h.watts) + " W · " + (h.subnet || ""), sub: "power ranking", deviceId: h.id, sort: (d?.updated_at || 0) + (h.watts || 0) / 1e6, }); } items.sort((x, y) => (y.sort || 0) - (x.sort || 0)); body.innerHTML = items .slice(0, 24) .map( (it) => `` ) .join(""); } updateFocusContext(); } function focusDeviceId(id) { if (id == null || id === "") return; const nid = Number(id); const n = (state.data?.devices || []).find((d) => d.id === nid || d.id === id); if (!n) return; state.selectedId = n.id; // ensure visible: clear blocking filters lightly showInspector(n); // pan toward node if laid out const laid = state.nodes.find((x) => x.id === n.id); if (laid) { const w = canvas.clientWidth; const h = canvas.clientHeight; state.cam.x = w / 2 - laid.x * state.cam.scale; state.cam.y = h / 2 - laid.y * state.cam.scale; } updateFocusContext(); } function snapNodes() { for (const n of state.nodes) { if (Number.isFinite(n.tx)) { n.x = n.tx; n.y = n.ty; } } } function fitCameraToNodes(opts = {}) { const nodes = state.nodes; const w = canvas.clientWidth; const h = canvas.clientHeight; if (!w || !h) { state.cam = { x: 0, y: 0, scale: 1 }; return; } if (!nodes.length) { state.cam = { x: 0, y: 0, scale: 1 }; return; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; const expand = (x, y, r = 0) => { minX = Math.min(minX, x - r); maxX = Math.max(maxX, x + r); minY = Math.min(minY, y - r); maxY = Math.max(maxY, y + r); }; for (const n of nodes) { const x = Number.isFinite(n.tx) ? n.tx : n.x; const y = Number.isFinite(n.ty) ? n.ty : n.y; expand(x, y, (n.r || 6) + 14); } if (state.hub) expand(state.hub.x, state.hub.y, (state.hub.r || 28) + 16); for (const ch of state.clusterHubs || []) expand(ch.x, ch.y, 56); const bw = Math.max(80, maxX - minX); const bh = Math.max(80, maxY - minY); // Leave room for ctx-bar (top) + legend/feed (bottom) const padX = opts.padX ?? 70; const padY = opts.padY ?? 110; const availW = Math.max(120, w - padX * 2); const availH = Math.max(120, h - padY * 2); const minScale = opts.minScale ?? 0.28; const maxScale = opts.maxScale ?? 1.35; let scale = Math.min(availW / bw, availH / bh); scale = Math.min(maxScale, Math.max(minScale, scale)); const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2; // Bias slightly downward so top ctx-bar does not cover the hub const yBias = 18; state.cam.scale = scale; state.cam.x = w / 2 - cx * scale; state.cam.y = h / 2 - cy * scale + yBias; } function resetView() { // Ensure canvas metrics are current, then rebuild layout in screen space const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = canvas.clientWidth; const h = canvas.clientHeight; if (w && h) { canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } layout(); snapNodes(); // Designed layouts assume identity camera; then nudge to fit with padding state.cam = { x: 0, y: 0, scale: 1 }; fitCameraToNodes(); updateFocusContext(); showToast("View reset · fit to fleet"); } function syncFilterInputs() { const f = state.filters; const set = (id, val) => { const el = document.getElementById(id); if (!el) return; if (el.type === "checkbox") el.checked = !!val; else el.value = val; }; set("f-connected", f.connected); set("f-powered", f.powered); set("f-servers", f.servers); set("f-idrac", f.idrac); set("f-has-power", f.hasPower); set("f-ghost-links", f.ghostLinks); set("f-pulse-links", f.pulseLinks !== false); set("f-min-watts", f.minWatts || 0); const lab = $("#min-w-label"); if (lab) lab.textContent = String(f.minWatts || 0); const search = $("#search"); if (search) search.value = f.search || ""; } function readFiltersFromDom() { const on = (id) => !!document.getElementById(id)?.checked; state.filters.connected = on("f-connected"); state.filters.powered = on("f-powered"); state.filters.servers = on("f-servers"); state.filters.idrac = on("f-idrac"); state.filters.hasPower = on("f-has-power"); state.filters.ghostLinks = on("f-ghost-links"); state.filters.pulseLinks = on("f-pulse-links"); const mw = document.getElementById("f-min-watts"); state.filters.minWatts = mw ? Number(mw.value) || 0 : 0; state.filters.search = $("#search")?.value || ""; } function clearFilters() { state.filters = { connected: false, powered: false, servers: true, idrac: false, hasPower: false, ghostLinks: false, pulseLinks: true, minWatts: 0, search: "", }; state.subnet = "all"; state.model = null; state.kpiFocus = null; state.groupHint = null; syncFilterInputs(); refreshLists(); layout(); snapNodes(); state.cam = { x: 0, y: 0, scale: 1 }; fitCameraToNodes(); updateFocusContext(); showToast("Filters reset · Servers on"); } function showToast(msg, opts = {}) { let el = $("#toast"); if (!el) { el = document.createElement("div"); el.id = "toast"; el.className = "toast"; document.body.appendChild(el); } el.textContent = msg; el.classList.toggle("toast-alert", !!opts.alert); el.classList.add("show"); clearTimeout(showToast._t); const ms = opts.ms != null ? opts.ms : opts.alert ? 8000 : 2200; showToast._t = setTimeout(() => el.classList.remove("show"), ms); } function pushNotifyCard(ev) { const host = $("#notify-stack"); if (!host) return; const card = document.createElement("button"); card.type = "button"; card.className = `notify-card ${ev.severity || "info"}`; card.dataset.deviceId = ev.device_id ?? ""; card.innerHTML = ` ${escapeHtml(ev.kind === "device_removed" ? "Removed" : "New on network")} ${escapeHtml(ev.title || "Fleet change")} ${escapeHtml(ev.text || "")} ${escapeHtml(relTime(ev.ts))}`; card.addEventListener("click", () => { if (ev.device_id != null) focusDeviceId(ev.device_id); card.remove(); }); host.prepend(card); while (host.children.length > 6) host.lastElementChild.remove(); setTimeout(() => card.classList.add("show"), 20); setTimeout(() => { card.classList.remove("show"); setTimeout(() => card.remove(), 400); }, 14000); } function handleFleetNotifications(data) { const notes = data?.context?.notifications || []; const events = data?.events || []; const candidates = [ ...notes, ...events.filter((e) => e.kind === "device_new" || e.kind === "device_removed"), ]; for (const ev of candidates) { const key = `${ev.kind}:${ev.device_id}:${Math.floor(ev.ts || 0)}`; if (state.notifiedEventKeys.has(key)) continue; state.notifiedEventKeys.add(key); if (state.notifiedEventKeys.size > 200) { state.notifiedEventKeys = new Set([...state.notifiedEventKeys].slice(-100)); } const role = ev.role || (String(ev.title || "").toLowerCase().includes("idrac") ? "iDRAC" : "server"); const msg = ev.kind === "device_removed" ? `${role} removed: ${ev.name || ev.title || "device"}` : `New ${role} on network: ${ev.name || ev.title || "device"} · ${ev.ip || ""}`.trim(); showToast(msg, { alert: true, ms: 9000 }); pushNotifyCard(ev); } } function hideTip() { state.hoverId = null; const tip = $("#node-tip"); if (tip) tip.classList.add("hidden"); } function showTip(node, clientX, clientY) { const tip = $("#node-tip"); if (!tip || !node) return; state.hoverId = node.id; const watts = node.watts != null ? `${Math.round(node.watts)} W` : "no power sample"; tip.innerHTML = `

${escapeHtml(node.name || "node")}

${escapeHtml(node.model || "—")} · ST ${escapeHtml(node.service_tag || "—")}
${node.connected ? "connected" : "offline"} · ${node.powered_on ? "powered on" : "power n/a"} · ${escapeHtml(watts)}
${escapeHtml(node.ip || "no IP")} · ${escapeHtml(node.subnet || "")}

Double-click · Quick Connect

`; tip.classList.remove("hidden"); const pad = 14; let x = clientX + pad; let y = clientY + pad; const tw = tip.offsetWidth || 220; const th = tip.offsetHeight || 100; if (x + tw > window.innerWidth - 8) x = clientX - tw - pad; if (y + th > window.innerHeight - 8) y = clientY - th - pad; tip.style.left = `${Math.max(8, x)}px`; tip.style.top = `${Math.max(8, y)}px`; } function relatedAlerts(node) { const id = node?.id; return (state.data?.alerts || []).filter((a) => a.device_id === id).slice(0, 3); } function openConnect(node) { if (!node || node.id == null) return; state.connectNode = node; const modal = $("#connect-modal"); const scrim = $("#scrim"); if (!modal) return; $("#connect-title").textContent = node.name || "Device"; $("#connect-sub").textContent = `${node.model || "—"} · ${node.service_tag || "no tag"} · OME #${node.id}`; $("#connect-badges").innerHTML = ` ${node.connected ? "CONNECTED" : "OFFLINE"} ${node.powered_on ? "POWERED ON" : "POWER N/A"} ${node.watts != null ? `${Math.round(node.watts)} W` : ""} ${node.is_idrac ? `iDRAC` : ""} ${node.is_server ? `SERVER` : ""}`; const alerts = relatedAlerts(node); $("#connect-context").innerHTML = ` `; const ip = node.ip; const idrac = node.idrac_url || (ip ? `https://${ip}` : null); $("#connect-grid").innerHTML = ` ${idrac ? ` iDRAC Web BMC console · ${escapeHtml(ip)} ` : `
iDRAC WebNo management IP
`} ${ip ? `` : ""} ${ip ? `` : ""} `; modal.classList.remove("hidden"); modal.setAttribute("aria-hidden", "false"); scrim?.classList.add("open"); scrim.dataset.mode = "connect"; $("#connect-grid").onclick = async (e) => { const copyBtn = e.target.closest("[data-copy]"); if (copyBtn) { try { await navigator.clipboard.writeText(copyBtn.dataset.copy); showToast("SSH command copied"); } catch (_) { showToast("Copy failed"); } return; } if (e.target.closest("#btn-modal-ssh")) { closeConnect(); window.cockpitSsh?.open(node); return; } if (e.target.closest("#btn-modal-rdp")) { closeConnect(); window.cockpitRdp?.open(node); return; } if (e.target.closest("#btn-modal-ai")) { closeConnect(); openAi(node); return; } if (e.target.closest("#btn-modal-inventory")) { closeConnect(); showInspector(node); setTimeout(() => $("#btn-detail")?.click(), 50); } }; } function closeConnect() { const modal = $("#connect-modal"); modal?.classList.add("hidden"); modal?.setAttribute("aria-hidden", "true"); state.connectNode = null; const scrim = $("#scrim"); if (scrim?.dataset.mode === "connect") { scrim.classList.remove("open"); delete scrim.dataset.mode; } } function softRefreshInspector(node) { if (!node) return; const body = $("#inspector-body"); if (!body || body.classList.contains("hidden")) { showInspector(node); return; } // Same node already open: update live fields only — keep inventory panel. if (state.selectedId !== node.id) { showInspector(node); return; } const head = body.querySelector(".insp-head h2"); if (head && head.textContent !== (node.name || "")) { showInspector(node); return; } const badges = body.querySelector(".badge-row"); if (badges) { badges.innerHTML = ` ${node.connected ? "CONNECTED" : "OFFLINE"} ${node.powered_on ? "POWERED ON" : "POWER OFF / N/A"} ${node.watts != null ? `${Math.round(node.watts)} W` : ""} ${node.is_idrac ? `iDRAC` : ""} ${node.is_server ? `SERVER` : ""}`; } const setKv = (label, value) => { for (const row of body.querySelectorAll(".kv-row")) { const k = row.querySelector(".k"); const v = row.querySelector(".v"); if (k && v && k.textContent === label) { if (label === "Subnet") return; // keep jump button v.textContent = value; } } }; setKv("IP", node.ip || "—"); setKv("Status", node.status || "—"); setKv("Avg W", node.avg_watts != null ? Math.round(node.avg_watts) + " W" : "—"); setKv("Peak W", node.peak_watts != null ? Math.round(node.peak_watts) + " W" : "—"); setKv("Energy", node.energy_kwh != null ? node.energy_kwh + " kWh" : "—"); setKv("Last status", node.last_status_time || "—"); setKv("Inventory", node.last_inventory_time || "—"); updateFocusContext(); } async function loadExpansionPreview(deviceId) { const mount = $("#warranty-compliance-mount"); if (!mount) return; try { const r = await fetch(`/api/devices/${deviceId}/expansion`); if (!r.ok) return; const data = await r.json(); const x = data.expansion || {}; const mem = x.memory || {}; const spec = x.spec || {}; const findings = (x.findings || []).filter((f) => f.severity === "warning").slice(0, 2); const box = document.createElement("div"); box.className = "inv-section"; box.innerHTML = `

Memory / expansion

${mem.total_gb ?? "—"} GB in ${mem.populated ?? 0} DIMMs · ${mem.empty ?? 0} empty slots · Dell max ${spec.max_memory_tb != null ? spec.max_memory_tb + " TB" : "—"}
Disks ${x.disks?.installed ?? "—"} / ${spec.drive_bays_max ?? "—"} bays · PCIe max ${spec.pcie_slots_max ?? "—"}
${findings.map((f) => `
${escapeHtml(f.text)}
`).join("")}
`; mount.appendChild(box); $("#btn-open-hw")?.addEventListener("click", () => { window.cockpitReports?.open?.(); // slight delay for drawer open setTimeout(() => { if (window.cockpitReports) { window.cockpitReports.state = window.cockpitReports.state || {}; } const st = document.querySelector('#reports-tabs [data-tab="hardware"]'); st?.click(); // set device after tab loads setTimeout(() => { const btn = document.querySelector(`[data-hw-id="${deviceId}"]`); btn?.click(); }, 400); }, 200); }); } catch (_) { /* ignore */ } } 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 = `

Warranty

`; if (primary) { html += `
ST ${escapeHtml(primary.service_tag || "—")} · ${escapeHtml(primary.service_level || "—")}
Ends ${escapeHtml(primary.end_date || "—")} · ${primary.days_remaining ?? "—"} days left
`; } else if ((w.items || []).length) { html += (w.items || []) .slice(0, 3) .map( (x) => `
${escapeHtml(x.service_level || "—")} · ends ${escapeHtml(x.end_date || "—")} · ${x.days_remaining ?? "—"}d
` ) .join(""); } else { html += `
No warranty records in OME
`; } html += `

Dell catalog compliance

`; if (c.device) { html += `
${escapeHtml(c.device.compliance_status || c.device.firmware_status || "—")} · baseline ${escapeHtml((c.baseline || {}).name || "—")}
`; } if (comps.length) { html += comps .slice(0, 8) .map( (x) => `
${escapeHtml(x.component || "component")}: ${escapeHtml(x.current_version || "?")} → ${escapeHtml(x.catalog_version || "?")} ${x.dell_uri ? `dell.com` : ""}
` ) .join(""); } else if (c.device) { html += `
No upgrade components in baseline report
`; } else { html += `
Not in active Dell baseline report
`; } html += `
`; mount.innerHTML = html; } catch (err) { mount.innerHTML = `

Warranty/compliance unavailable

`; } } async function showInspector(node) { state.selectedId = node?.id ?? null; const empty = $("#inspector-empty"); const body = $("#inspector-body"); if (!node) { empty.classList.remove("hidden"); body.classList.add("hidden"); return; } empty.classList.add("hidden"); body.classList.remove("hidden"); body.innerHTML = `

${escapeHtml(node.name)}

${escapeHtml(node.model || "—")} · id ${node.id}

${node.connected ? "CONNECTED" : "OFFLINE"} ${node.powered_on ? "POWERED ON" : "POWER OFF / N/A"} ${node.watts != null ? `${Math.round(node.watts)} W` : ""} ${node.is_idrac ? `iDRAC` : ""} ${node.is_server ? `SERVER` : ""}
Service Tag${escapeHtml(node.service_tag || "—")} ${node.service_tag ? `` : ""}
IP${escapeHtml(node.ip || "—")}
Subnet
Model${escapeHtml(node.model || "—")}
Status${escapeHtml(node.status)}
Avg W${node.avg_watts != null ? Math.round(node.avg_watts) + " W" : "—"}
Peak W${node.peak_watts != null ? Math.round(node.peak_watts) + " W" : "—"}
Energy${node.energy_kwh != null ? node.energy_kwh + " kWh" : "—"}
OME id${node.id}
Last status${escapeHtml(node.last_status_time || "—")}
Inventory${escapeHtml(node.last_inventory_time || "—")}

Loading warranty & Dell compliance…

${node.idrac_url ? `iDRAC Web` : ""} ${node.ip ? `` : ""} ${node.ip ? `` : ""}
`; body.querySelector("[data-jump-subnet]")?.addEventListener("click", (e) => { state.subnet = e.currentTarget.dataset.jumpSubnet; refreshLists(); layout(); }); $("#btn-focus-model")?.addEventListener("click", () => { state.model = node.model; 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-rdp-term")?.addEventListener("click", () => window.cockpitRdp?.open(node)); $("#btn-ask-ai")?.addEventListener("click", () => openAi(node)); loadWarrantyCompliance(node.id); loadExpansionPreview(node.id); const cachedInv = state.inventoryCache[node.id]; if (cachedInv) { const mount = $("#inv-mount"); if (mount) mount.innerHTML = cachedInv; } $("#btn-detail")?.addEventListener("click", async () => { const mount = $("#inv-mount"); const deviceId = node.id; state.inventoryLoadingId = deviceId; mount.innerHTML = `

Loading full inventory + application landscape…

`; try { const r = await fetch(`/api/devices/${deviceId}`); const detail = await r.json(); const inv = detail.inventory || {}; const land = detail.landscape || {}; const p = detail.power || {}; const line = (s) => `
${escapeHtml(s)}
`; const section = (title, rows) => { if (!rows || !rows.length) return ""; return `

${escapeHtml(title)} (${rows.length})

${rows.join("")}
`; }; let html = `

Power (live)

Instant ${p.watts ?? "—"} W · avg ${p.avg_watts ?? "—"} · peak ${p.peak_watts ?? "—"}
Energy ${p.energy_kwh ?? "—"} kWh
`; // Application / firmware landscape first const osRows = (land.os || []).map((x) => line(`${x.OsName || x.OperatingSystemName || "OS"} · ${x.OsVersion || ""} · host ${x.Hostname || "—"}`) ); html += section("Operating system", osRows); const soft = (items, title) => { const rows = (items || []).slice(0, 40).map((x) => line( `${x.DeviceDescription || x.SoftwareType || x.Name || "component"} · v${x.Version || "?"} · ${x.Status || ""} · ${x.InstallationDate || ""}`.trim() ) ); return section(title, rows); }; html += soft(land.firmware, "Firmware landscape"); html += soft(land.drivers, "Drivers"); html += soft(land.applications, "Software / apps"); if (!(land.firmware || []).length && (land.software || []).length) { html += soft(land.software, "Software inventory"); } const mgmtRows = (land.management || []).map((x) => { const agents = (x.EndPointAgents || []) .map((a) => a.AgentName || a.ManagementProfile || a.AgentType || "") .filter(Boolean) .join(", "); return line( `${x.DnsName || x.InstrumentationName || "mgmt"} · ${x.IpAddress || ""} · MAC ${x.MacAddress || "—"}` + (agents ? ` · agents: ${agents}` : "") ); }); html += section("Management / agents", mgmtRows); const licRows = (land.licenses || []).slice(0, 20).map((x) => line(`${x.LicenseDescription || x.EntitlementId || x.LicenseType || "license"} · ${x.LicenseStatus || x.Status || ""}`) ); html += section("Licenses", licRows); const sections = [ ["serverProcessors", "CPUs", (x) => `${x.ModelName || x.BrandName || x.Manufacturer || "CPU"} · ${x.CurrentSpeed || ""} MHz · ${x.NumberOfCores || "?"} cores · ${x.Status || ""}`], ["serverMemoryDevices", "Memory", (x) => `${x.Name || "DIMM"} · ${x.Size || "?"} · ${x.Speed || x.CurrentOperatingSpeed || ""} · ${x.Manufacturer || ""} · ${x.SerialNumber || ""}`], ["serverArrayDisks", "Disks", (x) => `${x.ModelNumber || x.SerialNumber || "disk"} · ${x.MediaType || ""} · ${x.Size || x.Capacity || "?"} · ${x.StatusString || x.Status || ""}`], ["serverRaidControllers", "RAID", (x) => `${x.Name || "RAID"} · FW ${x.FirmwareVersion || "?"} · cache ${x.CacheSizeInMb || "?"} MB · ${x.Status || ""}`], ["serverPowerSupplies", "PSUs", (x) => `${x.Name || x.Model || "PSU"} · ${x.OutputWatts || "?"} W · ${x.FirmwareVersion || ""} · ${x.Status || ""}`], ["serverNetworkInterfaces", "NICs", (x) => { const ports = (x.Ports || []).map((p) => p.ProductName || p.PermanentMACAddress || p.MacAddress || "").filter(Boolean).slice(0, 4).join(" | "); return `${x.ProductName || x.VendorName || "NIC"} · ${ports || x.PermanentMACAddress || ""}`; }], ["serverFcCards", "FC HBAs", (x) => `${x.ProductName || x.VendorName || "FC"} · ${x.WWN || x.PortName || ""}`], ["serverDellVideos", "GPUs / video", (x) => `${x.ProductName || x.Description || "video"} · ${x.Manufacturer || ""}`], ["serverDeviceCards", "Device cards", (x) => `${x.ProductName || x.Description || x.SlotName || "card"} · ${x.Manufacturer || ""}`], ["deviceBaseboards", "Baseboard", (x) => `${x.Manufacturer || ""} · ${x.ProductName || x.Model || ""} · SN ${x.SerialNumber || "—"}`], ["deviceFru", "FRU", (x) => `${x.Name || "FRU"} · ${x.Manufacturer || ""} · PN ${x.PartNumber || ""} · SN ${x.SerialNumber || ""}`], ["deviceLocation", "Location", (x) => `${x.Datacenter || ""} ${x.Room || ""} ${x.Aisle || ""} ${x.Rack || ""} U${x.Rackslot || x.RackSlot || ""}`.trim() || JSON.stringify(x).slice(0, 80)], ["subsystemRollupStatus", "Health rollup", (x) => `${x.SubsystemName || x.Type || "sub"} · ${x.Status || ""}`], ["serverStorageEnclosures", "Storage enclosures", (x) => `${x.Name || x.ProductName || "enclosure"} · ${x.Status || ""}`], ["serverVirtualFlashes", "Virtual flash", (x) => `${x.Name || "vFlash"} · ${x.Capacity || x.Size || ""} · ${x.Status || ""}`], ["serverBiosSystemProfileSettings", "BIOS profile", (x) => Object.entries(x).slice(0, 6).map(([k, v]) => `${k}=${v}`).join(" · ")], ["deviceCapabilities", "Capabilities", (x) => `CapabilityType ${x.CapabilityType ?? x.Id ?? "?"}`], ["serverSupportedPowerStates", "Power states", (x) => `PowerState ${x.PowerState ?? x.Id ?? "?"}`], ]; for (const [key, title, fmt] of sections) { const rows = inv[key] || []; if (!rows.length) continue; html += section( title, rows.slice(0, 24).map((row) => line(fmt(row))) ); } // Any remaining inventory keys not already shown const shown = new Set(sections.map((s) => s[0]).concat([ "deviceSoftware", "serverOperatingSystems", "deviceManagement", "deviceLicense", ])); for (const [key, rows] of Object.entries(inv)) { if (shown.has(key) || !Array.isArray(rows) || !rows.length) continue; html += section( key, rows.slice(0, 12).map((row) => line( typeof row === "object" ? Object.entries(row).slice(0, 6).map(([k, v]) => `${k}=${v}`).join(" · ") : String(row) ) ) ); } html = html || `

No inventory available

`; state.inventoryCache[deviceId] = html; if (state.selectedId === deviceId) { const live = $("#inv-mount"); if (live) live.innerHTML = html; } } catch (e) { const errHtml = `

Inventory failed: ${escapeHtml(e.message)}

`; if (state.selectedId === deviceId) { const live = $("#inv-mount"); if (live) live.innerHTML = errHtml; } } finally { if (state.inventoryLoadingId === deviceId) state.inventoryLoadingId = null; } }); } function showHubInspector() { const ome = state.data?.ome || {}; const s = state.data?.summary || {}; state.selectedId = null; $("#inspector-empty").classList.add("hidden"); const body = $("#inspector-body"); body.classList.remove("hidden"); body.innerHTML = `

${escapeHtml(ome.name || "OpenManage Enterprise")}

v${escapeHtml(ome.version || "?")} · build ${escapeHtml(String(ome.build || "?"))}

HUB ${s.total_watts != null ? Math.round(s.total_watts) + " W fleet" : "power…"}
FQDN${escapeHtml(ome.fqdn || "—")}
API${escapeHtml(ome.url || "—")}
Servers${s.servers ?? "—"}
Connected${s.connected ?? "—"}
Power samples${s.power_samples ?? "—"}
Open OME console
`; $("#btn-ai-hub")?.addEventListener("click", () => openAi(null)); } function openAi(node) { const url = state.data?.openwebui_url || "http://atc-portal01.dell-atc.lan:3080"; $("#ai-frame").src = url; $("#ai-drawer").classList.add("open"); $("#ai-drawer").setAttribute("aria-hidden", "false"); const scrim = $("#scrim"); scrim?.classList.add("open"); if (scrim) scrim.dataset.mode = "ai"; if (node) { console.info("Ask AI about", node.name, node.service_tag, node.watts); } } function closeAi() { $("#ai-drawer").classList.remove("open"); $("#ai-drawer").setAttribute("aria-hidden", "true"); const scrim = $("#scrim"); if (scrim?.dataset.mode === "ai" || !scrim?.dataset.mode) { scrim?.classList.remove("open"); if (scrim) delete scrim.dataset.mode; } } function refreshLists() { renderKpis(); renderSubnets(); renderModels(); renderGroups(); renderLegend(); renderTicker(); renderContext(); const ome = state.data?.ome; if (ome) { $("#ome-meta").textContent = `OM Enterprise ${ome.version || "?"} · build ${ome.build || "?"} · live`; } } function applySnapshot(data) { state.data = data; handleFleetNotifications(data); try { window.dispatchEvent(new CustomEvent("cockpit-snapshot", { detail: data })); if (data.gpu) window.dispatchEvent(new CustomEvent("cockpit-gpu", { detail: data.gpu })); } catch (_) {} // warm color maps for (const d of data.devices || []) { colorFor(state.modelColors, d.model || "?"); colorFor(state.subnetColors, d.subnet || "?"); } refreshLists(); layout(); if (state.selectedId) { const n = (data.devices || []).find((x) => x.id === state.selectedId); if (n) softRefreshInspector(n); } } // events $("#layout-modes")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-layout]"); if (!btn) return; state.layoutMode = btn.dataset.layout; $("#layout-modes").querySelectorAll(".viz-card").forEach((c) => c.classList.toggle("active", c === btn)); layout(); snapNodes(); state.cam = { x: 0, y: 0, scale: 1 }; fitCameraToNodes(); updateFocusContext(); }); $("#view-modes").addEventListener("click", (e) => { const btn = e.target.closest("[data-mode]"); if (!btn) return; state.viewMode = btn.dataset.mode; $("#view-modes").querySelectorAll(".chip").forEach((c) => c.classList.toggle("active", c === btn)); renderLegend(); layout(); }); $("#subnet-list").addEventListener("click", (e) => { const btn = e.target.closest("[data-subnet]"); if (!btn) return; state.subnet = btn.dataset.subnet; state.kpiFocus = null; refreshLists(); layout(); }); $("#model-list").addEventListener("click", (e) => { const btn = e.target.closest("[data-model]"); if (!btn) return; state.model = btn.dataset.model || null; refreshLists(); layout(); }); $("#group-list").addEventListener("click", (e) => { const btn = e.target.closest("[data-group]"); if (!btn) return; state.filters.search = btn.dataset.group || ""; $("#search").value = state.filters.search; state.groupHint = btn.dataset.group; showInspector({ id: null, name: `Group: ${btn.dataset.group}`, model: "OME Group", service_tag: "—", connected: true, powered_on: false, watts: null, avg_watts: null, peak_watts: null, energy_kwh: null, ip: null, subnet: "—", status: "group", is_idrac: false, is_server: false, idrac_url: null, }); // note: OME groups aren't device-membership expanded here — search/filter by name hint layout(); }); $("#kpi-strip").addEventListener("click", (e) => { const btn = e.target.closest("[data-kpi]"); if (!btn) return; const key = btn.dataset.kpi; openKpiPopup(key); // Keep strip highlight in sync with open context state.kpiFocus = key; renderKpis(); }); $("#btn-kpi-close")?.addEventListener("click", closeKpiPopup); $("#kpi-search")?.addEventListener("input", (e) => { kpiPopup.q = e.target.value || ""; renderKpiPopup(); }); $("#kpi-sort")?.addEventListener("change", (e) => { kpiPopup.sort = e.target.value || "name"; renderKpiPopup(); }); $("#btn-kpi-apply")?.addEventListener("click", () => { if (kpiPopup.key) applyKpiAsFilter(kpiPopup.key); }); $("#btn-kpi-clear")?.addEventListener("click", () => { state.kpiFocus = null; state.filters.connected = false; state.filters.powered = false; state.filters.hasPower = false; state.filters.servers = true; state.filters.idrac = false; syncFilterInputs(); refreshLists(); layout(); snapNodes(); fitCameraToNodes(); updateFocusContext(); renderKpis(); showToast("Map KPI filter cleared"); }); $("#kpi-list")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-kpi-device]"); if (!btn) return; kpiPopup.selectedId = Number(btn.dataset.kpiDevice); renderKpiPopup(); }); $("#kpi-detail")?.addEventListener("click", (e) => { const act = e.target.closest("[data-kpi-act]"); if (!act) return; const node = (state.data?.devices || []).find((d) => d.id === kpiPopup.selectedId); if (!node) return; const a = act.dataset.kpiAct; if (a === "focus") { closeKpiPopup(); focusDeviceId(node.id); showToast("Focused " + (node.name || "")); } else if (a === "ssh") { window.cockpitSsh?.open(node); } else if (a === "rdp") { window.cockpitRdp?.open(node); } else if (a === "connect") { closeKpiPopup(); openConnect(node); } else if (a === "inventory") { closeKpiPopup(); showInspector(node); setTimeout(() => $("#btn-detail")?.click(), 60); } else if (a === "chat") { closeKpiPopup(); document.getElementById("btn-chat")?.click(); setTimeout(() => { const input = document.getElementById("chat-input"); if (input) { input.value = `Give operational context for ${node.name} (${node.ip || "no IP"}). Status ${node.status}, connected=${node.connected}, watts=${node.watts ?? "n/a"}. Suggest next actions for ATC admins.`; input.focus(); } }, 150); } }); $("#filters").addEventListener("change", (e) => { const t = e.target; readFiltersFromDom(); if (t.id === "f-min-watts") { $("#min-w-label").textContent = String(state.filters.minWatts); } // Pulse-only toggle: no need to relayout if (t.id === "f-pulse-links") { updateFocusContext(); showToast(state.filters.pulseLinks ? "Link pulse on" : "Link pulse off"); return; } layout(); snapNodes(); fitCameraToNodes(); renderTicker(); updateFocusContext(); if (!state.filters.servers && !state.filters.idrac) { showToast("Enable Servers and/or iDRACs to show nodes"); } }); $("#search").addEventListener("input", (e) => { state.filters.search = e.target.value; layout(); renderTicker(); }); $("#ticker").addEventListener("click", (e) => { const btn = e.target.closest("[data-tick]"); if (!btn) return; const k = btn.dataset.tick; if (k === "ome") showHubInspector(); if (k === "power") { state.viewMode = "power"; state.kpiFocus = "power"; state.filters.hasPower = true; $("#f-has-power").checked = true; refreshLists(); layout(); } if (k === "connected") { state.kpiFocus = "connected"; state.filters.connected = true; $("#f-connected").checked = true; refreshLists(); layout(); } if (k === "alerts") { if (window.cockpit?.openTriage) window.cockpit.openTriage("critical"); else updateFocusContext(); } }); canvas.addEventListener("mousedown", (e) => { state.dragging = true; state.last = { x: e.clientX, y: e.clientY }; }); window.addEventListener("mouseup", () => { state.dragging = false; }); canvas.addEventListener("mousemove", (e) => { if (state.dragging) { state.cam.x += e.clientX - state.last.x; state.cam.y += e.clientY - state.last.y; state.last = { x: e.clientX, y: e.clientY }; hideTip(); return; } const rect = canvas.getBoundingClientRect(); const hit = hitTest(e.clientX - rect.left, e.clientY - rect.top); if (hit?.type === "node") showTip(hit.node, e.clientX, e.clientY); else hideTip(); }); canvas.addEventListener("mouseleave", hideTip); canvas.addEventListener("wheel", (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left; const my = e.clientY - rect.top; const before = screenToWorld(mx, my); const factor = e.deltaY > 0 ? 0.9 : 1.1; state.cam.scale = Math.min(3.5, Math.max(0.35, state.cam.scale * factor)); const after = screenToWorld(mx, my); state.cam.x += (after.x - before.x) * state.cam.scale; state.cam.y += (after.y - before.y) * state.cam.scale; }, { passive: false }); canvas.addEventListener("click", (e) => { const rect = canvas.getBoundingClientRect(); const hit = hitTest(e.clientX - rect.left, e.clientY - rect.top); if (!hit) { showInspector(null); hideTip(); return; } if (hit.type === "hub") { showHubInspector(); return; } showInspector(hit.node); }); canvas.addEventListener("dblclick", (e) => { e.preventDefault(); const rect = canvas.getBoundingClientRect(); const hit = hitTest(e.clientX - rect.left, e.clientY - rect.top); if (hit?.type === "node") { showInspector(hit.node); openConnect(hit.node); } else if (hit?.type === "hub") { showHubInspector(); } }); // Shared API for Ops desk / triage popup window.cockpit = { getState: () => state, focusDeviceId, openConnect, showInspector, openAi, relTime, escapeHtml, openTriage: null, openKpiPopup, closeKpiPopup, }; // Realtime context interactivity $("#ctx-stats")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-ctx]"); if (!btn) return; if (window.cockpit.openTriage) window.cockpit.openTriage(btn.dataset.ctx); }); $("#alert-list")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-alert-device]"); if (!btn) return; if (window.cockpit.openTriage) { window.cockpit.openTriage("all", { alertId: btn.dataset.alertId, deviceId: btn.dataset.alertDevice, }); } else { focusDeviceId(btn.dataset.alertDevice); } }); $("#ctx-feed-body")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-feed-device]"); if (!btn) return; if (window.cockpit.openTriage) { window.cockpit.openTriage("all", { deviceId: btn.dataset.feedDevice }); } else { focusDeviceId(btn.dataset.feedDevice); } }); function applyTheme(theme) { const t = theme === "dark" ? "dark" : "light"; document.documentElement.setAttribute("data-theme", t); try { localStorage.setItem("cockpit_theme", t); } catch (_) {} const btn = $("#btn-theme"); if (btn) { btn.dataset.theme = t; btn.title = t === "light" ? "Switch to dark theme" : "Switch to light theme"; btn.setAttribute("aria-label", btn.title); } } applyTheme(localStorage.getItem("cockpit_theme") || "light"); $("#btn-theme")?.addEventListener("click", () => { const cur = document.documentElement.getAttribute("data-theme") || "light"; applyTheme(cur === "light" ? "dark" : "light"); }); $("#btn-reset-view").addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); resetView(); }); $("#btn-clear-filters")?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); clearFilters(); }); $("#btn-ai")?.addEventListener("click", () => openAi(null)); $("#btn-ai-close").addEventListener("click", closeAi); $("#btn-connect-close")?.addEventListener("click", closeConnect); $("#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 === "chat-drawer" || mode === "ops-drawer" || mode === "ai-drawer") { /* ops.js also listens */ } else closeAi(); }); $("#btn-ome-console").addEventListener("click", () => { const url = state.data?.ome?.console_url || state.data?.ome?.url; if (url) window.open(url, "_blank", "noopener"); }); // collapsible left-rail modules $("#left-rail")?.addEventListener("click", (e) => { const btn = e.target.closest(".rail-toggle"); if (!btn) return; const block = btn.closest(".rail-block"); if (!block) return; const open = block.classList.toggle("open"); btn.setAttribute("aria-expanded", open ? "true" : "false"); }); window.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeConnect(); closeAi(); closeKpiPopup(); hideTip(); document.getElementById("btn-triage-close")?.click(); } }); window.addEventListener("resize", resize); window.addEventListener("cockpit-feed-expand", () => { try { renderContext(); } catch (_) {} }); async function boot() { try { const r = await fetch("/api/fleet"); applySnapshot(await r.json()); } catch (e) { console.error(e); } connectWs(); } function connectWs() { const proto = location.protocol === "https:" ? "wss" : "ws"; const ws = new WebSocket(`${proto}://${location.host}/ws/fleet`); ws.onmessage = (ev) => { try { const msg = JSON.parse(ev.data); if (msg.type === "snapshot" && msg.data) applySnapshot(msg.data); if (msg.type === "gpu" && msg.data) { if (state.data) state.data.gpu = msg.data; window.dispatchEvent(new CustomEvent("cockpit-gpu", { detail: msg.data })); } } catch (_) {} }; ws.onclose = () => setTimeout(connectWs, 2500); } // Keep JS filter state in sync with checkbox defaults in HTML readFiltersFromDom(); syncFilterInputs(); resize(); requestAnimationFrame(draw); boot(); // style for inline link buttons in inspector const style = document.createElement("style"); style.textContent = `.linkish{appearance:none;border:none;background:none;color:var(--dell-bright);font:inherit;font-family:var(--mono);padding:0;cursor:pointer;text-align:left}.linkish:hover{text-decoration:underline;color:#fff}`; document.head.appendChild(style); })();