This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 09:37:47 +02:00
1 parent bbfa8cbc3f
commit 2ccc598b12
49 files changed
+4771 -639

No files matched your search

+45 -50
View File
@@ -27,8 +27,6 @@ let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtoco
let liveEvents = null, liveConnected = false, liveFallback = false, liveInterval = null, authEpoch = 0, pendingAuthDestination = null;
let lastApiLatencyMs = null, sessionCheckPromise = null;
let adminAuthChannel = null; try { adminAuthChannel = new BroadcastChannel("rustproxy-admin-session"); adminAuthChannel.addEventListener("message", event => { if (event.data === "locked" && authenticated) lock(false); }); } catch { }
// Language content is discovered by build.rs from langs/*.json, never compiled
// into a two-language switch here. Locale metadata also drives Intl and RTL.
let languagePacks = new Map(), defaultLanguage = null;
function activePack() { return languagePacks.get(language) || languagePacks.get(defaultLanguage); }
function locale() { try { return Intl.getCanonicalLocales(activePack()?.meta.locale || "und")[0]; } catch { return "und"; } }
@@ -59,15 +57,13 @@ function translate() {
window.rustProxyDrafts?.refresh();
window.rustProxyPreferences?.refreshAppearanceUi?.();
$("connection-label").textContent = t(connected ? "connected" : "disconnected");
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); window.rustProxyBackendDashboard?.render(latestStatus, null, false); window.rustProxyExtraOverview?.render(latestStatus, false); window.rustProxyWideOverview?.render(latestStatus, false); } if (lastLogs) renderLogs(lastLogs);
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); window.rustProxyBackendDashboard?.render(latestStatus, null, false); window.rustProxyExtraOverview?.render(latestStatus, false); window.rustProxyWideOverview?.render(latestStatus, false); window.rustProxyWebOverview?.render(latestStatus, false); } if (lastLogs) renderLogs(lastLogs);
syncSettingsDirtyIndicators();
}
function hideToast() { const node = $("toast"); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
function raiseToast() {
const node = $("toast"); if (node.hidden) return;
const modal = [...document.querySelectorAll("dialog[open]")].at(-1), parent = modal || document.body;
// A popover outside a modal is still inert. Keep it inside the active dialog,
// then lift it to the top layer so its backdrop cannot blur or cover it.
if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover();
if (node.parentElement !== parent) parent.append(node);
if (typeof node.showPopover === "function") node.showPopover();
@@ -154,19 +150,16 @@ function setPanel(name, navigate = true) {
const previousPanel = currentPanel; if (name !== "overview" && previousPanel === "overview") window.rustProxyInlineLayout?.cancel?.();
currentPanel = name;
$("overview-refresh").hidden = name !== "overview";
$("overview-samples-toolbar").hidden = name !== "overview";
for (const id of ["overview-layout-open", "overview-layout-unlock"]) if ($(id)) $(id).hidden = name !== "overview";
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
document.querySelectorAll(".panel").forEach(e => e.hidden = e.id !== `panel-${name}`);
$("page-label").textContent = t(name); $("page-title").textContent = t(`${name}Title`); $("page-description").textContent = t(`${name}Description`);
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
if (name === "web-server" && latestStatus) window.rustProxyWebServer?.renderLive?.(latestStatus, false);
if (name === "settings") window.rustProxySettings?.syncLocation?.();
window.rustProxyRefresh?.setPanel(name);
// Paint Stream tiles immediately on return from Appearance using cached status.
// Do not create an extra sample/poll on navigation.
if (name === "overview" && previousPanel !== name && typeof latestStatus !== "undefined" && latestStatus)
{ window.rustProxyStreamDashboard?.render?.(latestStatus, null, false); window.rustProxyBackendDashboard?.render?.(latestStatus, null, false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); }
// Read the cached Docker status on each visit, independent of the Live-view toggle.
// This is GET /api/docker, not the POST that triggers an agent poll.
if (name === "overview" && previousPanel !== name && typeof latestStatus !== "undefined" && latestStatus) { window.rustProxyStreamDashboard?.render?.(latestStatus, null, false); window.rustProxyBackendDashboard?.render?.(latestStatus, null, false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
if (name === "docker-vhosts" && previousPanel !== name && authenticated && envelope) void window.rustProxyRefresh?.manual("docker");
if (name === "tls") window.rustProxyAcme?.syncLocation?.();
if (name === "filelogs") void window.rustProxyFileLogs?.refresh();
@@ -187,7 +180,7 @@ function uptime(value) {
function rateBytes(value) { value = Math.max(0, Number(value) || 0); if (value === 0) return "0.0 KiB"; value /= 1024; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { if (value < 1024 || unit === "TiB") { if (unit === "KiB" && value < 0.1) return "<0.1 KiB"; const digits = value < 10 ? 1 : 0; return `${Number(value.toFixed(digits))} ${unit}`; } value /= 1024; } }
function el(tag, text, cls) { const e = document.createElement(tag); if (text !== undefined) e.textContent = text; if (cls) e.className = cls; return e; }
function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
const label = input?.closest("label"); if (!label) return () => {};
const label = input?.closest("label"); if (!label) return () => { };
label.classList.add("vh-enable-control", "vh-wide");
let hint = label.querySelector(".vh-enable-hint");
if (!hint) { hint = el("small", undefined, "vh-enable-hint"); label.append(hint); }
@@ -203,9 +196,35 @@ function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
}
window.rustProxyEnableControl = enhanceEnabledControl;
function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; }
function redrawOverviewHistoryCharts() {
const connectionPeak = Math.max(1, ...connectionOpenSamples, ...connectionExchangeSamples);
const connectionPoints = values => values.map((value, index) => `${index * 300 / Math.max(1, values.length - 1)},${95 - value / connectionPeak * 85}`).join(" ");
$("connection-open-sparkline").setAttribute("points", connectionPoints(connectionOpenSamples));
$("connection-exchange-sparkline").setAttribute("points", connectionPoints(connectionExchangeSamples));
const peak = Math.max(1, ...samples, ...transformSamples);
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
$("sparkline").setAttribute("points", points(samples));
$("sparkline-h1").setAttribute("points", points(protocolSamples.h1));
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
$("sparkline-transform").setAttribute("points", points(transformSamples));
for (const key of ["h1", "h2", "h3"]) {
$("http-protocol-" + key).setAttribute("points", sparklinePoints(protocolSamples[key], 300, 60, 5));
$("ws-protocol-" + key).setAttribute("points", sparklinePoints(websocketProtocolSamples[key], 300, 60, 5));
}
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
}
window.rustProxyOverviewSamples.onChange(() => {
for (const history of [samples, transformSamples, connectionOpenSamples, connectionExchangeSamples,
...Object.values(protocolSamples), ...Object.values(websocketProtocolSamples)]) {
window.rustProxyOverviewSamples.trim(history);
}
redrawOverviewHistoryCharts();
});
function renderStatus(s, systemSample = false) {
const firstStatus = latestStatus === null;
latestStatus = { ...(latestStatus || {}), ...s,
latestStatus = {
...(latestStatus || {}), ...s,
streams: s.streams ? { ...(latestStatus?.streams || {}), ...s.streams } : latestStatus?.streams,
listeners: s.listeners ? { ...(latestStatus?.listeners || {}), ...s.listeners } : latestStatus?.listeners,
logging: s.logging ? { ...(latestStatus?.logging || {}), ...s.logging } : latestStatus?.logging
@@ -220,12 +239,8 @@ function renderStatus(s, systemSample = false) {
$("metric-h2").textContent = m.http2_requests || 0; $("metric-h2-active").textContent = m.http2_active_connections || 0; $("metric-tls-errors").textContent = m.tls_handshake_errors || 0;
$("metric-requests").textContent = m.requests.toLocaleString(locale()); $("metric-active").textContent = m.active; $("metric-bytes").textContent = bytes(m.bytes_in + m.bytes_out); $("metric-bytes-down").textContent = bytes(m.bytes_out); $("metric-bytes-up").textContent = bytes(m.bytes_in); $("metric-errors").textContent = m.errors;
const openConnections = Math.max(0, Number(m.active_connections) || 0), activeExchanges = Math.max(0, Number(m.active) || 0);
connectionOpenSamples.push(openConnections); connectionOpenSamples = connectionOpenSamples.slice(-30);
connectionExchangeSamples.push(activeExchanges); connectionExchangeSamples = connectionExchangeSamples.slice(-30);
const connectionPeak = Math.max(1, ...connectionOpenSamples, ...connectionExchangeSamples);
const connectionPoints = values => values.map((value, index) => `${index * 300 / Math.max(1, values.length - 1)},${95 - value / connectionPeak * 85}`).join(" ");
$("connection-open-sparkline").setAttribute("points", connectionPoints(connectionOpenSamples));
$("connection-exchange-sparkline").setAttribute("points", connectionPoints(connectionExchangeSamples));
connectionOpenSamples.push(openConnections); connectionOpenSamples = connectionOpenSamples.slice(-window.rustProxyOverviewSamples.getLimit());
connectionExchangeSamples.push(activeExchanges); connectionExchangeSamples = connectionExchangeSamples.slice(-window.rustProxyOverviewSamples.getLimit());
$("connection-current").textContent = fmt(openConnections); $("connection-open-value").textContent = fmt(openConnections); $("connection-exchange-value").textContent = fmt(activeExchanges); $("connection-backends").textContent = fmt(s.backends);
const now = performance.now();
const transformed = s.transform_metrics || {};
@@ -234,38 +249,25 @@ function renderStatus(s, systemSample = false) {
const pausedRate = previous !== null && rateSample.deltas === null; previous = rateSample.next;
const rates = Object.fromEntries(keys.map((key, i) => [key, rateSample.deltas ? rateSample.deltas[i] / rateSample.seconds : 0]));
if (!pausedRate) {
samples.push(rates.total); samples = samples.slice(-30);
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-30); }
samples.push(rates.total); samples = samples.slice(-window.rustProxyOverviewSamples.getLimit());
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-window.rustProxyOverviewSamples.getLimit()); }
}
websocketProtocolSamples.h1.push(Number(m.websocket_h1_active) || 0); websocketProtocolSamples.h1 = websocketProtocolSamples.h1.slice(-30);
websocketProtocolSamples.h2.push(Number(m.websocket_h2_active) || 0); websocketProtocolSamples.h2 = websocketProtocolSamples.h2.slice(-30);
websocketProtocolSamples.h3.push(Number(m.websocket_h3_active) || 0); websocketProtocolSamples.h3 = websocketProtocolSamples.h3.slice(-30);
if (!pausedRate) { transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-30); }
const peak = Math.max(1, ...samples, ...transformSamples);
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
$("sparkline").setAttribute("points", points(samples));
$("sparkline-h1").setAttribute("points", points(protocolSamples.h1));
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
$("sparkline-transform").setAttribute("points", points(transformSamples));
websocketProtocolSamples.h1.push(Number(m.websocket_h1_active) || 0); websocketProtocolSamples.h1 = websocketProtocolSamples.h1.slice(-window.rustProxyOverviewSamples.getLimit());
websocketProtocolSamples.h2.push(Number(m.websocket_h2_active) || 0); websocketProtocolSamples.h2 = websocketProtocolSamples.h2.slice(-window.rustProxyOverviewSamples.getLimit());
websocketProtocolSamples.h3.push(Number(m.websocket_h3_active) || 0); websocketProtocolSamples.h3 = websocketProtocolSamples.h3.slice(-window.rustProxyOverviewSamples.getLimit());
if (!pausedRate) { transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
redrawOverviewHistoryCharts();
const rateText = key => pausedRate ? "-" : `${rates[key].toFixed(1)}/s`;
$("request-rate").textContent = pausedRate ? "- req/s" : `${rates.total.toFixed(1)} req/s`;
$("rate-total").textContent = rateText("total"); $("rate-h1").textContent = rateText("h1"); $("rate-h2").textContent = rateText("h2"); $("rate-h3").textContent = rateText("h3"); $("rate-transform").textContent = rateText("transform");
$("metric-http-proto-total").textContent = rateText("total"); $("metric-http-proto-h1").textContent = rateText("h1");
$("metric-http-proto-h2").textContent = rateText("h2"); $("metric-http-proto-h3").textContent = rateText("h3");
$("http-protocol-h1").setAttribute("points", sparklinePoints(protocolSamples.h1, 300, 60, 5));
$("http-protocol-h2").setAttribute("points", sparklinePoints(protocolSamples.h2, 300, 60, 5));
$("http-protocol-h3").setAttribute("points", sparklinePoints(protocolSamples.h3, 300, 60, 5));
const wsTotalActive = (Number(m.websocket_h1_active) || 0) + (Number(m.websocket_h2_active) || 0) + (Number(m.websocket_h3_active) || 0);
$("metric-ws-proto-total").textContent = wsTotalActive.toLocaleString(locale());
$("metric-ws-proto-h1").textContent = (Number(m.websocket_h1_active) || 0).toLocaleString(locale());
$("metric-ws-proto-h2").textContent = (Number(m.websocket_h2_active) || 0).toLocaleString(locale());
$("metric-ws-proto-h3").textContent = (Number(m.websocket_h3_active) || 0).toLocaleString(locale());
$("ws-protocol-h1").setAttribute("points", sparklinePoints(websocketProtocolSamples.h1, 300, 60, 5));
$("ws-protocol-h2").setAttribute("points", sparklinePoints(websocketProtocolSamples.h2, 300, 60, 5));
$("ws-protocol-h3").setAttribute("points", sparklinePoints(websocketProtocolSamples.h3, 300, 60, 5));
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s); window.rustProxyBackendDashboard?.render(s); window.rustProxyExtraOverview?.render(s); window.rustProxyWideOverview?.render(s);
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s); window.rustProxyBackendDashboard?.render(s); window.rustProxyExtraOverview?.render(s); window.rustProxyWideOverview?.render(s); window.rustProxyWebOverview?.render(s); window.rustProxyWebServer?.renderLive?.(s);
}
function renderFooter(s) {
const apiLatency = lastApiLatencyMs == null ? "API -" : `API ${lastApiLatencyMs.toFixed(lastApiLatencyMs < 10 ? 1 : 0)} ms`;
@@ -370,17 +372,16 @@ function authReturnDestination() {
const tab = window.rustProxySettings?.current?.() || new URLSearchParams(location.search).get("tab");
return tab ? `/settings?tab=${encodeURIComponent(tab)}` : "/settings";
}
function lock(notify = true) { window.rustProxyInlineLayout?.cancel?.();
function lock(notify = true) {
window.rustProxyInlineLayout?.cancel?.();
window.rustProxyRefresh?.stop();
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); window.rustProxyWebOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; websocketProtocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
$("workspace").querySelectorAll("form").forEach(form => form.reset());
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
document.querySelectorAll("dialog").forEach(dialog => { dialog.close(); dialog.querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => input.value = ""); });
renderSession(); syncAuthForm();
// Replace the authenticated document with the minimal public login document.
// Keep the exact panel/settings tab so re-authentication resumes in place.
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
}
@@ -392,8 +393,6 @@ async function activate(info) {
for (const id of ["password", "password-confirm", "setup-token"]) $(id).value = "";
$("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false;
renderSession();
// Paint a usable shell immediately. Preferences, configuration and initial
// telemetry are network requests and must not block the first content.
document.documentElement.classList.remove("app-loading");
document.documentElement.classList.remove("app-startup-failed");
rustProxyRefresh.start();
@@ -415,8 +414,6 @@ async function activate(info) {
}
}
if (!authenticated || epoch !== authEpoch) return false;
// A single full snapshot seeds non-live diagnostics; subsequent updates
// continue over SSE rather than polling full status on a timer.
void refresh(true).catch(error => {
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
});
@@ -470,8 +467,6 @@ function setMobileMenu(open) {
}
mobileMenuToggle?.addEventListener("click", event => { event.stopPropagation(); setMobileMenu(!dashboardSidebar.classList.contains("mobile-nav-open")); });
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => { setPanel(button.dataset.panel); setMobileMenu(false); }));
// The brand is a Home navigation action, not a hard reload of the HTTP/3
// document. Preserve href="/" as the no-JS and modified-click fallback.
document.querySelector('.brand[href="/"]')?.addEventListener("click", event => {
if (!authenticated || event.defaultPrevented || event.button !== 0 ||
event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;