// 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 } from "../util/widgets.js" //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { generate_crumbs() {} async main() { this.set_title("proxy"); // Recording const record_section = this.add_section("Recording"); this._record_host = record_section; this._init_record_controls(record_section); // Summary const summary_section = this.add_section("Summary"); this._summary_grid = summary_section.tag().classify("grid").classify("stats-tiles"); // Mappings const mappings_section = this.add_section("Proxy Mappings"); this._mappings_host = mappings_section; this._mappings_table = null; // Active Connections const connections_section = this.add_section("Active Connections"); this._connections_host = connections_section; this._connections_table = null; try { this._view_mode = localStorage.getItem("zen-proxy-view-mode") || "per-conn"; } catch (e) { this._view_mode = "per-conn"; } this._init_view_tabs(connections_section); await this._update(); this.connect_stats_ws((all_stats) => { const data = all_stats["proxy"]; if (data) { this._render_summary(data); this._render_mappings(data); this._render_connections(data); } }); } async _update() { try { const data = await new Fetcher().resource("/proxy/stats").json(); this._render_summary(data); this._render_mappings(data); this._render_connections(data); } catch (e) { /* service unavailable */ } } _init_record_controls(host) { const container = host.tag().classify("card"); container.inner().style.display = "flex"; container.inner().style.alignItems = "center"; container.inner().style.gap = "12px"; container.inner().style.padding = "12px 16px"; this._record_btn = document.createElement("button"); this._record_btn.className = "history-tab"; this._record_btn.textContent = "Start Recording"; this._record_btn.addEventListener("click", () => this._toggle_recording()); container.inner().appendChild(this._record_btn); this._record_status = document.createElement("span"); this._record_status.style.fontSize = "0.85em"; this._record_status.style.opacity = "0.7"; this._record_status.textContent = "Off"; container.inner().appendChild(this._record_status); this._recording = false; } _update_record_ui(data) { const recording = !!data.recording; this._recording = recording; this._record_btn.textContent = recording ? "Stop Recording" : "Start Recording"; this._record_btn.classList.toggle("active", recording); const dir = data.recordDir || ""; this._record_status.textContent = recording ? "Recording to: " + dir : "Off"; } async _toggle_recording() { try { const endpoint = this._recording ? "/proxy/record/stop" : "/proxy/record/start"; await fetch(endpoint, { method: "POST" }); } catch (e) { /* ignore */ } } _render_summary(data) { this._update_record_ui(data); const grid = this._summary_grid; grid.inner().innerHTML = ""; const mappings = data.mappings || []; let totalActive = 0; let totalPeak = 0; let totalConn = 0; let totalBytes = 0; let totalRequestRate1 = 0; let totalByteRate1 = 0; let totalByteRate5 = 0; for (const m of mappings) { totalActive += (m.activeConnections || 0); totalPeak += (m.peakActiveConnections || 0); totalConn += (m.totalConnections || 0); totalBytes += (m.bytesFromClient || 0) + (m.bytesToClient || 0); totalRequestRate1 += (m.requestRate1 || 0); totalByteRate1 += (m.byteRate1 || 0); totalByteRate5 += (m.byteRate5 || 0); } { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Connections"); const body = tile.tag().classify("tile-metrics"); this._metric(body, Friendly.sep(totalActive), "currently open", true); this._metric(body, Friendly.sep(totalPeak), "peak"); this._metric(body, Friendly.sep(totalConn), "total since startup"); } { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Throughput"); const body = tile.tag().classify("tile-metrics"); this._metric(body, Friendly.sep(totalRequestRate1, 1) + "/s", "req/sec (1m)", true); this._metric(body, Friendly.bytes(totalByteRate1) + "/s", "bandwidth (1m)", true); this._metric(body, Friendly.bytes(totalByteRate5) + "/s", "bandwidth (5m)"); this._metric(body, Friendly.bytes(totalBytes), "total transferred"); } } _render_mappings(data) { const mappings = data.mappings || []; if (this._mappings_table) { this._mappings_table.clear(); } else { this._mappings_table = this._mappings_host.add_widget( Table, ["listen", "target", "active", "peak", "total", "from client", "to client"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_AlignNumeric, -1 ); } for (const m of mappings) { this._mappings_table.add_row( m.listen || "", m.target || "", Friendly.sep(m.activeConnections || 0), Friendly.sep(m.peakActiveConnections || 0), Friendly.sep(m.totalConnections || 0), Friendly.bytes(m.bytesFromClient || 0), Friendly.bytes(m.bytesToClient || 0), ); } } _init_view_tabs(host) { const tabs_el = document.createElement("div"); tabs_el.className = "history-tabs"; tabs_el.style.marginBottom = "8px"; tabs_el.style.width = "fit-content"; host.tag().inner().appendChild(tabs_el); this._view_tabs = {}; const make_tab = (label, mode) => { const btn = document.createElement("button"); btn.className = "history-tab"; btn.textContent = label; btn.addEventListener("click", () => { if (this._view_mode === mode) { return; } this._view_mode = mode; try { localStorage.setItem("zen-proxy-view-mode", mode); } catch (e) {} this._update_active_tab(); if (this._connections_table) { this._connections_table.destroy(); this._connections_table = null; } if (this._last_data) { this._render_connections(this._last_data); } }); tabs_el.appendChild(btn); this._view_tabs[mode] = btn; }; make_tab("Per Connection", "per-conn"); make_tab("Group by IP", "by-ip"); make_tab("Group by Session", "by-session"); this._update_active_tab(); } _update_active_tab() { for (const [mode, btn] of Object.entries(this._view_tabs)) { btn.classList.toggle("active", this._view_mode === mode); } } _render_connections(data) { this._last_data = data; const mappings = data.mappings || []; let connections = []; for (const m of mappings) { for (const c of (m.connections || [])) { connections.push(c); } } if (this._view_mode === "by-ip") { this._render_connections_grouped_ip(connections); } else if (this._view_mode === "by-session") { this._render_connections_grouped_session(connections); } else { this._render_connections_flat(connections); } } _render_connections_flat(connections) { if (this._connections_table) { this._connections_table.clear(); } else { this._connections_table = this._connections_host.add_widget( Table, ["client", "session", "target", "requests", "from client", "to client", "duration"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_AlignNumeric, -1 ); } for (const c of connections) { const row = this._connections_table.add_row( c.client || "", c.sessionId || "", c.target || "", Friendly.sep(c.requests || 0), Friendly.bytes(c.bytesFromClient || 0), Friendly.bytes(c.bytesToClient || 0), Friendly.duration((c.durationMs || 0) / 1000), ); row.get_cell(0).style("textAlign", "left"); row.get_cell(1).style("textAlign", "left"); row.get_cell(2).style("textAlign", "left"); if (c.websocket) { this._append_badge(row.get_cell(0), "WS"); } } } _append_badge(cell, text) { const badge = document.createElement("span"); badge.className = "detail-tag"; badge.style.marginLeft = "6px"; badge.style.background = "color-mix(in srgb, var(--theme_p0) 15%, transparent)"; badge.style.color = "var(--theme_p0)"; badge.textContent = text; cell.inner().appendChild(badge); } _render_connections_grouped_ip(connections) { if (this._connections_table) { this._connections_table.clear(); } else { this._connections_table = this._connections_host.add_widget( Table, ["client ip", "conns", "requests", "from client", "to client", "max duration"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_AlignNumeric, -1 ); } const groups = new Map(); for (const c of connections) { const ip = (c.client || "").replace(/:\d+$/, ""); let group = groups.get(ip); if (!group) { group = { ip: ip, conns: 0, wsConns: 0, requests: 0, bytesFromClient: 0, bytesToClient: 0, maxDurationMs: 0 }; groups.set(ip, group); } group.conns++; if (c.websocket) { group.wsConns++; } group.requests += (c.requests || 0); group.bytesFromClient += (c.bytesFromClient || 0); group.bytesToClient += (c.bytesToClient || 0); group.maxDurationMs = Math.max(group.maxDurationMs, c.durationMs || 0); } for (const g of groups.values()) { const row = this._connections_table.add_row( g.ip, Friendly.sep(g.conns), Friendly.sep(g.requests), Friendly.bytes(g.bytesFromClient), Friendly.bytes(g.bytesToClient), Friendly.duration(g.maxDurationMs / 1000), ); row.get_cell(0).style("textAlign", "left"); if (g.wsConns > 0) { this._append_badge(row.get_cell(0), g.wsConns === 1 ? "WS" : `${g.wsConns} WS`); } } } _render_connections_grouped_session(connections) { if (this._connections_table) { this._connections_table.clear(); } else { this._connections_table = this._connections_host.add_widget( Table, ["session", "conns", "requests", "from client", "to client", "max duration"], Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_AlignNumeric, -1 ); } const groups = new Map(); for (const c of connections) { const sid = c.sessionId || "(none)"; let group = groups.get(sid); if (!group) { group = { sessionId: sid, conns: 0, wsConns: 0, requests: 0, bytesFromClient: 0, bytesToClient: 0, maxDurationMs: 0 }; groups.set(sid, group); } group.conns++; if (c.websocket) { group.wsConns++; } group.requests += (c.requests || 0); group.bytesFromClient += (c.bytesFromClient || 0); group.bytesToClient += (c.bytesToClient || 0); group.maxDurationMs = Math.max(group.maxDurationMs, c.durationMs || 0); } for (const g of groups.values()) { const row = this._connections_table.add_row( g.sessionId, Friendly.sep(g.conns), Friendly.sep(g.requests), Friendly.bytes(g.bytesFromClient), Friendly.bytes(g.bytesToClient), Friendly.duration(g.maxDurationMs / 1000), ); row.get_cell(0).style("textAlign", "left"); if (g.wsConns > 0) { this._append_badge(row.get_cell(0), g.wsConns === 1 ? "WS" : `${g.wsConns} WS`); } } } _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); } }