Add VLAN inventory, Present decks, and network fabric mapping.
Seed all ATC/FDE VLANs with live host inventory and editable notes, fix cluster membership on the map, and ship Present/network UI polish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -26,6 +26,10 @@
|
||||
hasPower: false,
|
||||
ghostLinks: false,
|
||||
pulseLinks: true,
|
||||
pulseSpeed: 0.45,
|
||||
pulseLineColor: "#3dffe0",
|
||||
pulseGlowColor: "#00a8e8",
|
||||
pulsePacketColor: "#ffffff",
|
||||
minWatts: 0,
|
||||
search: "",
|
||||
},
|
||||
@@ -98,7 +102,10 @@
|
||||
if (!includeServers && !includeIdrac) return [];
|
||||
|
||||
return (d.devices || []).filter((n) => {
|
||||
if (state.subnet !== "all" && n.subnet !== state.subnet) return false;
|
||||
if (state.subnet !== "all") {
|
||||
const cidrs = n.map_cidrs && n.map_cidrs.length ? n.map_cidrs : [n.subnet];
|
||||
if (!cidrs.includes(state.subnet)) return false;
|
||||
}
|
||||
if (state.model && n.model !== state.model) return false;
|
||||
|
||||
const isServer = !!n.is_server;
|
||||
@@ -345,50 +352,112 @@
|
||||
ctx.arc(hub.x, hub.y, hub.r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#0076ce";
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "#3dffe0";
|
||||
const th = canvasTheme();
|
||||
ctx.strokeStyle = th.hubStroke;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillStyle = th.hubText;
|
||||
ctx.font = "700 11px IBM Plex Sans, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText("OME", hub.x, hub.y);
|
||||
}
|
||||
|
||||
|
||||
function isLightTheme() {
|
||||
return document.documentElement.getAttribute("data-theme") === "light";
|
||||
}
|
||||
function canvasTheme() {
|
||||
if (isLightTheme()) {
|
||||
return {
|
||||
label: "rgba(14, 28, 44, 0.92)",
|
||||
labelSoft: "rgba(30, 50, 72, 0.78)",
|
||||
empty: "rgba(14, 28, 44, 0.88)",
|
||||
emptyHint: "rgba(60, 85, 110, 0.95)",
|
||||
hubStroke: "#0076ce",
|
||||
hubText: "#ffffff",
|
||||
selectRing: "#0076ce",
|
||||
decor: (i) => `rgba(0, 100, 180, ${0.08 + i * 0.025})`,
|
||||
spiral: "rgba(0, 100, 180, 0.16)",
|
||||
helix: (i) => `rgba(0, 100, 180, ${0.1 + i * 0.04})`,
|
||||
ghostLink: (a) => `rgba(70, 90, 110, ${0.22 * a})`,
|
||||
clusterLabel: "rgba(14, 28, 44, 0.85)",
|
||||
laneLabel: "rgba(14, 28, 44, 0.8)",
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: "rgba(232,244,255,0.85)",
|
||||
labelSoft: "rgba(232,244,255,0.7)",
|
||||
empty: "rgba(232,244,255,0.82)",
|
||||
emptyHint: "rgba(122,147,168,0.95)",
|
||||
hubStroke: "#3dffe0",
|
||||
hubText: "#fff",
|
||||
selectRing: "#fff",
|
||||
decor: (i) => `rgba(0,168,232,${0.035 + i * 0.012})`,
|
||||
spiral: "rgba(0,168,232,0.08)",
|
||||
helix: (i) => `rgba(0,168,232,${0.06 + i * 0.03})`,
|
||||
ghostLink: (a) => `rgba(90,106,122,${0.14 * a})`,
|
||||
clusterLabel: "rgba(232,244,255,0.7)",
|
||||
laneLabel: "rgba(232,244,255,0.55)",
|
||||
};
|
||||
}
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const h = String(hex || "#3dffe0").replace("#", "");
|
||||
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h.padEnd(6, "0");
|
||||
const n = parseInt(full.slice(0, 6), 16);
|
||||
if (!Number.isFinite(n)) return { r: 61, g: 255, b: 224 };
|
||||
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
||||
}
|
||||
function rgba(hex, a) {
|
||||
const { r, g, b } = hexToRgb(hex);
|
||||
return `rgba(${r},${g},${b},${a})`;
|
||||
}
|
||||
|
||||
function drawPulseLink(x0, y0, x1, y1, connected, t, id, alphaScale) {
|
||||
if (!connected && !state.filters.ghostLinks) return;
|
||||
const animate = state.filters.pulseLinks !== false;
|
||||
const spd = Math.max(0.1, Number(state.filters.pulseSpeed) || 0.45);
|
||||
let line = state.filters.pulseLineColor || "#3dffe0";
|
||||
let glow = state.filters.pulseGlowColor || "#00a8e8";
|
||||
let packet = state.filters.pulsePacketColor || "#ffffff";
|
||||
if (isLightTheme()) {
|
||||
// Stronger, readable pulse on light stage when using default cyan palette
|
||||
if (!state.filters.pulseLineColor || line === "#3dffe0") line = "#0076ce";
|
||||
if (!state.filters.pulseGlowColor || glow === "#00a8e8") glow = "#00a4e4";
|
||||
if (!state.filters.pulsePacketColor || packet === "#ffffff") packet = "#0b2a44";
|
||||
}
|
||||
const alpha = connected
|
||||
? (animate ? (0.55 + 0.35 * Math.sin(t * 0.05 + id)) : 0.72) * alphaScale
|
||||
? (animate ? (0.55 + 0.35 * Math.sin(t * 0.05 * spd + 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.strokeStyle = rgba(glow, (animate ? 0.28 : 0.16) * 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.strokeStyle = connected ? rgba(line, alpha) : canvasTheme().ghostLink(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 u = ((t * 0.018 * spd + (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.fillStyle = packet;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 5.5, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "rgba(61,255,224,0.55)";
|
||||
ctx.strokeStyle = rgba(line, 0.6);
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -401,7 +470,7 @@
|
||||
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.strokeStyle = canvasTheme().decor(i);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -417,7 +486,7 @@
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.strokeStyle = "rgba(0,168,232,0.08)";
|
||||
ctx.strokeStyle = canvasTheme().spiral;
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -434,8 +503,8 @@
|
||||
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.fillStyle = canvasTheme().clusterLabel;
|
||||
ctx.font = "600 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);
|
||||
@@ -454,8 +523,8 @@
|
||||
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.fillStyle = canvasTheme().laneLabel;
|
||||
ctx.font = "600 9px IBM Plex Mono, monospace";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText((g.label || "").slice(0, 16), 74, g.y - 8);
|
||||
}
|
||||
@@ -473,7 +542,7 @@
|
||||
if (s === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.strokeStyle = `rgba(0,168,232,${0.06 + i * 0.03})`;
|
||||
ctx.strokeStyle = canvasTheme().helix(i);
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -540,7 +609,7 @@
|
||||
if (selected) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(n.x, n.y, n.r + 6, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "#fff";
|
||||
ctx.strokeStyle = canvasTheme().selectRing;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -558,14 +627,21 @@
|
||||
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 th = canvasTheme();
|
||||
// Halo behind text for contrast on busy light canvas
|
||||
const label =
|
||||
state.viewMode === "power" && n.watts != null
|
||||
? `${Math.round(n.watts)}W`
|
||||
: (n.name || "").slice(0, 18);
|
||||
ctx.font = "600 9px IBM Plex Mono, monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "top";
|
||||
if (isLightTheme()) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = "rgba(236, 242, 248, 0.92)";
|
||||
ctx.strokeText(label, n.x, n.y + n.r + 3);
|
||||
}
|
||||
ctx.fillStyle = th.label;
|
||||
ctx.fillText(label, n.x, n.y + n.r + 3);
|
||||
}
|
||||
}
|
||||
@@ -600,7 +676,8 @@
|
||||
ctx.restore();
|
||||
|
||||
if (!state.nodes.length) {
|
||||
ctx.fillStyle = "rgba(232,244,255,0.82)";
|
||||
const th = canvasTheme();
|
||||
ctx.fillStyle = th.empty;
|
||||
ctx.font = "600 15px IBM Plex Sans, sans-serif";
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
@@ -610,11 +687,13 @@
|
||||
: "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.fillStyle = th.emptyHint;
|
||||
ctx.fillText("Adjust Filters or click Reset filters", w / 2, h / 2 + 28);
|
||||
}
|
||||
|
||||
if (state.filters.pulseLinks !== false) state.pulseT += 1;
|
||||
if (state.filters.pulseLinks !== false) {
|
||||
state.pulseT += Math.max(0.1, Number(state.filters.pulseSpeed) || 0.45);
|
||||
}
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
@@ -922,12 +1001,17 @@
|
||||
<span class="t">All networks</span>
|
||||
<span class="s">${state.data?.summary?.total ?? 0} nodes · ${state.data?.summary?.total_watts != null ? Math.round(state.data.summary.total_watts) + " W" : "—"}</span>
|
||||
</button>`,
|
||||
...subs.map(
|
||||
(s) => `<button type="button" class="card${state.subnet === s.cidr ? " active" : ""}" data-subnet="${s.cidr}">
|
||||
<span class="t">${s.cidr}</span>
|
||||
<span class="s">${s.connected}/${s.count} connected${s.watts != null ? ` · ${Math.round(s.watts)} W` : ""}</span>
|
||||
</button>`
|
||||
),
|
||||
...subs.map((s) => {
|
||||
const vlanLabel =
|
||||
s.vlan_id != null
|
||||
? `VLAN ${s.vlan_id}${s.vlan_name ? " · " + s.vlan_name : ""}`
|
||||
: s.cidr;
|
||||
const accent = s.vlan_color ? ` style="--vlan:${escapeAttr(s.vlan_color)}"` : "";
|
||||
return `<button type="button" class="card${state.subnet === s.cidr ? " active" : ""}${s.vlan_id != null ? " has-vlan" : ""}" data-subnet="${escapeAttr(s.cidr)}"${accent}>
|
||||
<span class="t">${escapeHtml(vlanLabel)}</span>
|
||||
<span class="s">${escapeHtml(s.cidr)} · ${s.connected}/${s.count} connected${s.watts != null ? ` · ${Math.round(s.watts)} W` : ""}</span>
|
||||
</button>`;
|
||||
}),
|
||||
];
|
||||
$("#subnet-list").innerHTML = html.join("");
|
||||
}
|
||||
@@ -1301,13 +1385,34 @@
|
||||
set("f-has-power", f.hasPower);
|
||||
set("f-ghost-links", f.ghostLinks);
|
||||
set("f-pulse-links", f.pulseLinks !== false);
|
||||
set("f-pulse-speed", f.pulseSpeed ?? 0.45);
|
||||
set("f-pulse-line-color", f.pulseLineColor || "#3dffe0");
|
||||
set("f-pulse-glow-color", f.pulseGlowColor || "#00a8e8");
|
||||
set("f-pulse-packet-color", f.pulsePacketColor || "#ffffff");
|
||||
set("f-min-watts", f.minWatts || 0);
|
||||
const lab = $("#min-w-label");
|
||||
if (lab) lab.textContent = String(f.minWatts || 0);
|
||||
const pLab = $("#pulse-speed-label");
|
||||
if (pLab) pLab.textContent = `${Number(f.pulseSpeed ?? 0.45).toFixed(2)}×`;
|
||||
const search = $("#search");
|
||||
if (search) search.value = f.search || "";
|
||||
}
|
||||
|
||||
function savePulsePrefs() {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
"cockpit_pulse_prefs",
|
||||
JSON.stringify({
|
||||
pulseSpeed: state.filters.pulseSpeed ?? 0.45,
|
||||
pulseLineColor: state.filters.pulseLineColor || "#3dffe0",
|
||||
pulseGlowColor: state.filters.pulseGlowColor || "#00a8e8",
|
||||
pulsePacketColor: state.filters.pulsePacketColor || "#ffffff",
|
||||
})
|
||||
);
|
||||
localStorage.setItem("cockpit_pulse_speed", String(state.filters.pulseSpeed ?? 0.45));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function readFiltersFromDom() {
|
||||
const on = (id) => !!document.getElementById(id)?.checked;
|
||||
state.filters.connected = on("f-connected");
|
||||
@@ -1317,9 +1422,18 @@
|
||||
state.filters.hasPower = on("f-has-power");
|
||||
state.filters.ghostLinks = on("f-ghost-links");
|
||||
state.filters.pulseLinks = on("f-pulse-links");
|
||||
const ps = document.getElementById("f-pulse-speed");
|
||||
if (ps) state.filters.pulseSpeed = Math.max(0.1, Math.min(2, Number(ps.value) || 0.45));
|
||||
const line = document.getElementById("f-pulse-line-color");
|
||||
const glow = document.getElementById("f-pulse-glow-color");
|
||||
const packet = document.getElementById("f-pulse-packet-color");
|
||||
if (line?.value) state.filters.pulseLineColor = line.value;
|
||||
if (glow?.value) state.filters.pulseGlowColor = glow.value;
|
||||
if (packet?.value) state.filters.pulsePacketColor = packet.value;
|
||||
const mw = document.getElementById("f-min-watts");
|
||||
state.filters.minWatts = mw ? Number(mw.value) || 0 : 0;
|
||||
state.filters.search = $("#search")?.value || "";
|
||||
savePulsePrefs();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
@@ -1331,6 +1445,10 @@
|
||||
hasPower: false,
|
||||
ghostLinks: false,
|
||||
pulseLinks: true,
|
||||
pulseSpeed: 0.45,
|
||||
pulseLineColor: "#3dffe0",
|
||||
pulseGlowColor: "#00a8e8",
|
||||
pulsePacketColor: "#ffffff",
|
||||
minWatts: 0,
|
||||
search: "",
|
||||
};
|
||||
@@ -1690,8 +1808,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setInvOpen(on) {
|
||||
document.body.classList.toggle("inv-open", !!on);
|
||||
}
|
||||
|
||||
function wrapInvHtml(html) {
|
||||
return `<div class="inv-panel">
|
||||
<div class="inv-panel-head">
|
||||
<strong>Full inventory</strong>
|
||||
<span>Theme toggle parked left while reading</span>
|
||||
</div>
|
||||
${html}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function showInspector(node) {
|
||||
state.selectedId = node?.id ?? null;
|
||||
setInvOpen(false);
|
||||
const empty = $("#inspector-empty");
|
||||
const body = $("#inspector-body");
|
||||
if (!node) {
|
||||
@@ -1775,14 +1908,18 @@
|
||||
const cachedInv = state.inventoryCache[node.id];
|
||||
if (cachedInv) {
|
||||
const mount = $("#inv-mount");
|
||||
if (mount) mount.innerHTML = cachedInv;
|
||||
if (mount) {
|
||||
mount.innerHTML = cachedInv.includes("inv-panel") ? cachedInv : wrapInvHtml(cachedInv);
|
||||
setInvOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
$("#btn-detail")?.addEventListener("click", async () => {
|
||||
const mount = $("#inv-mount");
|
||||
const deviceId = node.id;
|
||||
state.inventoryLoadingId = deviceId;
|
||||
mount.innerHTML = `<p class="hint">Loading full inventory + application landscape…</p>`;
|
||||
setInvOpen(true);
|
||||
mount.innerHTML = `<div class="inv-panel"><p class="hint">Loading full inventory + application landscape…</p></div>`;
|
||||
try {
|
||||
const r = await fetch(`/api/devices/${deviceId}`);
|
||||
const detail = await r.json();
|
||||
@@ -1888,17 +2025,19 @@
|
||||
);
|
||||
}
|
||||
|
||||
html = html || `<p class="hint">No inventory available</p>`;
|
||||
html = wrapInvHtml(html || `<p class="hint">No inventory available</p>`);
|
||||
state.inventoryCache[deviceId] = html;
|
||||
if (state.selectedId === deviceId) {
|
||||
const live = $("#inv-mount");
|
||||
if (live) live.innerHTML = html;
|
||||
setInvOpen(true);
|
||||
}
|
||||
} catch (e) {
|
||||
const errHtml = `<p class="hint">Inventory failed: ${escapeHtml(e.message)}</p>`;
|
||||
const errHtml = wrapInvHtml(`<p class="hint">Inventory failed: ${escapeHtml(e.message)}</p>`);
|
||||
if (state.selectedId === deviceId) {
|
||||
const live = $("#inv-mount");
|
||||
if (live) live.innerHTML = errHtml;
|
||||
setInvOpen(true);
|
||||
}
|
||||
} finally {
|
||||
if (state.inventoryLoadingId === deviceId) state.inventoryLoadingId = null;
|
||||
@@ -1910,6 +2049,7 @@
|
||||
const ome = state.data?.ome || {};
|
||||
const s = state.data?.summary || {};
|
||||
state.selectedId = null;
|
||||
setInvOpen(false);
|
||||
$("#inspector-empty").classList.add("hidden");
|
||||
const body = $("#inspector-body");
|
||||
body.classList.remove("hidden");
|
||||
@@ -2144,6 +2284,12 @@
|
||||
if (t.id === "f-min-watts") {
|
||||
$("#min-w-label").textContent = String(state.filters.minWatts);
|
||||
}
|
||||
if (t.id === "f-pulse-speed") {
|
||||
const pLab = $("#pulse-speed-label");
|
||||
if (pLab) pLab.textContent = `${Number(state.filters.pulseSpeed).toFixed(2)}×`;
|
||||
showToast(`Pulse speed ${Number(state.filters.pulseSpeed).toFixed(2)}×`);
|
||||
return;
|
||||
}
|
||||
// Pulse-only toggle: no need to relayout
|
||||
if (t.id === "f-pulse-links") {
|
||||
updateFocusContext();
|
||||
@@ -2160,6 +2306,94 @@
|
||||
}
|
||||
});
|
||||
|
||||
$("#filters").addEventListener("input", (e) => {
|
||||
if (e.target?.id !== "f-pulse-speed") return;
|
||||
readFiltersFromDom();
|
||||
const pLab = $("#pulse-speed-label");
|
||||
if (pLab) pLab.textContent = `${Number(state.filters.pulseSpeed).toFixed(2)}×`;
|
||||
});
|
||||
|
||||
function togglePulsePop(force) {
|
||||
const pop = $("#pulse-pop");
|
||||
const btn = $("#btn-pulse-settings");
|
||||
const block = document.querySelector('.rail-block[data-module="filters"]');
|
||||
if (!pop || !btn) return;
|
||||
const open = force != null ? !!force : pop.hidden;
|
||||
pop.hidden = !open;
|
||||
btn.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
block?.classList.toggle("pulse-open", open);
|
||||
if (open) {
|
||||
const ps = $("#f-pulse-speed");
|
||||
if (ps) ps.value = String(state.filters.pulseSpeed ?? 0.45);
|
||||
const pLab = $("#pulse-speed-label");
|
||||
if (pLab) pLab.textContent = `${Number(state.filters.pulseSpeed ?? 0.45).toFixed(2)}×`;
|
||||
const line = $("#f-pulse-line-color");
|
||||
const glow = $("#f-pulse-glow-color");
|
||||
const packet = $("#f-pulse-packet-color");
|
||||
if (line) line.value = state.filters.pulseLineColor || "#3dffe0";
|
||||
if (glow) glow.value = state.filters.pulseGlowColor || "#00a8e8";
|
||||
if (packet) packet.value = state.filters.pulsePacketColor || "#ffffff";
|
||||
}
|
||||
}
|
||||
|
||||
$("#btn-pulse-settings")?.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
togglePulsePop();
|
||||
});
|
||||
|
||||
const PULSE_THEMES = {
|
||||
cyan: { line: "#3dffe0", glow: "#00a8e8", packet: "#ffffff" },
|
||||
orange: { line: "#ff9a3c", glow: "#ffb040", packet: "#fff4e5" },
|
||||
green: { line: "#3dffa0", glow: "#1b7a4a", packet: "#e8fff4" },
|
||||
purple: { line: "#c4a7ff", glow: "#7b6cf0", packet: "#f4efff" },
|
||||
pink: { line: "#f472b6", glow: "#db2777", packet: "#fff0f7" },
|
||||
gold: { line: "#f0e68c", glow: "#d4af37", packet: "#fffceb" },
|
||||
};
|
||||
|
||||
$("#pulse-pop")?.addEventListener("click", (e) => {
|
||||
const chip = e.target.closest("[data-pulse-speed]");
|
||||
if (chip) {
|
||||
const v = Number(chip.dataset.pulseSpeed);
|
||||
state.filters.pulseSpeed = v;
|
||||
const ps = $("#f-pulse-speed");
|
||||
if (ps) ps.value = String(v);
|
||||
const pLab = $("#pulse-speed-label");
|
||||
if (pLab) pLab.textContent = `${v.toFixed(2)}×`;
|
||||
savePulsePrefs();
|
||||
showToast(`Pulse speed ${v.toFixed(2)}×`);
|
||||
return;
|
||||
}
|
||||
const sw = e.target.closest("[data-pulse-theme]");
|
||||
if (sw) {
|
||||
const theme = PULSE_THEMES[sw.dataset.pulseTheme];
|
||||
if (!theme) return;
|
||||
state.filters.pulseLineColor = theme.line;
|
||||
state.filters.pulseGlowColor = theme.glow;
|
||||
state.filters.pulsePacketColor = theme.packet;
|
||||
const line = $("#f-pulse-line-color");
|
||||
const glow = $("#f-pulse-glow-color");
|
||||
const packet = $("#f-pulse-packet-color");
|
||||
if (line) line.value = theme.line;
|
||||
if (glow) glow.value = theme.glow;
|
||||
if (packet) packet.value = theme.packet;
|
||||
savePulsePrefs();
|
||||
showToast(`Pulse color: ${sw.dataset.pulseTheme}`);
|
||||
}
|
||||
});
|
||||
|
||||
$("#pulse-pop")?.addEventListener("input", (e) => {
|
||||
if (!e.target?.matches?.('input[type="color"]')) return;
|
||||
readFiltersFromDom();
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const pop = $("#pulse-pop");
|
||||
if (!pop || pop.hidden) return;
|
||||
if (e.target.closest("#pulse-pop") || e.target.closest("#btn-pulse-settings")) return;
|
||||
togglePulsePop(false);
|
||||
});
|
||||
|
||||
$("#search").addEventListener("input", (e) => {
|
||||
state.filters.search = e.target.value;
|
||||
layout();
|
||||
@@ -2332,8 +2566,15 @@
|
||||
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 if (
|
||||
mode === "chat-drawer" ||
|
||||
mode === "ops-drawer" ||
|
||||
mode === "ai-drawer" ||
|
||||
mode === "reports-drawer" ||
|
||||
mode === "an-context" ||
|
||||
mode === "network-drawer"
|
||||
) {
|
||||
/* reports.js / ops.js / network.js also listen */
|
||||
} else closeAi();
|
||||
});
|
||||
$("#btn-ome-console").addEventListener("click", () => {
|
||||
@@ -2353,6 +2594,10 @@
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (!$("#an-context-modal")?.classList.contains("hidden")) {
|
||||
window.cockpitReports?.closeAnContext?.();
|
||||
return;
|
||||
}
|
||||
closeConnect();
|
||||
closeAi();
|
||||
closeKpiPopup();
|
||||
@@ -2392,6 +2637,20 @@
|
||||
}
|
||||
|
||||
// Keep JS filter state in sync with checkbox defaults in HTML
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem("cockpit_pulse_prefs") || "null");
|
||||
if (saved && typeof saved === "object") {
|
||||
if (Number.isFinite(Number(saved.pulseSpeed)) && saved.pulseSpeed > 0) {
|
||||
state.filters.pulseSpeed = Number(saved.pulseSpeed);
|
||||
}
|
||||
if (saved.pulseLineColor) state.filters.pulseLineColor = saved.pulseLineColor;
|
||||
if (saved.pulseGlowColor) state.filters.pulseGlowColor = saved.pulseGlowColor;
|
||||
if (saved.pulsePacketColor) state.filters.pulsePacketColor = saved.pulsePacketColor;
|
||||
} else {
|
||||
const savedSpd = Number(localStorage.getItem("cockpit_pulse_speed"));
|
||||
if (Number.isFinite(savedSpd) && savedSpd > 0) state.filters.pulseSpeed = savedSpd;
|
||||
}
|
||||
} catch (_) {}
|
||||
readFiltersFromDom();
|
||||
syncFilterInputs();
|
||||
resize();
|
||||
|
||||
Reference in New Issue
Block a user