(() => { const $ = (sel) => document.querySelector(sel); const state = window.__cockpitState || null; // Hook into app.js state if exposed; else work via DOM + fetch function ensureApi() { return { async get(url) { const r = await fetch(url); if (!r.ok) throw new Error(await r.text()); return r.json(); }, async post(url, body) { const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(await r.text()); return r.json(); }, async patch(url, body) { const r = await fetch(url, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!r.ok) throw new Error(await r.text()); return r.json(); }, async del(url) { const r = await fetch(url, { method: "DELETE" }); if (!r.ok) throw new Error(await r.text()); return r.json(); }, }; } const api = ensureApi(); const chatHistory = []; let tickets = []; let activeTicketId = null; let gpuData = null; let chatModelsLoaded = false; function selectedChatModel() { const sel = $("#chat-model"); const saved = localStorage.getItem("cockpit_chat_model"); return (sel?.value || saved || "llama3-70b-gptq").trim(); } async function loadChatModels() { const sel = $("#chat-model"); if (!sel) return; try { const data = await api.get("/api/models"); const models = data.models || []; const preferred = localStorage.getItem("cockpit_chat_model") || data.default || "llama3-70b-gptq"; if (models.length) { sel.innerHTML = models .map((m) => { const id = m.id || m.name; const label = m.name && m.name !== id ? `${m.name} (${id})` : id; return ``; }) .join(""); const ids = models.map((m) => m.id); sel.value = ids.includes(preferred) ? preferred : ids[0]; } chatModelsLoaded = true; } catch (e) { console.warn("model list failed", e); } } function actor() { return $("#ops-actor")?.value || localStorage.getItem("atc_actor") || "jody"; } function adminName(id) { return id === "laurens" ? "Laurens Rammers" : "Jody van Dongen"; } function openDrawer(id) { closeDrawers(); const el = $(id); el?.classList.add("open"); el?.setAttribute("aria-hidden", "false"); const scrim = $("#scrim"); scrim?.classList.add("open"); if (scrim) scrim.dataset.mode = id.replace("#", ""); } function closeDrawers() { ["#chat-drawer", "#ops-drawer", "#ai-drawer", "#reports-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)) { scrim.classList.remove("open"); delete scrim.dataset.mode; } } function renderGpu(data) { gpuData = data; const box = $("#gpu-matrix"); const sum = $("#gpu-summary"); if (!box) return; if (!data || data.error) { if (sum) sum.textContent = "GPU telemetry unreachable"; box.innerHTML = ""; return; } const s = data.summary || {}; if (sum) { sum.innerHTML = `${s.avg_util ?? 0}% avg util · ${Math.round(s.total_power_w || 0)} W · mem ${Math.round((s.total_mem_used_mb || 0) / 1024)} / ${Math.round((s.total_mem_mb || 0) / 1024)} GB`; } box.innerHTML = (data.gpus || []) .map((g) => { const busy = g.util_gpu >= 5; const pct = Math.max(0, Math.min(100, g.util_gpu || 0)); return `
GPU ${g.index}${pct.toFixed(0)}%
${Math.round(g.mem_used_mb)} / ${Math.round(g.mem_total_mb)} MB · ${Math.round(g.temp_c)}°C · ${Math.round(g.power_w)} W
${g.name || "V100"}
`; }) .join(""); } function appendChat(role, text) { const body = $("#chat-body"); if (!body) return; const div = document.createElement("div"); div.className = `chat-bubble ${role}`; div.textContent = text; body.appendChild(div); body.scrollTop = body.scrollHeight; } function refreshChatContext() { const d = window.cockpit?.getState?.()?.data || {}; const s = d.summary || {}; const fleet = $("#chat-ctx-fleet"); if (fleet) { fleet.textContent = `${s.connected ?? "—"} connected · ${s.total_watts != null ? Math.round(s.total_watts) + "W" : "—"} · ${(d.alerts || []).filter((a) => a.severity === "Critical").length} crit`; } const focus = $("#chat-ctx-focus"); const sel = window.cockpit?.getState?.()?.selectedId; const node = (d.devices || []).find((x) => x.id === sel); if (focus) focus.textContent = node ? `focus: ${(node.name || "").slice(0, 28)}` : "focus: none"; } async function sendChat(msg) { refreshChatContext(); appendChat("user", msg); chatHistory.push({ role: "user", content: msg }); const model = selectedChatModel(); appendChat("assistant", `Thinking with ${model}…`); const thinking = $("#chat-body")?.lastElementChild; try { const focusId = window.cockpit?.getState?.()?.selectedId || null; const res = await api.post("/api/chat", { message: msg, history: chatHistory.slice(0, -1), focus_device_id: focusId, model, }); if (thinking) thinking.textContent = res.reply || "(empty reply)"; chatHistory.push({ role: "assistant", content: res.reply || "" }); const g = await api.get("/api/gpu"); renderGpu(g); refreshChatContext(); } catch (e) { if (thinking) thinking.textContent = "Chat error: " + e.message; } } function ticketFilterValue() { return $("#ops-filter")?.value || "openish"; } async function loadTickets() { const data = await api.get("/api/tickets"); 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 = `

No tickets in this filter.

`; return; } list.innerHTML = filtered .map((tkt) => { const active = tkt.id === activeTicketId ? " active" : ""; return ``; }) .join(""); } function escape(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } async function openTicket(id) { activeTicketId = id; await loadTickets(); const data = await api.get(`/api/tickets/${id}`); const tkt = data.ticket; const msgs = data.messages || []; $("#ops-empty")?.classList.add("hidden"); const thread = $("#ops-thread"); thread?.classList.remove("hidden"); if (!thread) return; const me = actor(); const canAccept = tkt.assignee === me && !tkt.accepted_by && tkt.status === "open"; thread.innerHTML = `

#${tkt.id} · ${escape(tkt.title)}

Status${escape(tkt.status)}
Priority${escape(tkt.priority)}
Created by${escape(adminName(tkt.created_by))}
Assignee${escape(adminName(tkt.assignee))}
Accepted by${tkt.accepted_by ? escape(adminName(tkt.accepted_by)) : "—"}
Updated${new Date(tkt.updated_at * 1000).toLocaleString()}
${canAccept ? `` : ""}
${msgs .map( (m) => `
${escape(adminName(m.author))} ${new Date(m.created_at * 1000).toLocaleString()}

${escape(m.body)}

` ) .join("")}
`; $("#ops-status").value = tkt.status || "open"; $("#ops-assign").value = tkt.assignee || "jody"; $("#ops-priority").value = tkt.priority || "normal"; $("#ops-save-meta")?.addEventListener("click", async () => { await api.patch(`/api/tickets/${id}`, { status: $("#ops-status").value, assignee: $("#ops-assign").value, priority: $("#ops-priority").value, }); openTicket(id); }); $("#ops-accept")?.addEventListener("click", async () => { await api.patch(`/api/tickets/${id}`, { accepted_by: me, status: "accepted" }); openTicket(id); }); $("#ops-close-ticket")?.addEventListener("click", async () => { await api.patch(`/api/tickets/${id}`, { status: "closed" }); openTicket(id); }); $("#ops-delete-ticket")?.addEventListener("click", async () => { if (!confirm(`Delete ticket #${id}? This cannot be undone.`)) return; await api.del(`/api/tickets/${id}`); activeTicketId = null; $("#ops-thread")?.classList.add("hidden"); $("#ops-empty")?.classList.remove("hidden"); await loadTickets(); }); $("#ops-reply")?.addEventListener("submit", async (e) => { e.preventDefault(); const body = $("#ops-reply-body")?.value?.trim(); if (!body) return; await api.post(`/api/tickets/${id}/messages`, { author: actor(), body }); openTicket(id); }); } let ticketDraft = null; function openTicketModal(draft = null) { ticketDraft = draft; const me = actor(); const other = me === "jody" ? "laurens" : "jody"; $("#ticket-title").value = draft?.title || ""; $("#ticket-body").value = draft?.body || ""; $("#ticket-priority").value = draft?.priority || "normal"; $("#ticket-assignee").value = draft?.assignee || other; const modal = $("#ticket-modal"); const scrim = $("#scrim"); modal?.classList.remove("hidden"); modal?.setAttribute("aria-hidden", "false"); scrim?.classList.add("open"); if (scrim) scrim.dataset.mode = "ticket"; setTimeout(() => $("#ticket-title")?.focus(), 50); } function closeTicketModal() { const modal = $("#ticket-modal"); modal?.classList.add("hidden"); modal?.setAttribute("aria-hidden", "true"); const scrim = $("#scrim"); if (scrim?.dataset.mode === "ticket") { scrim.classList.remove("open"); delete scrim.dataset.mode; } ticketDraft = null; } async function newTicket() { openTicketModal(null); } // Collapsible + lightly draggable live feed function initFeedDrag() { const feed = $("#ctx-feed"); const handle = $("#ctx-feed-drag"); if (!feed || !handle) return; const applyCollapsed = (collapsed) => { feed.classList.toggle("collapsed", collapsed); document.querySelector(".stage")?.classList.toggle("feed-collapsed", collapsed); $("#btn-feed-collapse")?.classList.toggle("hidden", collapsed); $("#btn-feed-expand")?.classList.toggle("hidden", !collapsed); localStorage.setItem("feed_collapsed", collapsed ? "1" : "0"); const body = $("#ctx-feed-body"); if (collapsed) { // park as compact bar near bottom-right; purge DOM to avoid Chromium ghost text feed.style.bottom = "0.55rem"; feed.style.left = ""; feed.style.right = "0.75rem"; feed.style.top = ""; feed.style.width = ""; feed.style.height = ""; if (body) body.innerHTML = ""; } else { const saved = localStorage.getItem("feed_bottom"); feed.style.bottom = saved && !saved.includes("rem") ? saved : "2.4rem"; // ask app to refill feed on next snapshot window.dispatchEvent(new CustomEvent("cockpit-feed-expand")); } }; // migrate: clear broken bottom values that pinned feed mid-screen const savedBottom = localStorage.getItem("feed_bottom"); if (savedBottom) { const n = parseFloat(savedBottom); if (!Number.isFinite(n) || n > 200) localStorage.removeItem("feed_bottom"); } applyCollapsed(localStorage.getItem("feed_collapsed") === "1"); $("#btn-feed-collapse")?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); applyCollapsed(true); }); $("#btn-feed-expand")?.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); applyCollapsed(false); }); // click header when collapsed = expand handle.addEventListener("click", (e) => { if (e.target.closest("button")) return; if (feed.classList.contains("collapsed")) applyCollapsed(false); }); const feedBottomPx = () => { const raw = feed.style.bottom || getComputedStyle(feed).bottom || "38px"; const n = parseFloat(raw); return Number.isFinite(n) ? n : 38; }; const setFeedBottom = (px) => { if (feed.classList.contains("collapsed")) return; const next = Math.max(8, Math.min(160, px)); feed.style.bottom = next + "px"; localStorage.setItem("feed_bottom", feed.style.bottom); }; let dragging = false; let startY = 0; let startBottom = 0; handle.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; if (feed.classList.contains("collapsed")) return; dragging = true; startY = e.clientY; startBottom = feedBottomPx(); e.preventDefault(); }); window.addEventListener("mousemove", (e) => { if (!dragging) return; const dy = e.clientY - startY; setFeedBottom(startBottom - dy); }); window.addEventListener("mouseup", () => { dragging = false; }); } // GPU via fleet snapshot + dedicated WS messages: monkey-patch WebSocket if needed function hookGpuFromFleet() { const orig = window.__applyGpu; // poll GPU every 2s as backup async function tick() { try { renderGpu(await api.get("/api/gpu")); } catch (_) {} } tick(); setInterval(tick, 2500); // listen custom event from app.js if dispatched window.addEventListener("cockpit-gpu", (e) => renderGpu(e.detail)); window.addEventListener("cockpit-snapshot", (e) => { if (e.detail?.gpu) renderGpu(e.detail.gpu); }); } // ===== Realtime triage popup ===== let triageMode = "critical"; let triageSelected = null; let triageFilter = ""; function closeTriage() { const modal = $("#triage-modal"); modal?.classList.add("hidden"); modal?.setAttribute("aria-hidden", "true"); const scrim = $("#scrim"); if (scrim?.dataset.mode === "triage") { scrim.classList.remove("open"); delete scrim.dataset.mode; } } function openTriage(mode = "critical", opts = {}) { triageMode = mode || "critical"; triageFilter = ""; const search = $("#triage-search"); if (search) search.value = ""; const modal = $("#triage-modal"); const scrim = $("#scrim"); if (!modal) return; // close other overlays lightly modal.classList.remove("hidden"); modal.setAttribute("aria-hidden", "false"); scrim?.classList.add("open"); if (scrim) scrim.dataset.mode = "triage"; $("#triage-tabs")?.querySelectorAll("[data-triage]").forEach((c) => { c.classList.toggle("active", c.dataset.triage === triageMode); }); const titles = { critical: "Critical alerts", warning: "Warning alerts", events: "Fleet deltas", hottest: "Live power ranking", all: "Realtime alert feed", }; const elTitle = $("#triage-title"); if (elTitle) elTitle.textContent = titles[triageMode] || "Realtime triage"; renderTriageList(opts); } function cockpitData() { return window.cockpit?.getState?.()?.data || {}; } function deviceById(id) { if (id == null || id === "") return null; return (cockpitData().devices || []).find((d) => String(d.id) === String(id)) || null; } function triageItems(mode) { const d = cockpitData(); const items = []; if (mode === "critical" || mode === "warning" || mode === "all") { const want = mode === "all" ? null : mode === "critical" ? "Critical" : "Warning"; for (const a of d.alerts || []) { if (want && String(a.severity) !== want) continue; items.push({ kind: "alert", id: "alert-" + a.id, alertId: a.id, deviceId: a.device_id, severity: a.severity, title: a.device || "Unknown system", message: a.message || "", meta: [a.ip || "no-ip", a.category, a.subcategory, a.message_id, a.time].filter(Boolean).join(" · "), raw: a, sort: Date.parse(String(a.time || "").replace(" ", "T") + "Z") || 0, }); } } if (mode === "events" || mode === "all") { for (const e of d.events || []) { items.push({ kind: "event", id: "event-" + e.ts + "-" + e.device_id + "-" + e.kind, deviceId: e.device_id, severity: e.severity || "info", title: e.title || e.kind, message: e.text || "", meta: (e.kind || "delta") + " · " + new Date((e.ts || 0) * 1000).toLocaleString(), raw: e, sort: (e.ts || 0) * 1000, }); } } if (mode === "hottest" || mode === "all") { for (const h of (d.context || {}).hottest || []) { items.push({ kind: "power", id: "hot-" + h.id, deviceId: h.id, severity: "info", title: h.name || "node", message: Math.round(h.watts || 0) + " W live sample", meta: (h.subnet || "") + " · power ranking", raw: h, sort: (h.watts || 0) * 1e6, }); } } items.sort((x, y) => (y.sort || 0) - (x.sort || 0)); return items; } function renderTriageList(opts = {}) { const list = $("#triage-list"); if (!list) return; let items = triageItems(triageMode); const q = (triageFilter || "").trim().toLowerCase(); if (q) { items = items.filter((it) => [it.title, it.message, it.meta, it.severity].join(" ").toLowerCase().includes(q) ); } const sub = $("#triage-sub"); if (sub) { const systems = new Set(items.map((i) => i.title)); sub.textContent = items.length + " items · " + systems.size + " systems · inspect & create ATC ticket"; } if (!items.length) { list.innerHTML = '

No matching items in the current live snapshot.

'; $("#triage-detail").innerHTML = '

Nothing selected.

'; triageSelected = null; return; } const groups = new Map(); for (const it of items) { const g = it.title || "Unknown"; if (!groups.has(g)) groups.set(g, []); groups.get(g).push(it); } let html = ""; for (const [sys, rows] of groups) { html += '
' + escape(sys) + " · " + rows.length + "
"; for (const it of rows) { const sev = String(it.severity || "").toLowerCase(); const cls = sev.includes("crit") ? "critical" : sev.includes("warn") ? "warning" : "info"; const active = triageSelected && triageSelected.id === it.id ? " active" : ""; html += '"; } } list.innerHTML = html; let pick = items[0]; if (opts.alertId) pick = items.find((i) => String(i.alertId) === String(opts.alertId)) || pick; else if (opts.deviceId) pick = items.find((i) => String(i.deviceId) === String(opts.deviceId)) || pick; selectTriageItem(pick); } function selectTriageItem(it) { triageSelected = it; $("#triage-list")?.querySelectorAll(".triage-item").forEach((el) => { el.classList.toggle("active", el.dataset.triageId === (it && it.id)); }); const detail = $("#triage-detail"); if (!detail || !it) return; const node = deviceById(it.deviceId); const a = it.kind === "alert" ? it.raw : null; const connected = node && node.connected ? "CONNECTED" : "OFFLINE / unknown"; const watts = node && node.watts != null ? Math.round(node.watts) + " W" : "n/a"; detail.innerHTML = "

" + escape(it.title) + '

' + escape(it.severity || it.kind) + '' + connected + "" + (node && node.watts != null ? '' + watts + "" : "") + '
' + '
System' + escape(it.title) + "
" + '
IP' + escape((node && node.ip) || (a && a.ip) || "—") + "
" + '
Model' + escape((node && node.model) || "—") + "
" + '
Service tag' + escape((node && node.service_tag) || "—") + "
" + '
Subnet' + escape((node && node.subnet) || "—") + "
" + '
Message' + escape(it.message || "—") + "
" + (a && a.action ? '
Recommended' + escape(a.action) + "
" : "") + '
Meta' + escape(it.meta) + "
" + '
' + '' + '' + '" + '' + '' + '
'; $("#triage-ticket")?.addEventListener("click", () => createTicketFromTriage(it, node)); $("#triage-focus")?.addEventListener("click", () => { if (it.deviceId != null) window.cockpit?.focusDeviceId?.(it.deviceId); closeTriage(); }); $("#triage-connect")?.addEventListener("click", () => { if (!node) return; closeTriage(); window.cockpit?.openConnect?.(node); }); $("#triage-ops")?.addEventListener("click", async () => { closeTriage(); openDrawer("#ops-drawer"); await loadTickets(); }); $("#triage-ai")?.addEventListener("click", () => { closeTriage(); openDrawer("#chat-drawer"); sendChat( "Triage this " + (it.severity || it.kind) + " on " + it.title + " (" + ((node && node.ip) || (a && a.ip) || "no-ip") + "): " + it.message + ". Suggest next steps for ATC admins Jody and Laurens." ); }); $("#triage-inspect")?.addEventListener("click", () => { if (node) window.cockpit?.showInspector?.(node); closeTriage(); }); } async function createTicketFromTriage(it, node) { const me = actor(); const other = me === "jody" ? "laurens" : "jody"; const sev = it.severity || it.kind || "alert"; const title = ("[" + sev + "] " + it.title).slice(0, 180); const body = [ "Auto-triage from OpenManage Cockpit", "Reporter: " + adminName(me), "Assignee: " + adminName(other), "System: " + it.title, "IP: " + ((node && node.ip) || (it.raw && it.raw.ip) || "—"), "Model: " + ((node && node.model) || "—"), "Service tag: " + ((node && node.service_tag) || "—"), "Subnet: " + ((node && node.subnet) || "—"), "Severity: " + sev, "Message: " + it.message, it.meta ? "Meta: " + it.meta : "", it.raw && it.raw.action ? "Recommended: " + it.raw.action : "", ] .filter(Boolean) .join("\n"); closeTriage(); openTicketModal({ title, body, assignee: other, priority: String(sev).toLowerCase().includes("crit") ? "critical" : "high", }); } function wire() { $("#ops-actor")?.addEventListener("change", (e) => { localStorage.setItem("atc_actor", e.target.value); }); const saved = localStorage.getItem("atc_actor"); if (saved && $("#ops-actor")) $("#ops-actor").value = saved; if (window.cockpit) window.cockpit.openTriage = openTriage; else window.cockpit = { openTriage }; $("#btn-triage-close")?.addEventListener("click", closeTriage); $("#triage-tabs")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-triage]"); if (!btn) return; openTriage(btn.dataset.triage); }); $("#triage-search")?.addEventListener("input", (e) => { triageFilter = e.target.value || ""; renderTriageList(); }); $("#triage-list")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-triage-id]"); if (!btn) return; const it = triageItems(triageMode).find((x) => x.id === btn.dataset.triageId); if (it) selectTriageItem(it); }); $("#btn-chat")?.addEventListener("click", () => { openDrawer("#chat-drawer"); loadChatModels(); 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." ); } }); $("#chat-model")?.addEventListener("change", (e) => { localStorage.setItem("cockpit_chat_model", e.target.value); chatHistory.length = 0; const body = $("#chat-body"); if (body) body.innerHTML = ""; appendChat("assistant", `Model switched to ${e.target.value}. Context cleared.`); }); $("#btn-chat-close")?.addEventListener("click", closeDrawers); $("#btn-ops")?.addEventListener("click", async () => { openDrawer("#ops-drawer"); await loadTickets(); }); $("#btn-ops-close")?.addEventListener("click", closeDrawers); $("#ops-filter")?.addEventListener("change", () => loadTickets()); $("#btn-ticket-modal-close")?.addEventListener("click", closeTicketModal); $("#btn-ticket-cancel")?.addEventListener("click", closeTicketModal); $("#ticket-form")?.addEventListener("submit", async (e) => { e.preventDefault(); const title = $("#ticket-title")?.value?.trim(); const body = $("#ticket-body")?.value?.trim(); if (!title || !body) return; try { const created = await api.post("/api/tickets", { title, body, created_by: actor(), assignee: $("#ticket-assignee").value, priority: $("#ticket-priority").value, }); closeTicketModal(); openDrawer("#ops-drawer"); await loadTickets(); await openTicket(created.id); } catch (err) { alert("Ticket create failed: " + err.message); } }); $("#chat-quick")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-q]"); if (!btn) return; sendChat(btn.dataset.q); }); window.addEventListener("cockpit-snapshot", () => refreshChatContext()); $("#btn-new-ticket")?.addEventListener("click", () => newTicket().catch((e) => alert(e.message))); $("#ticket-list")?.addEventListener("click", (e) => { const btn = e.target.closest("[data-ticket]"); if (!btn) return; openTicket(Number(btn.dataset.ticket)).catch((err) => alert(err.message)); }); $("#chat-form")?.addEventListener("submit", (e) => { e.preventDefault(); const input = $("#chat-input"); const msg = input?.value?.trim(); if (!msg) return; input.value = ""; sendChat(msg); }); // Extend scrim close 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(); }); initFeedDrag(); hookGpuFromFleet(); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", wire); else wire(); })();