`;
$("#ops-status").value = tkt.status || "open";
$("#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,
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 = 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 = 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 += '