// Copyright Epic Games, Inc. All Rights Reserved. "use strict"; import { ZenPage } from "./page.js" import { Fetcher } from "../util/fetcher.js" import { Friendly } from "../util/friendly.js" import { Table, add_copy_button } from "../util/widgets.js" //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { async main() { this.set_title("orchestrator"); // Provisioner section (hidden until data arrives) this._prov_section = this._collapsible_section("Provisioner"); this._prov_section._parent.inner().style.display = "none"; this._prov_grid = null; this._prov_target_dirty = false; this._prov_commit_timer = null; this._prov_last_target = null; // Agents section const agents_section = this._collapsible_section("Compute Agents"); this._agents_host = agents_section; this._agents_table = null; // Clients section const clients_section = this._collapsible_section("Connected Clients"); this._clients_host = clients_section; this._clients_table = null; // Event history const history_section = this._collapsible_section("Worker Events"); this._history_host = history_section; this._history_table = null; const client_history_section = this._collapsible_section("Client Events"); this._client_history_host = client_history_section; this._client_history_table = null; this._ws_paused = false; try { this._ws_paused = localStorage.getItem("zen-ws-paused") === "true"; } catch (e) {} document.addEventListener("zen-ws-toggle", (e) => { this._ws_paused = e.detail.paused; }); // Initial fetch await this._fetch_all(); // Connect WebSocket for live updates, fall back to polling this._connect_ws(); } async _fetch_all() { try { const [agents, history, clients, client_history, prov] = await Promise.all([ new Fetcher().resource("/orch/agents").json(), new Fetcher().resource("/orch/history").param("limit", "50").json().catch(() => null), new Fetcher().resource("/orch/clients").json().catch(() => null), new Fetcher().resource("/orch/clients/history").param("limit", "50").json().catch(() => null), new Fetcher().resource("/orch/provisioner/status").json().catch(() => null), ]); this._render_agents(agents); if (history) { this._render_history(history.events || []); } if (clients) { this._render_clients(clients.clients || []); } if (client_history) { this._render_client_history(client_history.client_events || []); } this._render_provisioner(prov); } catch (e) { /* service unavailable */ } } _connect_ws() { try { const proto = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${proto}//${location.host}/orch/ws`); ws.onopen = () => { if (this._poll_timer) { clearInterval(this._poll_timer); this._poll_timer = null; } }; ws.onmessage = (ev) => { if (this._ws_paused) { return; } try { const data = JSON.parse(ev.data); this._render_agents(data); if (data.events) { this._render_history(data.events); } if (data.clients) { this._render_clients(data.clients); } if (data.client_events) { this._render_client_history(data.client_events); } this._render_provisioner(data.provisioner); } catch (e) { /* ignore parse errors */ } }; ws.onclose = () => { this._start_polling(); setTimeout(() => this._connect_ws(), 3000); }; ws.onerror = () => { /* onclose will fire */ }; } catch (e) { this._start_polling(); } } _start_polling() { if (!this._poll_timer) { this._poll_timer = setInterval(() => this._fetch_all(), 2000); } } _render_agents(data) { const workers = data.workers || []; if (this._agents_table) { this._agents_table.clear(); } else { this._agents_table = this._agents_host.add_widget( Table, ["hostname", "CPUs", "CPU usage", "memory", "queues", "pending", "running", "completed", "traffic", "last seen"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable|Table.Flag_AlignNumeric, -1 ); } if (workers.length === 0) { return; } let totalCpus = 0, activeCpus = 0, totalWeightedCpu = 0; let totalMemUsed = 0, totalMemTotal = 0; let totalQueues = 0, totalPending = 0, totalRunning = 0, totalCompleted = 0; let totalRecv = 0, totalSent = 0; for (const w of workers) { const cpus = w.cpus || 0; const cpuUsage = w.cpu_usage; const memUsed = w.memory_used || 0; const memTotal = w.memory_total || 0; const queues = w.active_queues || 0; const pending = w.actions_pending || 0; const running = w.actions_running || 0; const completed = w.actions_completed || 0; const recv = w.bytes_received || 0; const sent = w.bytes_sent || 0; const provisioner = w.provisioner || ""; const isProvisioned = provisioner !== ""; totalCpus += cpus; if (w.provisioner_status === "active") { activeCpus += cpus; } if (cpus > 0 && typeof cpuUsage === "number") { totalWeightedCpu += cpuUsage * cpus; } totalMemUsed += memUsed; totalMemTotal += memTotal; totalQueues += queues; totalPending += pending; totalRunning += running; totalCompleted += completed; totalRecv += recv; totalSent += sent; const hostname = w.hostname || ""; const row = this._agents_table.add_row( hostname, cpus > 0 ? Friendly.sep(cpus) : "-", typeof cpuUsage === "number" ? cpuUsage.toFixed(1) + "%" : "-", memTotal > 0 ? Friendly.bytes(memUsed) + " / " + Friendly.bytes(memTotal) : "-", queues > 0 ? Friendly.sep(queues) : "-", Friendly.sep(pending), Friendly.sep(running), Friendly.sep(completed), this._format_traffic(recv, sent), this._format_last_seen(w.dt), ); // Link hostname to worker dashboard if (w.uri) { const cell = row.get_cell(0); cell.inner().textContent = ""; cell.tag("a").text(hostname).attr("href", w.uri + "/dashboard/compute/").attr("target", "_blank"); } // Visual treatment based on provisioner status const provStatus = w.provisioner_status || ""; if (!isProvisioned) { row.inner().style.opacity = "0.45"; } else { const hostCell = row.get_cell(0); const el = hostCell.inner(); const badge = document.createElement("span"); const badgeBase = "display:inline-block;margin-left:6px;padding:1px 5px;border-radius:8px;" + "font-size:9px;font-weight:600;color:#fff;vertical-align:middle;"; if (provStatus === "draining") { badge.textContent = "draining"; badge.style.cssText = badgeBase + "background:var(--theme_warn);"; row.inner().style.opacity = "0.6"; } else if (provStatus === "active") { badge.textContent = provisioner; badge.style.cssText = badgeBase + "background:#8957e5;"; } else { badge.textContent = "deallocated"; badge.style.cssText = badgeBase + "background:var(--theme_fail);"; row.inner().style.opacity = "0.45"; } el.appendChild(badge); } } // Total row — show active / total in CPUs column const cpuLabel = activeCpus < totalCpus ? Friendly.sep(activeCpus) + " / " + Friendly.sep(totalCpus) : Friendly.sep(totalCpus); const total = this._agents_table.add_row( "TOTAL", cpuLabel, "", totalMemTotal > 0 ? Friendly.bytes(totalMemUsed) + " / " + Friendly.bytes(totalMemTotal) : "-", Friendly.sep(totalQueues), Friendly.sep(totalPending), Friendly.sep(totalRunning), Friendly.sep(totalCompleted), this._format_traffic(totalRecv, totalSent), "", ); total.get_cell(0).style("fontWeight", "bold"); } _render_clients(clients) { if (this._clients_table) { this._clients_table.clear(); } else { this._clients_table = this._clients_host.add_widget( Table, ["client ID", "hostname", "address", "last seen"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable, -1 ); } for (const c of clients) { const crow = this._clients_table.add_row( c.id || "", c.hostname || "", c.address || "", this._format_last_seen(c.dt), ); if (c.id) { add_copy_button(crow.get_cell(0).inner(), c.id); } } } _render_history(events) { if (this._history_table) { this._history_table.clear(); } else { this._history_table = this._history_host.add_widget( Table, ["time", "event", "worker", "hostname"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable, -1 ); } for (const evt of events) { this._history_table.add_row( this._format_timestamp(evt.ts), evt.type || "", evt.worker_id || "", evt.hostname || "", ); } } _render_client_history(events) { if (this._client_history_table) { this._client_history_table.clear(); } else { this._client_history_table = this._client_history_host.add_widget( Table, ["time", "event", "client", "hostname"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable, -1 ); } for (const evt of events) { this._client_history_table.add_row( this._format_timestamp(evt.ts), evt.type || "", evt.client_id || "", evt.hostname || "", ); } } _render_provisioner(prov) { const container = this._prov_section._parent.inner(); if (!prov || !prov.name) { container.style.display = "none"; return; } container.style.display = ""; if (!this._prov_grid) { this._prov_grid = this._prov_section.tag().classify("grid").classify("stats-tiles"); this._prov_tiles = {}; // Target cores tile with editable input const target_tile = this._prov_grid.tag().classify("card").classify("stats-tile"); target_tile.tag().classify("card-title").text("Target Cores"); const target_body = target_tile.tag().classify("tile-metrics"); const target_m = target_body.tag().classify("tile-metric").classify("tile-metric-hero"); const input = document.createElement("input"); input.type = "number"; input.min = "0"; input.style.cssText = "width:100px;padding:4px 8px;border:1px solid var(--theme_g2);border-radius:4px;" + "background:var(--theme_g4);color:var(--theme_bright);font-size:20px;font-weight:600;text-align:right;"; target_m.inner().appendChild(input); target_m.tag().classify("metric-label").text("target"); this._prov_tiles.target_input = input; input.addEventListener("focus", () => { this._prov_target_dirty = true; }); input.addEventListener("input", () => { this._prov_target_dirty = true; if (this._prov_commit_timer) { clearTimeout(this._prov_commit_timer); } this._prov_commit_timer = setTimeout(() => this._commit_provisioner_target(), 800); }); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { if (this._prov_commit_timer) { clearTimeout(this._prov_commit_timer); } this._commit_provisioner_target(); input.blur(); } }); input.addEventListener("blur", () => { if (this._prov_commit_timer) { clearTimeout(this._prov_commit_timer); } this._commit_provisioner_target(); }); // Active cores const active_tile = this._prov_grid.tag().classify("card").classify("stats-tile"); active_tile.tag().classify("card-title").text("Active Cores"); const active_body = active_tile.tag().classify("tile-metrics"); this._prov_tiles.active = active_body; // Estimated cores const est_tile = this._prov_grid.tag().classify("card").classify("stats-tile"); est_tile.tag().classify("card-title").text("Estimated Cores"); const est_body = est_tile.tag().classify("tile-metrics"); this._prov_tiles.estimated = est_body; // Agents const agents_tile = this._prov_grid.tag().classify("card").classify("stats-tile"); agents_tile.tag().classify("card-title").text("Agents"); const agents_body = agents_tile.tag().classify("tile-metrics"); this._prov_tiles.agents = agents_body; // Draining const drain_tile = this._prov_grid.tag().classify("card").classify("stats-tile"); drain_tile.tag().classify("card-title").text("Draining"); const drain_body = drain_tile.tag().classify("tile-metrics"); this._prov_tiles.draining = drain_body; } // Update values const input = this._prov_tiles.target_input; if (!this._prov_target_dirty && document.activeElement !== input) { input.value = prov.target_cores; } this._prov_last_target = prov.target_cores; // Re-render metric tiles (clear and recreate content) for (const key of ["active", "estimated", "agents", "draining"]) { this._prov_tiles[key].inner().innerHTML = ""; } this._metric(this._prov_tiles.active, Friendly.sep(prov.active_cores), "cores", true); this._metric(this._prov_tiles.estimated, Friendly.sep(prov.estimated_cores), "cores", true); this._metric(this._prov_tiles.agents, Friendly.sep(prov.agents), "active", true); this._metric(this._prov_tiles.draining, Friendly.sep(prov.agents_draining || 0), "agents", true); } async _commit_provisioner_target() { const input = this._prov_tiles?.target_input; if (!input || this._prov_committing) { return; } const value = parseInt(input.value, 10); if (isNaN(value) || value < 0) { return; } if (value === this._prov_last_target) { this._prov_target_dirty = false; return; } this._prov_committing = true; try { const resp = await fetch("/orch/provisioner/target", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ target_cores: value }), }); if (resp.ok) { this._prov_target_dirty = false; console.log("Target cores set to", value); } else { const text = await resp.text(); console.error("Failed to set target cores: HTTP", resp.status, text); } } catch (e) { console.error("Failed to set target cores:", e); } finally { this._prov_committing = false; } } _metric(parent, value, label, hero = false) { const m = parent.tag().classify("tile-metric"); if (hero) { m.classify("tile-metric-hero"); } m.tag().classify("metric-value").text(value); m.tag().classify("metric-label").text(label); } _format_last_seen(dtMs) { if (dtMs == null) { return "-"; } const seconds = Math.floor(dtMs / 1000); if (seconds < 60) { return seconds + "s ago"; } const minutes = Math.floor(seconds / 60); if (minutes < 60) { return minutes + "m " + (seconds % 60) + "s ago"; } const hours = Math.floor(minutes / 60); return hours + "h " + (minutes % 60) + "m ago"; } _format_traffic(recv, sent) { if (!recv && !sent) { return "-"; } return Friendly.bytes(recv) + " / " + Friendly.bytes(sent); } _format_timestamp(ts) { if (!ts) { return "-"; } let date; if (typeof ts === "number") { // .NET-style ticks: convert to Unix ms const unixMs = (ts - 621355968000000000) / 10000; date = new Date(unixMs); } else { date = new Date(ts); } if (isNaN(date.getTime())) { return "-"; } return date.toLocaleTimeString(); } }