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:
@@ -43,6 +43,12 @@
|
||||
let activeTicketId = null;
|
||||
let gpuData = null;
|
||||
let chatModelsLoaded = false;
|
||||
let opsUsers = [
|
||||
{ id: "jody", name: "Jody van Dongen", team: "admin", role: "ATC Datacenter Admin" },
|
||||
{ id: "laurens", name: "Laurens Rammers", team: "admin", role: "ATC Datacenter Admin" },
|
||||
{ id: "mo", name: "Mohamed El Kadi", team: "fde", role: "Data Forward Deployed Engineer" },
|
||||
{ id: "bart", name: "Bart Sjerps", team: "fde", role: "Data Forward Deployed Engineer" },
|
||||
];
|
||||
|
||||
function selectedChatModel() {
|
||||
const sel = $("#chat-model");
|
||||
@@ -79,7 +85,44 @@
|
||||
}
|
||||
|
||||
function adminName(id) {
|
||||
return id === "laurens" ? "Laurens Rammers" : "Jody van Dongen";
|
||||
const u = opsUsers.find((x) => x.id === id);
|
||||
return u?.name || id || "—";
|
||||
}
|
||||
|
||||
function userTeam(id) {
|
||||
return opsUsers.find((x) => x.id === id)?.team || "admin";
|
||||
}
|
||||
|
||||
function defaultAssignee(me) {
|
||||
if (me === "jody") return "laurens";
|
||||
if (me === "laurens") return "jody";
|
||||
if (me === "mo") return "bart";
|
||||
if (me === "bart") return "mo";
|
||||
return "jody";
|
||||
}
|
||||
|
||||
function assigneeOptionsHtml(selected) {
|
||||
const admins = opsUsers.filter((u) => u.team === "admin");
|
||||
const fde = opsUsers.filter((u) => u.team === "fde");
|
||||
const opt = (u) =>
|
||||
`<option value="${u.id}" ${u.id === selected ? "selected" : ""}>${escape(u.name)}</option>`;
|
||||
return `<optgroup label="ATC Admins">${admins.map(opt).join("")}</optgroup>
|
||||
<optgroup label="FDE team">${fde.map(opt).join("")}</optgroup>`;
|
||||
}
|
||||
|
||||
async function loadOpsUsers() {
|
||||
try {
|
||||
const data = await api.get("/api/ops/users");
|
||||
if (data.users?.length) opsUsers = data.users;
|
||||
} catch (_) {
|
||||
try {
|
||||
const data = await api.get("/api/admins");
|
||||
if (data.users?.length) opsUsers = data.users;
|
||||
else if (data.admins?.length) opsUsers = data.admins;
|
||||
} catch (_) {
|
||||
/* keep defaults */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openDrawer(id) {
|
||||
@@ -224,20 +267,42 @@
|
||||
function filteredTicketList() {
|
||||
const f = ticketFilterValue();
|
||||
const me = actor();
|
||||
const fdeIds = new Set(opsUsers.filter((u) => u.team === "fde").map((u) => u.id));
|
||||
const adminIds = new Set(opsUsers.filter((u) => u.team === "admin").map((u) => u.id));
|
||||
return (tickets || []).filter((t) => {
|
||||
if (f === "all") return true;
|
||||
if (f === "mine") return t.assignee === me;
|
||||
if (f === "created") return t.created_by === me;
|
||||
if (f === "openish") return ["open", "accepted", "in_progress"].includes(t.status);
|
||||
if (f === "fde") return fdeIds.has(t.assignee) || fdeIds.has(t.created_by);
|
||||
if (f === "admin") return adminIds.has(t.assignee) && adminIds.has(t.created_by);
|
||||
return t.status === f;
|
||||
});
|
||||
}
|
||||
|
||||
function renderOpsStats() {
|
||||
const box = $("#ops-stats");
|
||||
if (!box) return;
|
||||
const all = tickets || [];
|
||||
const active = all.filter((t) => ["open", "accepted", "in_progress"].includes(t.status)).length;
|
||||
const mine = all.filter((t) => t.assignee === actor()).length;
|
||||
const crit = all.filter((t) => t.priority === "critical" || t.priority === "high").length;
|
||||
box.innerHTML = `
|
||||
<div class="ops-stat"><em>${all.length}</em><span>Total</span></div>
|
||||
<div class="ops-stat"><em>${active}</em><span>Active</span></div>
|
||||
<div class="ops-stat"><em>${mine}</em><span>Assigned to me</span></div>
|
||||
<div class="ops-stat"><em>${crit}</em><span>High / critical</span></div>`;
|
||||
}
|
||||
|
||||
async function loadTickets() {
|
||||
const data = await api.get("/api/tickets");
|
||||
tickets = data.tickets || [];
|
||||
if (data.users?.length) opsUsers = data.users;
|
||||
else if (data.admins?.length) opsUsers = data.admins;
|
||||
const list = $("#ticket-list");
|
||||
if (!list) return;
|
||||
filteredTickets = filteredTicketList();
|
||||
renderOpsStats();
|
||||
const count = $("#ops-count");
|
||||
if (count) {
|
||||
count.textContent =
|
||||
@@ -257,9 +322,10 @@
|
||||
const when = tkt.updated_at
|
||||
? new Date(tkt.updated_at * 1000).toLocaleString()
|
||||
: "";
|
||||
const team = userTeam(tkt.assignee);
|
||||
return `<button type="button" class="card${active}" data-ticket="${tkt.id}">
|
||||
<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>
|
||||
<span class="s"><span class="st-${escape(tkt.status)}">${escape(tkt.status)}</span> · ${escape(tkt.priority)} · <span class="ops-team-pill ${team}">${team}</span> · ${escape(adminName(tkt.created_by))} → ${escape(adminName(tkt.assignee))}${tkt.accepted_by ? " · accepted " + escape(adminName(tkt.accepted_by)) : ""}${when ? " · " + escape(when) : ""}</span>
|
||||
</button>`;
|
||||
})
|
||||
.join("");
|
||||
@@ -318,8 +384,7 @@
|
||||
<option value="closed">Closed</option>
|
||||
</select>
|
||||
<select id="ops-assign">
|
||||
<option value="jody">Assign Jody</option>
|
||||
<option value="laurens">Assign Laurens</option>
|
||||
${assigneeOptionsHtml(tkt.assignee || "jody")}
|
||||
</select>
|
||||
<select id="ops-priority">
|
||||
<option value="low">Low</option>
|
||||
@@ -347,7 +412,7 @@
|
||||
.join("")}
|
||||
</div>
|
||||
<form class="ops-reply" id="ops-reply">
|
||||
<textarea id="ops-reply-body" rows="3" placeholder="Write a note to the other admin…"></textarea>
|
||||
<textarea id="ops-reply-body" rows="3" placeholder="Write a note to the team…"></textarea>
|
||||
<button class="btn primary" type="submit">Send note</button>
|
||||
</form>`;
|
||||
|
||||
@@ -401,11 +466,13 @@
|
||||
function openTicketModal(draft = null) {
|
||||
ticketDraft = draft;
|
||||
const me = actor();
|
||||
const other = me === "jody" ? "laurens" : "jody";
|
||||
const other = draft?.assignee || defaultAssignee(me);
|
||||
const assigneeSel = $("#ticket-assignee");
|
||||
if (assigneeSel) assigneeSel.innerHTML = assigneeOptionsHtml(other);
|
||||
$("#ticket-title").value = draft?.title || "";
|
||||
$("#ticket-body").value = draft?.body || "";
|
||||
$("#ticket-priority").value = draft?.priority || "normal";
|
||||
$("#ticket-assignee").value = draft?.assignee || other;
|
||||
$("#ticket-assignee").value = other;
|
||||
const modal = $("#ticket-modal");
|
||||
const scrim = $("#scrim");
|
||||
modal?.classList.remove("hidden");
|
||||
@@ -800,7 +867,7 @@
|
||||
|
||||
async function createTicketFromTriage(it, node) {
|
||||
const me = actor();
|
||||
const other = me === "jody" ? "laurens" : "jody";
|
||||
const other = userTeam(me) === "fde" ? "jody" : defaultAssignee(me);
|
||||
const sev = it.severity || it.kind || "alert";
|
||||
const title = ("[" + sev + "] " + it.title).slice(0, 180);
|
||||
const body = [
|
||||
@@ -833,6 +900,7 @@
|
||||
function wire() {
|
||||
$("#ops-actor")?.addEventListener("change", (e) => {
|
||||
localStorage.setItem("atc_actor", e.target.value);
|
||||
loadTickets();
|
||||
});
|
||||
const saved = localStorage.getItem("atc_actor");
|
||||
if (saved && $("#ops-actor")) $("#ops-actor").value = saved;
|
||||
@@ -877,9 +945,59 @@
|
||||
$("#btn-chat-close")?.addEventListener("click", closeDrawers);
|
||||
$("#btn-ops")?.addEventListener("click", async () => {
|
||||
openDrawer("#ops-drawer");
|
||||
await loadOpsUsers();
|
||||
await loadTickets();
|
||||
$("#ops-empty")?.classList.remove("hidden");
|
||||
$("#ops-thread")?.classList.add("hidden");
|
||||
activeTicketId = null;
|
||||
});
|
||||
$("#btn-ops-close")?.addEventListener("click", closeDrawers);
|
||||
$("#btn-ops-refresh")?.addEventListener("click", () => loadTickets().catch((e) => alert(e.message)));
|
||||
|
||||
$("#ops-quick")?.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-ops-quick]");
|
||||
if (!btn) return;
|
||||
const q = btn.dataset.opsQuick;
|
||||
if (q === "ticket") {
|
||||
openTicketModal();
|
||||
return;
|
||||
}
|
||||
if (q === "to-admin") {
|
||||
openTicketModal({
|
||||
title: "Handoff to ATC admin",
|
||||
body: "Context for Jody / Laurens:\n\n",
|
||||
assignee: "jody",
|
||||
priority: "normal",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (q === "to-fde") {
|
||||
openTicketModal({
|
||||
title: "Data FDE / AI cluster request",
|
||||
body: "Request for Mohamed El Kadi / Bart Sjerps (Data Forward Deployed Engineers · AI workloads):\n\n",
|
||||
assignee: "mo",
|
||||
priority: "normal",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (q === "active") {
|
||||
const sel = $("#ops-filter");
|
||||
if (sel) {
|
||||
sel.value = "openish";
|
||||
localStorage.setItem("cockpit_ops_filter", "openish");
|
||||
}
|
||||
loadTickets();
|
||||
return;
|
||||
}
|
||||
if (q === "chat") {
|
||||
openDrawer("#chat-drawer");
|
||||
loadChatModels();
|
||||
return;
|
||||
}
|
||||
if (q === "reports") {
|
||||
window.cockpitReports?.open?.();
|
||||
}
|
||||
});
|
||||
|
||||
$("#ops-filter")?.addEventListener("change", (e) => {
|
||||
localStorage.setItem("cockpit_ops_filter", e.target.value);
|
||||
|
||||
Reference in New Issue
Block a user