This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 00:00:06 +02:00
1 parent d23fa5e154
commit 81ab39e70a
51 files changed
+5352 -1489

No files matched your search

+7 -3
View File
@@ -59,7 +59,7 @@ 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); } 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); } if (lastLogs) renderLogs(lastLogs);
syncSettingsDirtyIndicators();
}
function hideToast() { const node = $("toast"); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
@@ -160,6 +160,10 @@ function setPanel(name, navigate = true) {
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
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);
// 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 === "docker-vhosts" && previousPanel !== name && authenticated && envelope) void window.rustProxyRefresh?.manual("docker");
@@ -262,7 +266,7 @@ function renderStatus(s, systemSample = false) {
$("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);
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s);
}
function renderFooter(s) {
const apiLatency = lastApiLatencyMs == null ? "API -" : `API ${lastApiLatencyMs.toFixed(lastApiLatencyMs < 10 ? 1 : 0)} ms`;
@@ -370,7 +374,7 @@ function authReturnDestination() {
function lock(notify = true) {
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(); 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(); 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("");
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 = ""; });
+29 -8
View File
@@ -3,10 +3,10 @@ const ADMIN_TIME_FORMATS = new Set(["24h", "12h"]);
const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool"]);
const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]);
const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]);
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(["requests", "active", "bandwidth", "errors", "cache", "memory", "websocket", "http2", "http3", "tls", "http_protocol", "websocket_protocol"]);
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(["requests", "active", "bandwidth", "errors", "cache", "memory", "websocket", "http2", "http3", "tls", "http_protocol", "websocket_protocol", "stream_overview", "stream_traffic", "stream_sessions", "stream_backend_health"]);
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
button_style: "normal", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [],
button_style: "normal", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: ["stream_overview", "stream_traffic", "stream_sessions", "stream_backend_health"],
});
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
@@ -24,9 +24,14 @@ function normalizeDashboardWidgets(value) {
for (const id of ADMIN_DASHBOARD_WIDGETS) if (!seen.has(id)) result.push(id);
return result;
}
function normalizeHiddenDashboardWidgets(value) {
function normalizeHiddenDashboardWidgets(value, savedOrder = ADMIN_DASHBOARD_WIDGETS) {
const seen = new Set();
return (Array.isArray(value) ? value : []).filter(id => ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id) && seen.add(id));
const hidden = (Array.isArray(value) ? value : []).filter(id => ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id) && seen.add(id));
// Also protect preferences written by 0.31.x with an explicit empty hidden list.
for (const id of ADMIN_DASHBOARD_WIDGETS.filter(id => id.startsWith("stream_"))) {
if (!savedOrder?.includes(id) && !seen.has(id)) { hidden.push(id); seen.add(id); }
}
return hidden;
}
function browserLanguage() {
const packs = [...languagePacks.values()], candidates = (navigator.languages || [navigator.language]).filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase());
@@ -81,6 +86,11 @@ function applyDashboardLayout(order, hidden) {
const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
for (const id of normalizedOrder) { const node = nodes.get(id); if (!node) continue; node.hidden = hiddenSet.has(id); grid.append(node); }
// Apply the newly selected visibility, not the previous saved preferences.
// During load, adminPreferences is assigned only after this function returns.
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview")
window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false);
return { order: normalizedOrder, hidden: [...hiddenSet] };
}
function dashboardWidgetLabel(id) {
@@ -94,7 +104,18 @@ function renderDashboardWidgetSettings() {
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
const visible = !hidden.has(id);
const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
const labelBlock = el("span", undefined, "dashboard-widget-setting-label-block");
labelBlock.append(el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
const descriptionKey = ({ stream_overview: "streamWidgetOverviewDescription", stream_traffic: "streamWidgetTrafficDescription", stream_sessions: "streamWidgetSessionsDescription", stream_backend_health: "streamWidgetHealthDescription" })[id];
if (descriptionKey) {
const description = el("small", t(descriptionKey), "dashboard-widget-description");
description.dataset.i18n = descriptionKey;
description.id = `dashboard-widget-help-${id}`;
description.setAttribute("role", "tooltip");
toggle.setAttribute("aria-describedby", description.id);
labelBlock.append(description);
}
toggle.append(el("span", undefined, "dashboard-widget-visibility"), labelBlock);
const actions = el("span", undefined, "dashboard-widget-setting-actions");
const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp"));
const down = el("button", "↓", "quiet"); down.type = "button"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("moveDown"));
@@ -127,7 +148,7 @@ function formatDateTime(value) { const date = value instanceof Date ? value : ne
function formatTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleTimeString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
function applyAdminPreferences(value, cache = true) {
if (!value) return value;
const dashboard = applyDashboardLayout(value.dashboard_widgets, value.dashboard_hidden_widgets);
const dashboard = applyDashboardLayout(value.dashboard_widgets, normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets));
const normalized = {
...value,
refresh: normalizeRefreshPreferences(value.refresh),
@@ -279,9 +300,9 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
const patches = {
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal" }, charts: { chart_style: "default" }, tables: { table_density: "normal" },
dashboard: { dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [] },
dashboard: { dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] },
};
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
}));
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [] }, $("appearance-reset"), "appearanceResetDone"));
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] }, $("appearance-reset"), "appearanceResetDone"));
refreshAppearanceUi();
+114
View File
@@ -0,0 +1,114 @@
// Four opt-in cards in the EXISTING dashboard widget order and status refresh.
// No timers, fetches, or independent persistence are created here.
window.rustProxyStreamDashboard = (() => {
const ids = ["stream_overview", "stream_traffic", "stream_sessions", "stream_backend_health"];
const history = { last: null, rx: [], tx: [] };
let wasVisible = false;
const card = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
const set = (id, value) => { const node = $(id); if (node) node.textContent = value; };
const trafficRates = (rx, tx) => {
set("stream-dashboard-rx-rate", rx);
set("stream-dashboard-tx-rate", tx);
};
function clearTraffic() {
history.last = null; history.rx.length = 0; history.tx.length = 0;
$("stream-dashboard-rx")?.setAttribute("points", "0,55 300,55");
$("stream-dashboard-tx")?.setAttribute("points", "0,55 300,55");
}
function syncVisibility(hiddenOverride = null) {
const prefs = window.rustProxyPreferences?.current?.();
const hidden = new Set(hiddenOverride ?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
// Visibility belongs exclusively to Appearance. An absent, disabled or
// restart-pending Stream must not silently override an explicit opt-in.
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
if (card("stream_traffic")?.hidden && wasVisible) clearTraffic();
wasVisible = !card("stream_traffic")?.hidden;
}
function line(points) {
const peak = Math.max(1, ...history.rx, ...history.tx);
return points.map((value, i) => `${i * 300 / Math.max(1, points.length - 1)},${55 - value / peak * 50}`).join(" ");
}
function render(status, hiddenOverride = null, recordTrafficSample = true) {
syncVisibility(hiddenOverride);
if (!status || currentPanel !== "overview" || ids.every(id => card(id)?.hidden)) return;
const configured = envelope?.config?.streams || status?.streams?.desired || [];
const active = (status?.streams?.active || []).filter(item => item.enabled !== false);
const metrics = status?.streams?.metrics || {};
if (!configured.some(item => item.enabled !== false) && !active.length) {
// A deliberately shown tile has a neutral empty state, not a
// hidden DOM node. No history is sampled without running streams.
for (const name of ["overview", "sessions", "health", "traffic"]) {
if (!card(({ overview: "stream_overview", sessions: "stream_sessions", health: "stream_backend_health", traffic: "stream_traffic" })[name])?.hidden) {
set(`stream-dashboard-${name}`, "-");
if (name === "traffic") trafficRates("-", "-");
else set(`stream-dashboard-${name}-detail`, t("streamWidgetUnavailable"));
}
}
if (!card("stream_traffic")?.hidden) clearTraffic();
return;
}
if (!card("stream_overview")?.hidden) {
const desired = configured.filter(item => item.enabled !== false);
const attention = desired.filter(item => !active.some(a => a.id === item.id)).length +
active.filter(item => (metrics[item.id]?.backends || []).length &&
!(metrics[item.id].backends || []).some(b => b.enabled && !b.drain && b.status !== "unhealthy")).length;
set("stream-dashboard-overview", `${active.length} / ${desired.length}`);
set("stream-dashboard-overview-detail", `${t("streamWidgetActiveConfigured")} · ${attention} ${t("streamWidgetAttention")}`);
}
if (!card("stream_sessions")?.hidden) {
const sum = protocol => active.filter(item => item.protocol === protocol)
.reduce((count, item) => count + (Number(metrics[item.id]?.active) || 0), 0);
const tcp = sum("tcp"), udp = sum("udp");
set("stream-dashboard-sessions", fmt(tcp + udp));
set("stream-dashboard-sessions-detail", `TCP ${fmt(tcp)} · UDP ${fmt(udp)}`);
}
if (!card("stream_backend_health")?.hidden) {
const backends = active.flatMap(item => metrics[item.id]?.backends || []);
const healthy = backends.filter(b => b.status === "healthy").length;
const unhealthy = backends.filter(b => b.status === "unhealthy").length;
const unverified = backends.filter(b => b.status === "unverified" || b.status === "checking").length;
const emptyPools = active.filter(item => (metrics[item.id]?.backends || []).length > 0 &&
!(metrics[item.id].backends || []).some(b => b.enabled && !b.drain && b.status !== "unhealthy")).length;
set("stream-dashboard-health", `${healthy} / ${unhealthy}`);
set("stream-dashboard-health-detail", `${t("streamWidgetHealthyUnhealthy")} · ${unverified} ${t("streamWidgetUnverified")} · ${emptyPools} ${t("streamWidgetEmptyPools")}`);
}
if (card("stream_traffic")?.hidden) return;
const views = active.map(item => metrics[item.id]).filter(Boolean);
if (!views.length) {
set("stream-dashboard-traffic", t("streamWidgetUnavailable"));
trafficRates("-", "-");
return;
}
// Switching tabs or appearance preferences paints cached numbers without
// generating an extra rate sample from the same status response.
if (!recordTrafficSample) {
if (history.rx.length) {
const rx = history.rx.at(-1), tx = history.tx.at(-1);
set("stream-dashboard-traffic", `${bytes(Math.round(rx))}/s`);
trafficRates(`${bytes(Math.round(rx))}/s`, `${bytes(Math.round(tx))}/s`);
} else {
set("stream-dashboard-traffic", "-");
trafficRates("-", "-");
}
return;
}
const rxTotal = views.reduce((sum, view) => sum + (Number(view.bytes_in) || 0), 0);
const txTotal = views.reduce((sum, view) => sum + (Number(view.bytes_out) || 0), 0);
const now = performance.now();
let rx = 0, tx = 0;
if (history.last) {
const seconds = Math.max(.001, (now - history.last.when) / 1000);
if (rxTotal >= history.last.rx) rx = (rxTotal - history.last.rx) / seconds;
if (txTotal >= history.last.tx) tx = (txTotal - history.last.tx) / seconds;
}
history.last = { when: now, rx: rxTotal, tx: txTotal };
history.rx.push(rx); history.tx.push(tx);
if (history.rx.length > 60) { history.rx.shift(); history.tx.shift(); }
set("stream-dashboard-traffic", `${bytes(Math.round(rx))}/s`);
trafficRates(`${bytes(Math.round(rx))}/s`, `${bytes(Math.round(tx))}/s`);
$("stream-dashboard-rx")?.setAttribute("points", line(history.rx));
$("stream-dashboard-tx")?.setAttribute("points", line(history.tx));
}
function reset() { clearTraffic(); wasVisible = false; }
return { syncVisibility, render, reset };
})();