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:
root
2026-07-18 21:56:30 +02:00
parent 91222faffe
commit 1e87f22006
15 changed files with 7587 additions and 382 deletions
+126 -8
View File
@@ -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);