import { get } from "./api.js"; import { ago, errorText, h, openPanel, replace } from "./dom.js"; import { findTask, on, onEach, state } from "./state.js"; import { taskStyle } from "./taskcolor.js"; const refreshKinds = new Set(["session.created", "session.state", "task.created", "task.merged", "task.discarded", "project.created", "admin.health"]); export function openStatus() { openPanel("Status", mountStatus); } function mountStatus(root) { let status; let error = ""; let inFlight = false; let again = false; let disposed = false; const render = () => { if (!status) { replace(root, h("div", { class: error ? "empty error-text" : "empty" }, error || "Loading…")); return; } const scrollTop = root.querySelector(".status-view")?.scrollTop ?? 0; const open = new Set([...root.querySelectorAll(".problems details[open]")].map((d) => d.dataset["vendor"] ?? "")); replace(root, h("div", { class: "status-view" }, error && h("p", { class: "error-text" }, error), problemsSection(status, open), turnsSection(status), rateLimitsSection(status.rate_limits), serverSection(status))); root.querySelector(".status-view").scrollTop = scrollTop; }; const refresh = async () => { if (inFlight) { again = true; return; } inFlight = true; try { const next = await get("/api/admin/status"); if (disposed) { return; } status = next; error = ""; } catch (err) { error = `Couldn't load status: ${errorText(err)}`; } inFlight = false; render(); if (again && !disposed) { again = false; void refresh(); } }; const onEvent = (detail) => { const ev = detail; if (refreshKinds.has(ev.kind) || (ev.kind === "agent.vendor" && ev.payload["type"] === "rate_limit_event")) { void refresh(); return; } if (!status) { return; } let changed = false; const proc = status.agent_processes.find((p) => p.session_id === ev.agent_session_id); if (proc && Date.parse(ev.created_at) - Date.parse(proc.last_event_at) >= 1000) { proc.last_event_at = ev.created_at; changed = true; } if (ev.seq > status.last_event_seq) { status.last_event_seq = ev.seq; changed = true; } if (changed) { render(); } }; const offs = [ on("event", onEvent), on("connection", () => { if (state.connected) { void refresh(); } }), onEach(["projects", "tasks"], render), ]; render(); void refresh(); return () => { disposed = true; for (const off of offs) { off(); } }; } function turnsSection(s) { return h("section", { class: "status-section" }, h("h2", {}, "Running turns"), turnList(s.running_turns, s.agent_processes)); } function turnList(turns, procs) { return h("ul", { class: "task-list" }, ...turns.map((t) => { const task = findTask(t.task_id); const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined; return h("li", { style: taskStyle(t.task_id) }, h("a", { href: `#/task/${t.task_id}` }, h("span", { class: "dot running", title: "running" }), h("span", { class: "task-title" }, project && h("span", { class: "project-name" }, `${project.name} ▸ `), h("span", { class: "task-name" }, task?.title ?? t.task_id)), liveness(procs.find((p) => p.session_id === t.session_id)), h("span", { class: "task-state" }, t.vendor))); }), turns.length === 0 && h("li", { class: "empty-row" }, "No turns running.")); } function taskLink(taskId) { if (!taskId) { return "lead"; } const task = findTask(taskId); const project = task ? state.projects.find((p) => p.id === task.project_id) : undefined; return h("a", { href: `#/task/${taskId}`, class: "task-link", style: taskStyle(taskId) }, project ? `${project.name} ▸ ` : "", task?.title ?? taskId); } function since(iso) { const seconds = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000)); if (seconds < 60) { return `${seconds}s ago`; } if (seconds < 3600) { return `${Math.floor(seconds / 60)}m ${seconds % 60}s ago`; } return ago(iso); } function liveness(p) { if (!p) { return false; } if (p.health === "ok") { return h("span", { class: "task-state" }, since(p.last_event_at)); } return h("span", { class: "task-state danger" }, `${p.health} · ${since(p.last_event_at)}`); } const zombieLabel = { running_without_process: "Running with no process", process_without_session: "Process with no session", }; function problemsSection(s, openVendors) { const pe = s.protocol_errors; const vendors = Object.keys(pe.counts).sort(); if (s.zombies.length === 0 && vendors.length === 0) { return false; } return h("section", { class: "status-section" }, h("h2", {}, "Problems"), h("ul", { class: "problems" }, ...s.zombies.map((z) => h("li", {}, h("span", { class: "danger" }, zombieLabel[z.kind]), " ", z.task_id ? taskLink(z.task_id) : "", " ", h("span", { class: "hint" }, z.detail))), ...vendors.map((v) => { const latest = pe.recent.find((e) => e.vendor === v); const n = pe.counts[v] ?? 0; return h("li", {}, h("span", { class: "danger" }, `${v}: ${n} bad stream line${n === 1 ? "" : "s"}`), latest && h("details", { data: { vendor: v }, open: openVendors.has(v) }, h("summary", {}, `latest ${since(latest.at)}: ${latest.reason}`), h("pre", { class: "raw-line" }, latest.raw, new TextEncoder().encode(latest.raw).length < latest.size ? `… (${bytes(latest.size)})` : ""))); }))); } function rateLimitsSection(limits) { const vendors = Object.keys(limits).sort(); return h("section", { class: "status-section" }, h("h2", {}, "Rate limits"), ...vendors.map((v) => rateLimitCard(v, limits[v] ?? {})), vendors.length === 0 && h("p", { class: "hint" }, "No vendor has reported a rate-limit status since byre started.")); } function rateLimitCard(vendor, info) { const { status, rateLimitType, resetsAt, unifiedWindows, ...rest } = info; const statusText = String(status ?? "unknown"); const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn"; const windows = (unifiedWindows ?? {}); return h("div", { class: "card rate-limit" }, h("header", {}, h("span", { class: "vendor" }, vendor), h("span", { class: `rl-status ${tone}` }, statusText), rateLimitType !== undefined && h("span", {}, String(rateLimitType)), typeof resetsAt === "number" && h("span", { class: "resets" }, "resets ", resetText(resetsAt))), Object.keys(windows).length > 0 && h("table", { class: "rl-windows" }, h("tr", {}, h("th", {}, "Window"), h("th", {}, "Used"), h("th", {}, "Resets")), ...Object.entries(windows).map(([name, w]) => h("tr", {}, h("td", {}, name), h("td", { class: utilizationClass(w.utilization) }, w.utilization === undefined ? "—" : `${Math.round(w.utilization * 100)}%`), h("td", {}, w.resetsAt === undefined ? "—" : resetText(w.resetsAt))))), Object.keys(rest).length > 0 && h("dl", { class: "kv" }, ...Object.entries(rest).flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, typeof v === "object" ? JSON.stringify(v) : String(v))]))); } function utilizationClass(u) { if (u === undefined) { return ""; } if (u >= 0.9) { return "danger"; } if (u >= 0.7) { return "warn"; } return ""; } function resetText(unixSeconds) { const at = new Date(unixSeconds * 1000); const time = at.toLocaleString(undefined, { weekday: "short", hour: "numeric", minute: "2-digit" }); const minutes = Math.round((at.getTime() - Date.now()) / 60000); if (minutes <= 0) { return `${time} (passed)`; } const rel = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.floor(minutes / 60)}h ${minutes % 60}m` : `${Math.floor(minutes / 1440)}d ${Math.floor((minutes % 1440) / 60)}h`; return `${time} (in ${rel})`; } function serverSection(s) { const rows = [ ["Started", `${new Date(s.started_at).toLocaleString()} (${ago(s.started_at)})`], ["Agent processes", String(s.live_agents)], ["Projects", String(s.projects)], ["Active tasks", String(s.active_tasks)], ["Live clients", String(s.sse_clients)], ["Database", bytes(s.db_bytes)], ["Last event", `#${s.last_event_seq}`], ]; return h("section", { class: "status-section" }, h("h2", {}, "Server"), h("dl", { class: "kv" }, ...rows.flatMap(([k, v]) => [h("dt", {}, k), h("dd", {}, v)]))); } function bytes(n) { const units = ["B", "KB", "MB", "GB", "TB"]; let i = 0; let v = n; while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } return `${v.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; } //# sourceMappingURL=status.js.map