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:
@@ -39,6 +39,7 @@
|
||||
const api = ensureApi();
|
||||
const chatHistory = [];
|
||||
let tickets = [];
|
||||
let filteredTickets = [];
|
||||
let activeTicketId = null;
|
||||
let gpuData = null;
|
||||
let chatModelsLoaded = false;
|
||||
@@ -92,14 +93,14 @@
|
||||
}
|
||||
|
||||
function closeDrawers() {
|
||||
["#chat-drawer", "#ops-drawer", "#ai-drawer", "#reports-drawer"].forEach((id) => {
|
||||
["#chat-drawer", "#ops-drawer", "#ai-drawer", "#reports-drawer", "#network-drawer"].forEach((id) => {
|
||||
const el = $(id);
|
||||
el?.classList.remove("open");
|
||||
el?.setAttribute("aria-hidden", "true");
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent("cockpit-close-drawers"));
|
||||
const scrim = $("#scrim");
|
||||
if (scrim && ["chat-drawer", "ops-drawer", "ai-drawer", "reports-drawer"].includes(scrim.dataset.mode)) {
|
||||
if (scrim && ["chat-drawer", "ops-drawer", "ai-drawer", "reports-drawer", "network-drawer"].includes(scrim.dataset.mode)) {
|
||||
scrim.classList.remove("open");
|
||||
delete scrim.dataset.mode;
|
||||
}
|
||||
@@ -133,14 +134,28 @@
|
||||
.join("");
|
||||
}
|
||||
|
||||
function appendChat(role, text) {
|
||||
function appendChat(role, text, meta) {
|
||||
const body = $("#chat-body");
|
||||
if (!body) return;
|
||||
const div = document.createElement("div");
|
||||
div.className = `chat-bubble ${role}`;
|
||||
div.textContent = text;
|
||||
if (meta?.tools?.length) {
|
||||
const tools = document.createElement("div");
|
||||
tools.className = "chat-tools";
|
||||
tools.title = "Live OME lookups used for this answer";
|
||||
meta.tools.forEach((t) => {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "chat-tool-chip";
|
||||
const q = t.args?.query || t.args?.service_tag || t.args?.device_id || "";
|
||||
chip.textContent = q ? `${t.name}:${q}` : t.name;
|
||||
tools.appendChild(chip);
|
||||
});
|
||||
div.appendChild(tools);
|
||||
}
|
||||
body.appendChild(div);
|
||||
body.scrollTop = body.scrollHeight;
|
||||
return div;
|
||||
}
|
||||
|
||||
function refreshChatContext() {
|
||||
@@ -161,8 +176,7 @@
|
||||
appendChat("user", msg);
|
||||
chatHistory.push({ role: "user", content: msg });
|
||||
const model = selectedChatModel();
|
||||
appendChat("assistant", `Thinking with ${model}…`);
|
||||
const thinking = $("#chat-body")?.lastElementChild;
|
||||
const thinking = appendChat("assistant", `Looking up live OME facts with ${model}…`);
|
||||
try {
|
||||
const focusId = window.cockpit?.getState?.()?.selectedId || null;
|
||||
const res = await api.post("/api/chat", {
|
||||
@@ -171,7 +185,22 @@
|
||||
focus_device_id: focusId,
|
||||
model,
|
||||
});
|
||||
if (thinking) thinking.textContent = res.reply || "(empty reply)";
|
||||
if (thinking) {
|
||||
thinking.textContent = res.reply || "(empty reply)";
|
||||
if (res.tools_used?.length) {
|
||||
const tools = document.createElement("div");
|
||||
tools.className = "chat-tools";
|
||||
tools.title = "Live OME lookups used for this answer";
|
||||
res.tools_used.forEach((t) => {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "chat-tool-chip";
|
||||
const q = t.args?.query || t.args?.service_tag || t.args?.device_id || "";
|
||||
chip.textContent = q ? `${t.name}:${q}` : t.name;
|
||||
tools.appendChild(chip);
|
||||
});
|
||||
thinking.appendChild(tools);
|
||||
}
|
||||
}
|
||||
chatHistory.push({ role: "assistant", content: res.reply || "" });
|
||||
const g = await api.get("/api/gpu");
|
||||
renderGpu(g);
|
||||
@@ -182,7 +211,25 @@
|
||||
}
|
||||
|
||||
function ticketFilterValue() {
|
||||
return $("#ops-filter")?.value || "openish";
|
||||
const saved = localStorage.getItem("cockpit_ops_filter");
|
||||
const sel = $("#ops-filter");
|
||||
if (sel && !sel.dataset.ready) {
|
||||
if (saved) sel.value = saved;
|
||||
else sel.value = "all";
|
||||
sel.dataset.ready = "1";
|
||||
}
|
||||
return sel?.value || saved || "all";
|
||||
}
|
||||
|
||||
function filteredTicketList() {
|
||||
const f = ticketFilterValue();
|
||||
const me = actor();
|
||||
return (tickets || []).filter((t) => {
|
||||
if (f === "all") return true;
|
||||
if (f === "mine") return t.assignee === me;
|
||||
if (f === "openish") return ["open", "accepted", "in_progress"].includes(t.status);
|
||||
return t.status === f;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTickets() {
|
||||
@@ -190,29 +237,46 @@
|
||||
tickets = data.tickets || [];
|
||||
const list = $("#ticket-list");
|
||||
if (!list) return;
|
||||
const f = ticketFilterValue();
|
||||
const me = actor();
|
||||
const filtered = tickets.filter((t) => {
|
||||
if (f === "all") return true;
|
||||
if (f === "mine") return t.assignee === me;
|
||||
if (f === "openish") return ["open", "accepted", "in_progress"].includes(t.status);
|
||||
return t.status === f;
|
||||
});
|
||||
if (!filtered.length) {
|
||||
list.innerHTML = `<p class="hint">No tickets in this filter.</p>`;
|
||||
filteredTickets = filteredTicketList();
|
||||
const count = $("#ops-count");
|
||||
if (count) {
|
||||
count.textContent =
|
||||
filteredTickets.length === tickets.length
|
||||
? `${tickets.length} ticket${tickets.length === 1 ? "" : "s"}`
|
||||
: `${filteredTickets.length} / ${tickets.length} tickets`;
|
||||
}
|
||||
if (!filteredTickets.length) {
|
||||
list.innerHTML = tickets.length
|
||||
? `<p class="hint">No tickets in this filter. Switch to <strong>All history</strong>.</p>`
|
||||
: `<p class="hint">No tickets yet. Create one to start the ops history.</p>`;
|
||||
return;
|
||||
}
|
||||
list.innerHTML = filtered
|
||||
list.innerHTML = filteredTickets
|
||||
.map((tkt) => {
|
||||
const active = tkt.id === activeTicketId ? " active" : "";
|
||||
const when = tkt.updated_at
|
||||
? new Date(tkt.updated_at * 1000).toLocaleString()
|
||||
: "";
|
||||
return `<button type="button" class="card${active}" data-ticket="${tkt.id}">
|
||||
<span class="t">${escape(tkt.title)}</span>
|
||||
<span class="s"><span class="st-${escape(tkt.status)}">${escape(tkt.status)}</span> · ${escape(tkt.priority)} · ${escape(adminName(tkt.created_by))} → ${escape(adminName(tkt.assignee))}${tkt.accepted_by ? " · accepted " + escape(adminName(tkt.accepted_by)) : ""}</span>
|
||||
<span class="t">#${tkt.id} · ${escape(tkt.title)}</span>
|
||||
<span class="s"><span class="st-${escape(tkt.status)}">${escape(tkt.status)}</span> · ${escape(tkt.priority)} · ${escape(adminName(tkt.created_by))} → ${escape(adminName(tkt.assignee))}${tkt.accepted_by ? " · accepted " + escape(adminName(tkt.accepted_by)) : ""}${when ? " · " + escape(when) : ""}</span>
|
||||
</button>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function ticketNavHtml(id) {
|
||||
const idx = filteredTickets.findIndex((t) => t.id === id);
|
||||
if (idx < 0) return "";
|
||||
const prev = idx > 0 ? filteredTickets[idx - 1] : null;
|
||||
const next = idx < filteredTickets.length - 1 ? filteredTickets[idx + 1] : null;
|
||||
return `<div class="ops-nav">
|
||||
<button type="button" class="btn" id="ops-prev" ${prev ? "" : "disabled"} title="Previous ticket">← Prev</button>
|
||||
<span class="ops-nav-pos">${idx + 1} / ${filteredTickets.length}</span>
|
||||
<button type="button" class="btn" id="ops-next" ${next ? "" : "disabled"} title="Next ticket">Next →</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function escape(s) {
|
||||
return String(s ?? "")
|
||||
.replace(/&/g, "&")
|
||||
@@ -234,6 +298,7 @@
|
||||
const me = actor();
|
||||
const canAccept = tkt.assignee === me && !tkt.accepted_by && tkt.status === "open";
|
||||
thread.innerHTML = `
|
||||
${ticketNavHtml(id)}
|
||||
<div class="ops-ticket-head">
|
||||
<h3>#${tkt.id} · ${escape(tkt.title)}</h3>
|
||||
<div class="ops-meta-grid">
|
||||
@@ -268,6 +333,10 @@
|
||||
<button type="button" class="btn danger" id="ops-delete-ticket">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ops-body-box">
|
||||
<h4>Original request</h4>
|
||||
<p class="ops-body-text">${escape(tkt.body)}</p>
|
||||
</div>
|
||||
<div class="ops-msgs" id="ops-msgs">
|
||||
${msgs
|
||||
.map(
|
||||
@@ -286,6 +355,14 @@
|
||||
$("#ops-assign").value = tkt.assignee || "jody";
|
||||
$("#ops-priority").value = tkt.priority || "normal";
|
||||
|
||||
const idx = filteredTickets.findIndex((t) => t.id === id);
|
||||
$("#ops-prev")?.addEventListener("click", () => {
|
||||
if (idx > 0) openTicket(filteredTickets[idx - 1].id);
|
||||
});
|
||||
$("#ops-next")?.addEventListener("click", () => {
|
||||
if (idx >= 0 && idx < filteredTickets.length - 1) openTicket(filteredTickets[idx + 1].id);
|
||||
});
|
||||
|
||||
$("#ops-save-meta")?.addEventListener("click", async () => {
|
||||
await api.patch(`/api/tickets/${id}`, {
|
||||
status: $("#ops-status").value,
|
||||
@@ -786,7 +863,7 @@
|
||||
if (!$("#chat-body")?.children.length) {
|
||||
appendChat(
|
||||
"assistant",
|
||||
"Cockpit Copilot online. Pick a model above (same catalog as OpenManage AI), then ask about fleet health, alerts, power, or GPUs."
|
||||
"Cockpit Copilot online. Live V100 GPUs are shown above. Pick a model (Llama or OpenManage AI), then ask about fleet memory, alerts, power, or GPUs."
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -804,7 +881,10 @@
|
||||
});
|
||||
$("#btn-ops-close")?.addEventListener("click", closeDrawers);
|
||||
|
||||
$("#ops-filter")?.addEventListener("change", () => loadTickets());
|
||||
$("#ops-filter")?.addEventListener("change", (e) => {
|
||||
localStorage.setItem("cockpit_ops_filter", e.target.value);
|
||||
loadTickets();
|
||||
});
|
||||
$("#btn-ticket-modal-close")?.addEventListener("click", closeTicketModal);
|
||||
$("#btn-ticket-cancel")?.addEventListener("click", closeTicketModal);
|
||||
$("#ticket-form")?.addEventListener("submit", async (e) => {
|
||||
@@ -855,7 +935,7 @@
|
||||
const scrim = $("#scrim");
|
||||
scrim?.addEventListener("click", () => {
|
||||
const mode = scrim.dataset.mode;
|
||||
if (mode === "chat-drawer" || mode === "ops-drawer" || mode === "ai-drawer" || mode === "reports-drawer") closeDrawers();
|
||||
if (mode === "chat-drawer" || mode === "ops-drawer" || mode === "ai-drawer" || mode === "reports-drawer" || mode === "network-drawer") closeDrawers();
|
||||
});
|
||||
|
||||
initFeedDrag();
|
||||
|
||||
Reference in New Issue
Block a user