Add Network Design Studio with multi-U Dell rack designer.

Includes rack catalog/API, Visio-style drag-and-drop, faceplate assets, readable nameplates, and theme/RDP UI updates so multi-U placement stays correct during design.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-07-18 01:18:14 +02:00
parent a3d0ed72bd
commit 91222faffe
17 changed files with 8865 additions and 223 deletions
+77 -1
View File
@@ -757,6 +757,7 @@
<div class="kpi-actions">
<button type="button" class="btn primary" data-kpi-act="focus">Focus on map</button>
<button type="button" class="btn conn-link" data-kpi-act="ssh" ${node.ip ? "" : "disabled"}>SSH terminal</button>
<button type="button" class="btn conn-link" data-kpi-act="rdp" ${node.ip ? "" : "disabled"}>RDP popout</button>
<button type="button" class="btn" data-kpi-act="connect">Quick Connect</button>
<button type="button" class="btn" data-kpi-act="inventory">Inspector + inventory</button>
<button type="button" class="btn" data-kpi-act="chat">Ask Cockpit chat</button>
@@ -1480,7 +1481,13 @@
<span class="ct-t">SSH terminal</span>
<span class="ct-s">Login with your username · ${escapeHtml(ip)}</span>
</button>` : ""}
${ip ? `<button type="button" class="connect-tile conn-link" id="btn-modal-rdp">
<span class="ct-ico">▭</span>
<span class="ct-t">RDP popout</span>
<span class="ct-s">${node.rdp_host ? `OS ${escapeHtml(node.rdp_host)}` : node.os_hostname ? `Resolve ${escapeHtml(node.os_hostname)}` : `Edit host · ${escapeHtml(ip)}`}${node.is_windows ? " · Windows" : ""}</span>
</button>` : ""}
<button type="button" class="connect-tile" id="btn-modal-ai">
<span class="ct-ico">✦</span>
<span class="ct-t">Ask AI</span>
<span class="ct-s">OpenManage AI about this node</span>
@@ -1512,6 +1519,11 @@
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);
@@ -1583,7 +1595,47 @@
updateFocusContext();
}
async function loadWarrantyCompliance(deviceId) {
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 = `<h3>Memory / expansion</h3>
<div class="inv-line">${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" : "—"}</div>
<div class="inv-line">Disks ${x.disks?.installed ?? "—"} / ${spec.drive_bays_max ?? "—"} bays · PCIe max ${spec.pcie_slots_max ?? "—"}</div>
${findings.map((f) => `<div class="inv-line">${escapeHtml(f.text)}</div>`).join("")}
<div class="inv-line"><button type="button" class="btn compact" id="btn-open-hw">Open full DIMM map &amp; Dell guidance</button></div>`;
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 {
@@ -1681,6 +1733,7 @@
<button type="button" class="btn primary" id="btn-quick-connect">Quick Connect · iDRAC / SSH</button>
${node.idrac_url ? `<a class="btn conn-link" href="${escapeAttr(node.idrac_url)}" target="_blank" rel="noopener">iDRAC Web</a>` : ""}
${node.ip ? `<button type="button" class="btn conn-link" id="btn-ssh-term">SSH terminal · ${escapeHtml(node.ip)}</button>` : ""}
${node.ip ? `<button type="button" class="btn conn-link" id="btn-rdp-term">RDP popout · ${escapeHtml(node.rdp_host || node.os_hostname || node.ip)}</button>` : ""}
<button type="button" class="btn" id="btn-detail">Load full inventory + app landscape</button>
<button type="button" class="btn" id="btn-ask-ai">Ask OpenManage AI</button>
<button type="button" class="btn ghost" id="btn-focus-model">Filter this model</button>
@@ -1715,8 +1768,10 @@
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");
@@ -2061,6 +2116,8 @@
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);
@@ -2238,6 +2295,25 @@
}
});
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();