v0.36.8
This commit is contained in:
1 parent
1ac94213e0
commit
0119b11005
28 files changed
+698
-153
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.36.7",
|
||||
"version": "0.36.8",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
@@ -3953,8 +3953,10 @@
|
||||
"connection_chart",
|
||||
"cache_diagnostics",
|
||||
"process_diagnostics",
|
||||
"process_simple",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"backend_tiny",
|
||||
"extra_cold_queue_memory",
|
||||
"extra_cold_storage",
|
||||
"wide_transform_activity",
|
||||
@@ -4038,8 +4040,10 @@
|
||||
"connection_chart",
|
||||
"cache_diagnostics",
|
||||
"process_diagnostics",
|
||||
"process_simple",
|
||||
"network_diagnostics",
|
||||
"backend_table"
|
||||
"backend_table",
|
||||
"backend_tiny"
|
||||
],
|
||||
"description": "Account-scoped order for configurable Overview telemetry cards."
|
||||
},
|
||||
@@ -4130,8 +4134,10 @@
|
||||
"connection_chart",
|
||||
"cache_diagnostics",
|
||||
"process_diagnostics",
|
||||
"process_simple",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"backend_tiny",
|
||||
"extra_cold_queue_memory",
|
||||
"extra_cold_storage",
|
||||
"wide_transform_activity",
|
||||
|
||||
@@ -18,6 +18,7 @@ fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
"wide_compression_activity",
|
||||
"cache",
|
||||
"backend_table",
|
||||
"process_diagnostics",
|
||||
] {
|
||||
assert!(order.contains(&id.to_owned()), "not registered: {id}");
|
||||
assert!(
|
||||
@@ -28,6 +29,8 @@ fn overview_factory_preset_matches_registry_and_operator_visibility() {
|
||||
for id in [
|
||||
"bandwidth",
|
||||
"backend_health",
|
||||
"backend_tiny",
|
||||
"process_simple",
|
||||
"extra_process_cpu",
|
||||
"stream_traffic",
|
||||
"wide_cache_activity",
|
||||
|
||||
@@ -61,15 +61,23 @@ function translate() {
|
||||
syncSettingsDirtyIndicators();
|
||||
window.rustProxyIcons?.refresh?.();
|
||||
}
|
||||
function hideToast() { const node = $("toast"); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
|
||||
function toastNode() {
|
||||
let node = document.getElementById("toast");
|
||||
if (!node) { // the node lived inside a dialog that was rebuilt; recreate it instead of failing every later toast
|
||||
node = document.createElement("div"); node.id = "toast"; node.className = "toast"; node.hidden = true;
|
||||
node.setAttribute("role", "status"); node.setAttribute("aria-live", "polite"); node.setAttribute("popover", "manual"); document.body.append(node);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
function hideToast() { const node = toastNode(); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
|
||||
function raiseToast() {
|
||||
const node = $("toast"); if (node.hidden) return;
|
||||
const node = toastNode(); if (node.hidden) return;
|
||||
const modal = [...document.querySelectorAll("dialog[open]")].at(-1), parent = modal || document.body;
|
||||
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();
|
||||
}
|
||||
function toast(message, error = false) { clearTimeout(toastTimer); const node = $("toast"); node.textContent = message; node.classList.toggle("error", error); node.hidden = false; raiseToast(); toastTimer = setTimeout(hideToast, error ? 10000 : 4000); }
|
||||
function toast(message, error = false) { clearTimeout(toastTimer); const node = toastNode(); node.textContent = message; node.classList.toggle("error", error); node.hidden = false; raiseToast(); toastTimer = setTimeout(hideToast, error ? 10000 : 4000); }
|
||||
function authMessage(message = "") { const node = $("auth-message"); if (!node) return; node.textContent = message; node.classList.toggle("error", Boolean(message)); }
|
||||
function fieldError(target, message) { const error = new Error(message); error.fieldTarget = target; return error; }
|
||||
function clearFieldError(target) {
|
||||
@@ -134,7 +142,7 @@ function apiErrorMessage(path, response, value) {
|
||||
const area = apiArea(path);
|
||||
return area ? t("errorContext").replace("{area}", t(area)).replace("{message}", detail) : detail;
|
||||
}
|
||||
new MutationObserver(records => { if (!records.some(r => r.target.tagName === "DIALOG")) return; const node = $("toast"); if (!node.hidden) raiseToast(); }).observe(document.body, { subtree: true, attributes: true, attributeFilter: ["open"] });
|
||||
new MutationObserver(records => { if (!records.some(r => r.target.tagName === "DIALOG")) return; const node = toastNode(); if (!node.hidden) raiseToast(); }).observe(document.body, { subtree: true, attributes: true, attributeFilter: ["open"] });
|
||||
async function api(path, options = {}) {
|
||||
const started = performance.now(), trackLatency = path.startsWith("/api/");
|
||||
try {
|
||||
|
||||
@@ -81,7 +81,7 @@ window.rustProxyDashboardLayout = (() => {
|
||||
if (ADMIN_DASHBOARD_BY_ID.get(id)?.category === "panels") {
|
||||
const card = el("article", undefined, "overview-layout-panel-preview");
|
||||
card.append(el("strong", label(id)));
|
||||
const chart = id === "backend_table" ? null : original.querySelector?.("svg");
|
||||
const chart = id === "backend_tiny" || id === "backend_table" ? null : original.querySelector?.("svg");
|
||||
if (chart) {
|
||||
const graphic = chart.cloneNode(true);
|
||||
for (const node of [graphic, ...graphic.querySelectorAll("[id]")]) node.removeAttribute("id");
|
||||
|
||||
@@ -226,7 +226,7 @@ $("reset-certificate").addEventListener("click", () => { $("certificate-form").r
|
||||
syncCertificateFileSource();
|
||||
|
||||
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, lastCacheOutcomeRates = null,
|
||||
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
|
||||
processCpuSamples = [], processRssSamples = [], processFdSamples = [], processThreadSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
|
||||
const hiddenNetworkInterfaces = (() => {
|
||||
try {
|
||||
const parsed = JSON.parse(localStorage.getItem("rp-hidden-network-interfaces") || "[]");
|
||||
@@ -257,6 +257,68 @@ function setOptionalMetric(id, supported, text) {
|
||||
const row = node.closest("div"); if (row) row.hidden = !supported;
|
||||
if (supported) node.textContent = text;
|
||||
}
|
||||
function setProcessMeter(id, used, limit) {
|
||||
const meter = $(id)?.closest("div")?.querySelector(".pr-meter"); if (!meter) return;
|
||||
const usedNumber = Number(used), limitNumber = Number(limit);
|
||||
const ok = used != null && limit != null && Number.isFinite(usedNumber) && Number.isFinite(limitNumber) && limitNumber > 0;
|
||||
meter.hidden = !ok; if (!ok) return;
|
||||
const percent = Math.max(0, Math.min(100, usedNumber / limitNumber * 100));
|
||||
const bar = meter.firstElementChild; bar.style.width = `${percent}%`; bar.classList.toggle("is-high", percent >= 85);
|
||||
}
|
||||
function renderProcessOverview(d, status, live, systemSample) {
|
||||
if (!$("po-cpu")) return;
|
||||
if (systemSample) {
|
||||
const memory = status.memory_accounting || {};
|
||||
const platform = [d.platform, d.environment].filter(Boolean).join(" / ");
|
||||
setOptionalMetric("po-platform", !!d.platform && d.platform !== "Unsupported", platform);
|
||||
setOptionalMetric("po-threads", d.process_threads != null, fmt(d.process_threads));
|
||||
setOptionalMetric("po-start", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000));
|
||||
setOptionalMetric("po-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds));
|
||||
setOptionalMetric("po-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
|
||||
setProcessMeter("po-cpu", d.process_cpu_percent, 100);
|
||||
setOptionalMetric("po-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes));
|
||||
setProcessMeter("po-rss-total", d.process_rss_bytes, d.cgroup_memory_max_bytes);
|
||||
const fds = knownPair(d.open_fds, d.max_fds, value => fmt(value), t("unlimited"));
|
||||
setOptionalMetric("po-fds", fds != null, fds);
|
||||
setProcessMeter("po-fds", d.open_fds, d.max_fds);
|
||||
const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||
setOptionalMetric("po-cgroup", cgroupMemory != null, cgroupMemory);
|
||||
setProcessMeter("po-cgroup", d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes);
|
||||
setOptionalMetric("po-rss-anon", d.process_rss_anon_bytes != null, fmtBytes(d.process_rss_anon_bytes));
|
||||
const rssFile = knownPair(d.process_rss_file_bytes, d.process_rss_shmem_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("po-rss-file", rssFile != null, rssFile);
|
||||
setOptionalMetric("po-non-cache", memory.non_cache_rss_bytes != null, fmtBytes(memory.non_cache_rss_bytes));
|
||||
const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||
setOptionalMetric("po-virtual", virtualMemory != null, virtualMemory);
|
||||
const dataSwap = knownPair(d.process_data_bytes, d.process_swap_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("po-data-swap", dataSwap != null, dataSwap);
|
||||
const split = $("po-rss-split"), anon = Number(d.process_rss_anon_bytes), file = Number(d.process_rss_file_bytes);
|
||||
const known = d.process_rss_anon_bytes != null && d.process_rss_file_bytes != null && anon + file > 0;
|
||||
split.hidden = !known;
|
||||
if (known) { split.firstElementChild.style.width = `${anon / (anon + file) * 100}%`; split.lastElementChild.style.width = `${file / (anon + file) * 100}%`; }
|
||||
}
|
||||
$("po-cpu-sparkline").setAttribute("points", sparklinePoints(processCpuSamples));
|
||||
window.rustProxyChartScale.axes("po-cpu-sparkline", Math.max(1, ...processCpuSamples), "percent",
|
||||
processCpuSamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("po-cpu-chart-value"), live.process_cpu_percent == null ? "" : `${fmt(live.process_cpu_percent, 1)}%`);
|
||||
$("po-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples));
|
||||
window.rustProxyChartScale.axes("po-rss-sparkline", Math.max(1, ...processRssSamples), "bytes",
|
||||
processRssSamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("po-rss-chart-value"), live.process_rss_bytes == null ? "" : fmtBytes(live.process_rss_bytes));
|
||||
$("po-cpu-sparkline").closest(".mini-live-chart").hidden = live.process_cpu_percent == null;
|
||||
$("po-rss-sparkline").closest(".mini-live-chart").hidden = live.process_rss_bytes == null;
|
||||
const openFdsNow = live.open_fds ?? d.open_fds, threadsNow = live.process_threads ?? d.process_threads;
|
||||
$("po-fd-sparkline").setAttribute("points", sparklinePoints(processFdSamples));
|
||||
window.rustProxyChartScale.axes("po-fd-sparkline", Math.max(1, ...processFdSamples), "count",
|
||||
processFdSamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("po-fd-chart-value"), openFdsNow == null ? "" : fmt(openFdsNow));
|
||||
$("po-fd-sparkline").closest(".mini-live-chart").hidden = openFdsNow == null;
|
||||
$("po-thread-sparkline").setAttribute("points", sparklinePoints(processThreadSamples));
|
||||
window.rustProxyChartScale.axes("po-thread-sparkline", Math.max(1, ...processThreadSamples), "count",
|
||||
processThreadSamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("po-thread-chart-value"), threadsNow == null ? "" : fmt(threadsNow));
|
||||
$("po-thread-sparkline").closest(".mini-live-chart").hidden = threadsNow == null;
|
||||
}
|
||||
function knownPair(left, right, format, zeroRight = null) {
|
||||
if (left == null && right == null) return null;
|
||||
if (left == null) return format(right);
|
||||
@@ -528,6 +590,9 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
if (appendSample) {
|
||||
if (live.process_cpu_percent != null) { processCpuSamples.push(Number(live.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (live.process_rss_bytes != null) { processRssSamples.push(Number(live.process_rss_bytes)); processRssSamples = processRssSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
const openFds = live.open_fds ?? d.open_fds, threadCount = live.process_threads ?? d.process_threads;
|
||||
if (openFds != null) { processFdSamples.push(Number(openFds)); processFdSamples = processFdSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
if (threadCount != null) { processThreadSamples.push(Number(threadCount)); processThreadSamples = processThreadSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
}
|
||||
const cacheOutcomePeak = Math.max(1, ...cacheHitSamples, ...cacheMissSamples, ...cacheBypassSamples);
|
||||
$("cache-hit-sparkline").setAttribute("points", sparklinePoints(cacheHitSamples, 300, 60, 5, cacheOutcomePeak));
|
||||
@@ -599,6 +664,7 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
setChartText($("process-rss-chart-value"), live.process_rss_bytes == null ? "" : fmtBytes(live.process_rss_bytes));
|
||||
$("process-cpu-sparkline").closest(".mini-live-chart").hidden = live.process_cpu_percent == null;
|
||||
$("process-rss-sparkline").closest(".mini-live-chart").hidden = live.process_rss_bytes == null;
|
||||
renderProcessOverview(d, status, live, systemSample);
|
||||
updateCacheMemorySizing();
|
||||
const networkRateSupported = live.network_rx_bytes_per_second != null && live.network_tx_bytes_per_second != null;
|
||||
if (systemSample) setOptionalMetric("system-network", d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null, `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}`);
|
||||
@@ -628,6 +694,7 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
$("system-sample-age").hidden = age == null;
|
||||
if (age != null) $("system-sample-age").textContent = `${fmt(age)} s`;
|
||||
$("system-sample-age").classList.toggle("status-error", age != null && age > 10);
|
||||
if ($("po-sample-age")) { $("po-sample-age").hidden = age == null; if (age != null) $("po-sample-age").textContent = `${fmt(age)} s`; $("po-sample-age").classList.toggle("status-error", age != null && age > 10); }
|
||||
const l = status.listeners || {};
|
||||
$("listener-pending").hidden = !l.restart_required;
|
||||
$("listener-pending").textContent = t("restartPending");
|
||||
|
||||
@@ -758,7 +758,8 @@ progress::-moz-progress-bar {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#pool-search {
|
||||
#pool-search,
|
||||
#bo-search {
|
||||
width: calc(min(250px, 30vw));
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
@@ -2349,3 +2349,108 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
|
||||
text-align: right;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Backends overview card (0.36.7): follows table density (global table rules), corner style tokens and interface colours */
|
||||
#panel-overview .backend-overview-card.is-no-proxy { display: none !important; }
|
||||
.bo-heading { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; min-width: 0; }
|
||||
.bo-summary { color: var(--muted); font: 11px var(--mono); }
|
||||
.bo-filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 10px; }
|
||||
.bo-chip { display: inline-flex; align-items: center; gap: 8px; padding: 6px 10px; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 0); background: transparent; color: var(--muted); font: 500 12px/1 var(--sans); cursor: pointer; }
|
||||
.bo-chip:hover { color: var(--text); border-color: var(--border-hover); }
|
||||
.bo-chip[aria-pressed=true] { color: var(--text); background: var(--nav-active-bg); border-color: var(--pill-line); }
|
||||
.bo-chip b { color: var(--text-subtle); font: 600 11px var(--mono); }
|
||||
.bo-chip[aria-pressed=true] b { color: var(--accent); }
|
||||
.bo-chip.is-down b:not(.is-zero) { color: var(--red); }
|
||||
.backend-overview-card .table-scroll { max-height: 520px; overflow: auto; border-top: 1px solid var(--line); }
|
||||
.bo-table { width: 100%; min-width: 960px; border-collapse: collapse; font-size: 12px; }
|
||||
.bo-table th { position: sticky; top: 0; z-index: 1; padding: 8px 10px; background: var(--panel); color: var(--muted); font: 600 10px/1.2 var(--sans); letter-spacing: .05em; text-align: left; text-transform: uppercase; white-space: nowrap; }
|
||||
.bo-table td { padding: 9px 10px; border-top: 1px solid var(--row-line); vertical-align: middle; white-space: nowrap; }
|
||||
.bo-table .is-num { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
|
||||
.bo-table th.is-num { font-family: var(--sans); }
|
||||
.bo-table .is-zero { color: var(--text-subtle); }
|
||||
.bo-row:hover>td { background: var(--panel2); }
|
||||
.bo-row.is-disabled>td:not(:first-child) { opacity: .6; }
|
||||
.bo-row>td:first-child { box-shadow: inset 3px 0 0 var(--line); }
|
||||
.bo-row.is-up>td:first-child { box-shadow: inset 3px 0 0 var(--pill-live-line); }
|
||||
.bo-row.is-down>td:first-child { box-shadow: inset 3px 0 0 var(--red); }
|
||||
.bo-row.is-warn>td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
|
||||
.bo-expand { width: 24px; height: 24px; min-width: 0; padding: 0; border: 1px solid transparent; background: transparent; color: var(--muted); cursor: pointer; line-height: 1; }
|
||||
.bo-expand::before { content: "›"; display: inline-block; font-size: 15px; transition: transform .15s ease; }
|
||||
.bo-expand[aria-expanded=true]::before { transform: rotate(90deg); }
|
||||
.bo-expand:hover { color: var(--text); border-color: var(--field-line); }
|
||||
.bo-identity { max-width: 360px; }
|
||||
.bo-name { overflow: hidden; color: var(--text); font-weight: 600; text-overflow: ellipsis; }
|
||||
.bo-sub { margin-top: 2px; overflow: hidden; color: var(--muted); font: .88em var(--mono); text-overflow: ellipsis; }
|
||||
.bo-badge { display: inline-flex; align-items: center; gap: 7px; padding: 2px 8px; border: 1px solid var(--field-line); border-radius: var(--rp-radius-control, 0); color: var(--muted); font: .82em var(--mono); letter-spacing: 1px; text-transform: uppercase; }
|
||||
.bo-badge>i { width: 6px; height: 6px; border-radius: 50%; background: var(--text-subtle); }
|
||||
.bo-badge.is-up { border-color: var(--pill-live-line); color: var(--green); }
|
||||
.bo-badge.is-up>i { background: var(--green); }
|
||||
.bo-badge.is-down { border-color: var(--danger-line-strong); color: var(--red); }
|
||||
.bo-badge.is-down>i { background: var(--red); }
|
||||
.bo-badge.is-warn { border-color: var(--warn-line); color: var(--accent); }
|
||||
.bo-badge.is-warn>i { background: var(--accent); }
|
||||
.bo-errors { min-width: 130px; }
|
||||
.bo-meter-top { display: flex; justify-content: space-between; gap: 8px; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.bo-meter-top small { color: var(--text-subtle); font-size: .9em; }
|
||||
.bo-meter { height: 2px; margin-top: 5px; overflow: hidden; background: var(--line); border-radius: var(--rp-progress-radius, 0); }
|
||||
.bo-meter>i { display: block; height: 100%; background: var(--accent); border-radius: var(--rp-progress-radius, 0); }
|
||||
.bo-meter>i.is-high { background: var(--red); }
|
||||
.bo-latency { min-width: 130px; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.bo-latency>span { display: inline-block; min-width: 62px; }
|
||||
.bo-spark { width: 56px; height: 18px; margin-left: 6px; vertical-align: middle; }
|
||||
.bo-spark polyline { fill: none; stroke: var(--green); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.bo-row.is-down .bo-spark polyline { stroke: var(--red); }
|
||||
.bo-row.is-warn .bo-spark polyline { stroke: var(--accent); }
|
||||
.bo-event small { display: block; margin-top: 1px; color: var(--text-subtle); font-size: .9em; }
|
||||
.bo-event .is-failed, .bo-detail-item .is-failed { color: var(--red); }
|
||||
.bo-event .is-recovered, .bo-detail-item .is-recovered { color: var(--green); }
|
||||
.bo-detail-row>td { padding: 0 10px 10px 40px; border-top: 0; white-space: normal; }
|
||||
.bo-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; padding: 10px 12px; border: 1px solid var(--row-line); border-radius: var(--rp-radius-surface, 0); background: var(--bg); }
|
||||
.bo-detail-item span { display: block; margin-bottom: 3px; color: var(--text-subtle); font-size: 11px; }
|
||||
.bo-detail-item b { font: 500 12px var(--mono); overflow-wrap: anywhere; }
|
||||
html[data-table-density=normal] .bo-table td { padding-top: 10px; padding-bottom: 10px; }
|
||||
html[data-table-density=ultra] .bo-table th { padding: 4px 8px; }
|
||||
html[data-table-density=ultra] .bo-table td { padding-left: 8px; padding-right: 8px; }
|
||||
html[data-table-density=ultra] .bo-sub { display: none; }
|
||||
html[data-table-density=ultra] .bo-spark { width: 40px; height: 12px; }
|
||||
html[data-table-density=ultra] .bo-meter { margin-top: 2px; }
|
||||
html[data-table-density=ultra] .bo-event small { display: inline; margin-left: 6px; }
|
||||
html[data-table-density=ultra] .bo-badge { padding: 0 6px; }
|
||||
html[data-table-density=ultra] .bo-chip { padding: 4px 8px; font-size: 11px; }
|
||||
html[data-table-density=compact] .bo-spark { width: 48px; height: 15px; }
|
||||
html[data-table-density=compact] .bo-chip { padding: 5px 9px; }
|
||||
html[data-table-density=compact] .bo-meter { margin-top: 3px; }
|
||||
|
||||
/* Process overview card (process_diagnostics) */
|
||||
[data-dashboard-widget="process_diagnostics"] { display: flex; flex-direction: column; min-height: 560px; }
|
||||
.pr-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
|
||||
.pr-chip { display: inline-flex; align-items: baseline; gap: 7px; margin: 0; padding: 3px 9px; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 0); font: 11px var(--mono); }
|
||||
.pr-chip dt { color: var(--text-subtle); font-size: 10px; letter-spacing: .05em; text-transform: uppercase; }
|
||||
.pr-chip dd { margin: 0; font-weight: 500; }
|
||||
.pr-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0 22px; margin-top: 6px; }
|
||||
.pr-title { margin: 10px 0 4px; color: var(--muted); font: 600 10px var(--sans); letter-spacing: .06em; text-transform: uppercase; }
|
||||
.pr-list { margin: 0; }
|
||||
.pr-row { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 2px 10px; padding: 7px 2px; border-top: 1px solid var(--row-line); }
|
||||
.pr-row dt { color: var(--muted); font-size: 11px; }
|
||||
.pr-row dd { margin: 0 0 0 auto; font: 12px var(--mono); font-variant-numeric: tabular-nums; text-align: end; overflow-wrap: anywhere; }
|
||||
.pr-meter { flex: 0 0 100%; height: 2px; margin-top: 3px; overflow: hidden; background: var(--line); border-radius: var(--rp-progress-radius, 0); }
|
||||
.pr-meter b { display: block; width: 0; height: 100%; background: var(--accent); border-radius: var(--rp-progress-radius, 0); }
|
||||
.pr-meter b.is-high { background: var(--red); }
|
||||
.pr-split { display: flex; height: 6px; margin: 2px 0 4px; overflow: hidden; background: var(--line); border-radius: var(--rp-progress-radius, 0); }
|
||||
.pr-split .a { display: block; background: var(--accent); }
|
||||
.pr-split .f { display: block; background: var(--blue); }
|
||||
.pr-charts { flex: 1 1 auto; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 10px; }
|
||||
.pr-charts .mini-live-chart { display: flex; flex-direction: column; margin-top: 0; }
|
||||
.pr-charts .mini-live-chart svg:not(.ui-icon) { flex: 1 1 0; height: 0; min-height: 56px; }
|
||||
.diagnostic-line.pr-line-blue { stroke: var(--blue); }
|
||||
.diagnostic-line.pr-line-muted { stroke: var(--muted); }
|
||||
html[data-table-density=compact] .pr-row { padding-top: 6px; padding-bottom: 6px; }
|
||||
html[data-table-density=ultra] .pr-row { padding-top: 3px; padding-bottom: 3px; }
|
||||
html[data-table-density=ultra] .pr-chip { padding: 1px 7px; }
|
||||
@media (max-width: 720px) {
|
||||
[data-dashboard-widget="process_diagnostics"] { min-height: 0; }
|
||||
.pr-cols, .pr-charts { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* initial width of the cache outcome segments (set from JS via CSSOM; inline style attributes are blocked by the CSP) */
|
||||
.cache-mix-hit, .cache-mix-miss, .cache-mix-bypass, .cache-mix-other { width: 0; }
|
||||
@@ -1202,3 +1202,21 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
|
||||
.cache-target-text { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cache-target-wrap .cache-transform-badge { flex: 0 0 auto; margin-left: 0; }
|
||||
|
||||
|
||||
/* Cache pager (0.36.7): never wraps inside its frame; the heading wraps instead */
|
||||
#panel-cache .card-heading:has(.cache-pager) { flex-wrap: wrap; row-gap: 8px; }
|
||||
.cache-pager { flex-wrap: nowrap; flex: 0 0 auto; max-width: 100%; margin-left: auto; }
|
||||
.cache-pager button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; flex: 0 0 auto; }
|
||||
.cache-pager #cache-page { flex: 1 1 auto; white-space: nowrap; }
|
||||
|
||||
/* Cache pager follows the interface rounding setting: frame and the two outer buttons share the control radius */
|
||||
.cache-pager { overflow: visible; border-radius: var(--rp-radius-control, 0px); }
|
||||
.cache-pager button { border-radius: 0; }
|
||||
.cache-pager>button:first-child { border-radius: max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)); }
|
||||
.cache-pager>button:last-child { border-radius: 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)) max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0; }
|
||||
.cache-pager button:focus-visible { outline-offset: -2px; }
|
||||
|
||||
/* The global corner-style rule forces the full control radius on every button with !important; the pager is a segmented control, so only its outer corners are rounded */
|
||||
#panel-cache .cache-pager>button { border-radius: 0 !important; }
|
||||
#panel-cache .cache-pager>button:first-child { border-radius: max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)) !important; }
|
||||
#panel-cache .cache-pager>button:last-child { border-radius: 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)) max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0 !important; }
|
||||
@@ -819,7 +819,8 @@
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#pool-search {
|
||||
#pool-search,
|
||||
#bo-search {
|
||||
flex: 1 1 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
|
||||
@@ -65,8 +65,10 @@
|
||||
"cache_diagnostics",
|
||||
"extra_cold_queue_memory",
|
||||
"process_diagnostics",
|
||||
"process_simple",
|
||||
"network_diagnostics",
|
||||
"backend_table",
|
||||
"backend_tiny",
|
||||
"www_status",
|
||||
"www_kind",
|
||||
"www_capacity",
|
||||
@@ -140,7 +142,9 @@
|
||||
"extra_backends_healthy",
|
||||
"extra_error_share",
|
||||
"extra_rejected_rate",
|
||||
"extra_cert_expiry"
|
||||
"extra_cert_expiry",
|
||||
"backend_tiny",
|
||||
"process_simple"
|
||||
],
|
||||
"dashboard_density": "compact",
|
||||
"overview_samples": 600
|
||||
|
||||
@@ -705,8 +705,8 @@
|
||||
},
|
||||
{
|
||||
"id": "process_diagnostics",
|
||||
"label_key": "processDiagnostics",
|
||||
"description_key": "overviewPanelProcessDescription",
|
||||
"label_key": "processOverviewTitle",
|
||||
"description_key": "processOverviewDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"process_diagnostics\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
@@ -714,6 +714,17 @@
|
||||
"default_hidden": false,
|
||||
"display_type": "panel"
|
||||
},
|
||||
{
|
||||
"id": "process_simple",
|
||||
"label_key": "processClassicTitle",
|
||||
"description_key": "overviewPanelProcessDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"process_simple\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "panel"
|
||||
},
|
||||
{
|
||||
"id": "network_diagnostics",
|
||||
"label_key": "networkDiagnostics",
|
||||
@@ -727,8 +738,8 @@
|
||||
},
|
||||
{
|
||||
"id": "backend_table",
|
||||
"label_key": "vhHealthTitle",
|
||||
"description_key": "overviewPanelBackendsDescription",
|
||||
"label_key": "backendOverviewTitle",
|
||||
"description_key": "backendOverviewDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_table\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
@@ -736,6 +747,17 @@
|
||||
"default_hidden": false,
|
||||
"display_type": "table"
|
||||
},
|
||||
{
|
||||
"id": "backend_tiny",
|
||||
"label_key": "backendTinyTitle",
|
||||
"description_key": "backendTinyDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_tiny\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "table"
|
||||
},
|
||||
{
|
||||
"id": "www_status",
|
||||
"label_key": "webMetricStatus",
|
||||
|
||||
@@ -98,5 +98,11 @@
|
||||
run: async () => render()
|
||||
});
|
||||
|
||||
document.addEventListener("click", async event => {
|
||||
const button = event.target.closest?.("[data-editor-cancel]"); if (!button) return;
|
||||
const api = editors.get(button.dataset.editorCancel); if (!api?.isOpen?.()) return;
|
||||
if (api.isDirty?.() && !(await appConfirm(t("draftDiscardConfirm"), { danger: true, confirmLabel: t("discardAction") }))) return;
|
||||
api.close?.();
|
||||
});
|
||||
window.rustProxyDrafts = { register, dock: dockEditor, prepare, reset, refresh: render, captureControls, restoreControls, takeChildren, restoreChildren };
|
||||
})();
|
||||
+194
-81
@@ -261,9 +261,87 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card" data-dashboard-widget="process_diagnostics">
|
||||
<article class="card pr-card" data-dashboard-widget="process_diagnostics">
|
||||
<div class="mini-heading">
|
||||
<h3 data-i18n="processDiagnostics">Process</h3>
|
||||
<h3 data-i18n="processOverviewTitle">Process</h3>
|
||||
<div class="mini-heading-actions">
|
||||
<details class="metric-info">
|
||||
<summary data-i18n-aria="memoryAccountingInfo"
|
||||
aria-label="Memory accounting information">i</summary>
|
||||
<div class="metric-info-popover">
|
||||
<p class="fine" data-i18n="memoryAccountingHint">RSS is the real process
|
||||
footprint. HOT/transient cache values are RustProxy-accounted
|
||||
allocations; transform memory is a conservative active reservation.
|
||||
Non-cache RSS is RSS minus accounted cache and includes transforms,
|
||||
connections, TLS, logging, Tokio/runtime state, allocator overhead and
|
||||
other buffers, so these rows are not all additive.</p>
|
||||
</div>
|
||||
</details>
|
||||
<span class="meta" id="po-sample-age">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="pr-chips">
|
||||
<div class="pr-chip"><dt data-i18n="platform">Platform</dt><dd id="po-platform">-</dd></div>
|
||||
<div class="pr-chip"><dt data-i18n="processThreads">OS threads</dt><dd id="po-threads">-</dd></div>
|
||||
<div class="pr-chip"><dt data-i18n="processStartTime">Process start</dt><dd id="po-start">-</dd></div>
|
||||
<div class="pr-chip"><dt data-i18n="uptime">Uptime</dt><dd id="po-uptime">-</dd></div>
|
||||
</dl>
|
||||
<div class="pr-cols">
|
||||
<section>
|
||||
<h4 class="pr-title" data-i18n="processLoadTitle">Load</h4>
|
||||
<dl class="pr-list">
|
||||
<div class="pr-row"><dt data-i18n="processCpu">CPU (100% = 1 core)</dt><dd id="po-cpu">-</dd><i class="pr-meter"><b></b></i></div>
|
||||
<div class="pr-row"><dt data-i18n="processRssTotal">RSS total</dt><dd id="po-rss-total">-</dd><i class="pr-meter"><b></b></i></div>
|
||||
<div class="pr-row"><dt data-i18n="fileDescriptors">File descriptors / limit</dt><dd id="po-fds">-</dd><i class="pr-meter"><b></b></i></div>
|
||||
<div class="pr-row"><dt data-i18n="containerMemory">cgroup RAM / limit</dt><dd id="po-cgroup">-</dd><i class="pr-meter"><b></b></i></div>
|
||||
</dl>
|
||||
</section>
|
||||
<section>
|
||||
<h4 class="pr-title" data-i18n="processMemoryTitle">Memory</h4>
|
||||
<div class="pr-split" id="po-rss-split" aria-hidden="true"><i class="a"></i><i class="f"></i></div>
|
||||
<dl class="pr-list pr-plain">
|
||||
<div class="pr-row"><dt data-i18n="processRssAnon">Anonymous RSS</dt><dd id="po-rss-anon">-</dd></div>
|
||||
<div class="pr-row"><dt data-i18n="processRssFile">File-backed / shared RSS</dt><dd id="po-rss-file">-</dd></div>
|
||||
<div class="pr-row"><dt data-i18n="memoryNonCacheRss">Non-cache process RSS</dt><dd id="po-non-cache">-</dd></div>
|
||||
<div class="pr-row"><dt data-i18n="processVirtualMemory">Virtual memory / limit</dt><dd id="po-virtual">-</dd></div>
|
||||
<div class="pr-row"><dt data-i18n="processDataSwap">Data address space / swap</dt><dd id="po-data-swap">-</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
<div class="mini-live-chart-stack pr-charts">
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="processCpuHistory">Process CPU history</span><b id="po-cpu-chart-value">-</b></div>
|
||||
<svg preserveAspectRatio="none" aria-label="Process CPU history" data-i18n-aria="processCpuHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="po-cpu-sparkline" points="0,55 300,55"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="processRssHistory">Process RSS history</span><b id="po-rss-chart-value">-</b></div>
|
||||
<svg preserveAspectRatio="none" aria-label="Process RSS history" data-i18n-aria="processRssHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line process-rss-line" id="po-rss-sparkline" points="0,55 300,55"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="processFdHistory">File descriptors history</span><b id="po-fd-chart-value">-</b></div>
|
||||
<svg preserveAspectRatio="none" aria-label="File descriptors history" data-i18n-aria="processFdHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line pr-line-blue" id="po-fd-sparkline" points="0,55 300,55"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="processThreadHistory">OS threads history</span><b id="po-thread-chart-value">-</b></div>
|
||||
<svg preserveAspectRatio="none" aria-label="OS threads history" data-i18n-aria="processThreadHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line pr-line-muted" id="po-thread-sparkline" points="0,55 300,55"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card" data-dashboard-widget="process_simple">
|
||||
<div class="mini-heading">
|
||||
<h3 data-i18n="processClassicTitle">Process simple</h3>
|
||||
<div class="mini-heading-actions">
|
||||
<details class="metric-info">
|
||||
<summary data-i18n-aria="memoryAccountingInfo"
|
||||
@@ -279,78 +357,77 @@
|
||||
</details>
|
||||
<span class="meta" id="system-sample-age">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="mini-metrics">
|
||||
<div>
|
||||
<dt data-i18n="platform">Platform</dt>
|
||||
<dd id="process-platform">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processCpu">CPU (100% = 1 core)</dt>
|
||||
<dd id="process-cpu">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="fileDescriptors">File descriptors / limit</dt>
|
||||
<dd id="process-fds">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processThreads">OS threads</dt>
|
||||
<dd id="process-threads">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processVirtualMemory">Virtual memory / limit</dt>
|
||||
<dd id="process-virtual-memory">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processStartTime">Process start</dt>
|
||||
<dd id="process-start-time">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="uptime">Uptime</dt>
|
||||
<dd id="process-uptime">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processRssTotal">RSS total</dt>
|
||||
<dd id="process-rss-total">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryCacheHot">HOT cache RAM / limit</dt>
|
||||
<dd id="memory-cache-hot">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryCacheTransient">Cache transient RAM / limit</dt>
|
||||
<dd id="memory-cache-transient">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryTransform">Transform reserved RAM / budget</dt>
|
||||
<dd id="memory-transform">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryTransformInflight">Active transforms / limit</dt>
|
||||
<dd id="memory-transform-inflight">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryNonCacheRss">Non-cache process RSS</dt>
|
||||
<dd id="memory-non-cache-rss">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processRssAnon">Anonymous RSS</dt>
|
||||
<dd id="process-rss-anon">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processRssFile">File-backed / shared RSS</dt>
|
||||
<dd id="process-rss-file">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processDataSwap">Data address space / swap</dt>
|
||||
<dd id="process-data-swap">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="containerMemory">cgroup RAM / limit</dt>
|
||||
<dd id="cgroup-memory">-</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="mini-live-chart-stack">
|
||||
</d <dl class="mini-metrics">
|
||||
<div>
|
||||
<dt data-i18n="platform">Platform</dt>
|
||||
<dd id="process-platform">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processCpu">CPU (100% = 1 core)</dt>
|
||||
<dd id="process-cpu">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="fileDescriptors">File descriptors / limit</dt>
|
||||
<dd id="process-fds">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processThreads">OS threads</dt>
|
||||
<dd id="process-threads">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processVirtualMemory">Virtual memory / limit</dt>
|
||||
<dd id="process-virtual-memory">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processStartTime">Process start</dt>
|
||||
<dd id="process-start-time">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="uptime">Uptime</dt>
|
||||
<dd id="process-uptime">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processRssTotal">RSS total</dt>
|
||||
<dd id="process-rss-total">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryCacheHot">HOT cache RAM / limit</dt>
|
||||
<dd id="memory-cache-hot">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryCacheTransient">Cache transient RAM / limit</dt>
|
||||
<dd id="memory-cache-transient">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryTransform">Transform reserved RAM / budget</dt>
|
||||
<dd id="memory-transform">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryTransformInflight">Active transforms / limit</dt>
|
||||
<dd id="memory-transform-inflight">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="memoryNonCacheRss">Non-cache process RSS</dt>
|
||||
<dd id="memory-non-cache-rss">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processRssAnon">Anonymous RSS</dt>
|
||||
<dd id="process-rss-anon">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processRssFile">File-backed / shared RSS</dt>
|
||||
<dd id="process-rss-file">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="processDataSwap">Data address space / swap</dt>
|
||||
<dd id="process-data-swap">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt data-i18n="containerMemory">cgroup RAM / limit</dt>
|
||||
<dd id="cgroup-memory">-</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="mini-live-chart-stack">
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span data-i18n="processCpuHistory">Process CPU
|
||||
history</span><b id="process-cpu-chart-value">-</b></div>
|
||||
@@ -433,10 +510,46 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card pool-card" id="backend-diagnostics-card"
|
||||
<article class="card pool-card backend-overview-card" id="backend-overview-card"
|
||||
data-dashboard-widget="backend_table">
|
||||
<div class="card-heading backend-card-heading">
|
||||
<h2 data-i18n="vhHealthTitle">Backends</h2>
|
||||
<div class="bo-heading">
|
||||
<h2 data-i18n="backendOverviewTitle">Backends</h2>
|
||||
<span class="bo-summary" id="bo-summary" aria-live="polite"></span>
|
||||
</div>
|
||||
<div class="backend-table-tools">
|
||||
<label class="sr-only" data-i18n="backendSearch" for="bo-search">Search
|
||||
backends</label>
|
||||
<input autocomplete="off" data-i18n-aria="backendSearch"
|
||||
data-i18n-placeholder="backendSearchPlaceholder" id="bo-search"
|
||||
type="search" />
|
||||
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
|
||||
by</span><select data-i18n-aria="backendSortBy" id="bo-sort"></select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="bo-sort-direction" type="button">↑</button>
|
||||
<details class="backend-column-picker">
|
||||
<summary class="quiet" data-i18n="backendColumns">Columns</summary>
|
||||
<div class="backend-column-options" id="bo-column-options"></div>
|
||||
</details>
|
||||
<button class="quiet" data-i18n="vhManage" id="bo-open" type="button">Manage
|
||||
vhosts</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bo-filters" id="bo-filters" role="group"></div>
|
||||
<div class="table-scroll">
|
||||
<table class="bo-table">
|
||||
<thead>
|
||||
<tr id="bo-head"></tr>
|
||||
</thead>
|
||||
<tbody id="bo-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="fine" data-i18n="vhHealthEmpty" id="bo-empty">No backend diagnostics yet.</p>
|
||||
</article>
|
||||
<article class="card pool-card" id="backend-diagnostics-card"
|
||||
data-dashboard-widget="backend_tiny">
|
||||
<div class="card-heading backend-card-heading">
|
||||
<h2 data-i18n="backendTinyTitle">Tiny backends</h2>
|
||||
<div class="backend-table-tools">
|
||||
<label class="sr-only" data-i18n="backendSearch" for="pool-search">Search
|
||||
backends</label>
|
||||
@@ -4120,7 +4233,7 @@
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="primary" data-i18n="apply" type="submit">Validate, save and apply</button>
|
||||
<button class="quiet" data-i18n="vhReset" id="web-service-reset" type="button">Discard changes</button>
|
||||
<button class="quiet" data-i18n="vhReset" id="web-service-reset" type="button">Discard changes</button><button class="quiet" data-editor-cancel="web-server" data-i18n="vhCancel" type="button">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
@@ -4267,7 +4380,7 @@ log_errors = On</code></pre>
|
||||
<div id="vhost-fields"></div>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply" id="vhost-save" type="submit">Validate,
|
||||
save and apply</button><button class="quiet" data-i18n="vhReset" id="vhost-reset"
|
||||
type="button">Discard changes</button></div>
|
||||
type="button">Discard changes</button><button class="quiet" data-editor-cancel="vhost" data-i18n="vhCancel" type="button">Cancel</button></div>
|
||||
<p class="fine" data-i18n="vhSafetyHint">Private IP binding is not authentication. Keep firewall/ACL rules.
|
||||
HSTS preload is opt-in; affinity cookies are not login sessions.</p>
|
||||
</form>
|
||||
@@ -4332,7 +4445,7 @@ log_errors = On</code></pre>
|
||||
</section>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply" type="submit">Validate, save and
|
||||
apply</button><button class="quiet" data-i18n="vhReset" id="redirect-reset" type="button">Discard
|
||||
changes</button></div>
|
||||
changes</button><button class="quiet" data-editor-cancel="redirect" data-i18n="vhCancel" type="button">Cancel</button></div>
|
||||
<p class="fine" data-i18n="redirectSafetyHint">Redirect hosts do not contact a backend. Global host ACLs
|
||||
still apply. Avoid self-referential rules that can create browser redirect loops.</p>
|
||||
</form>
|
||||
@@ -4403,7 +4516,7 @@ log_errors = On</code></pre>
|
||||
</section>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply" type="submit">Validate, save and
|
||||
apply</button><button class="quiet" data-i18n="vhReset" id="notfound-reset" type="button">Discard
|
||||
changes</button></div>
|
||||
changes</button><button class="quiet" data-editor-cancel="notfound" data-i18n="vhCancel" type="button">Cancel</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@@ -4419,7 +4532,7 @@ log_errors = On</code></pre>
|
||||
<div id="stream-fields"></div>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply" id="stream-save" type="submit">Validate,
|
||||
save and apply</button><button class="quiet" data-i18n="vhReset" id="stream-reset"
|
||||
type="button">Discard changes</button></div>
|
||||
type="button">Discard changes</button><button class="quiet" data-editor-cancel="stream" data-i18n="vhCancel" type="button">Cancel</button></div>
|
||||
<p class="fine" data-i18n="streamRestartHelp">Changing a stream listener requires a RustProxy restart.</p>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
function addRow(item = { id: `listener-${$("named-listener-rows").children.length + 1}`, name: "", scope: "internal", enabled: true, binding: { address: "127.0.0.1:8181", tls: false, http2: false, http2_settings: null, h2c: false, http3: null } }) {
|
||||
if ($("named-listener-rows").children.length >= 64) { toast(t("listenerLimit"), true); return; }
|
||||
const row = el("fieldset", undefined, "named-listener-row"); row.append(label("legend", "listenerEndpoint")); const grid = el("div", undefined, "vh-grid"); row.append(grid); const b = item.binding || {};
|
||||
const id = field(grid, "listener-id", "listenerId", item.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" });
|
||||
const id = field(grid, "listener-id", "listenerId", item.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_\\-]{1,64}" });
|
||||
const name = field(grid, "listener-name", "listenerName", item.name, "text", { maxLength: 96 });
|
||||
const scope = field(grid, "listener-scope", "listenerScope", item.scope || "internal", "select", { values: [["internal", "listenerInternal"], ["public", "listenerPublic"]] });
|
||||
const currentAddresses = [b.address, ...(item.additional_addresses || [])].filter(Boolean);
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<strong id="cache-mix-headline">-</strong>
|
||||
<div class="cache-mix-bar" id="cache-mix-bar" role="img"
|
||||
aria-label="Cumulative HTTP cache outcomes since service start">
|
||||
<span class="cache-mix-hit" id="cache-mix-hit" style="width:0%"></span>
|
||||
<span class="cache-mix-miss" id="cache-mix-miss" style="width:0%"></span>
|
||||
<span class="cache-mix-bypass" id="cache-mix-bypass" style="width:0%"></span>
|
||||
<span class="cache-mix-other" id="cache-mix-other" style="width:0%"></span>
|
||||
<span class="cache-mix-hit" id="cache-mix-hit"></span>
|
||||
<span class="cache-mix-miss" id="cache-mix-miss"></span>
|
||||
<span class="cache-mix-bypass" id="cache-mix-bypass"></span>
|
||||
<span class="cache-mix-other" id="cache-mix-other"></span>
|
||||
</div>
|
||||
<small class="cache-mix-legend">
|
||||
<span class="cache-mix-hit-key" id="cache-mix-hit-key"><i></i>HIT <b id="cache-mix-hit-count">-</b></span>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
return grid;
|
||||
};
|
||||
const connectionFields = backendSection("streamBackendConnectionSection");
|
||||
field(connectionFields, "backend-id", "streamBackendId", backend.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" });
|
||||
field(connectionFields, "backend-id", "streamBackendId", backend.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_\\-]{1,64}" });
|
||||
field(connectionFields, "backend-name", "streamBackendName", backend.name || "", "text", { maxLength: 96 });
|
||||
field(connectionFields, "backend-address", "streamBackendAddress", backend.address, "text", { required: true, maxLength: 128 });
|
||||
field(connectionFields, "backend-weight", "streamBackendWeight", backend.weight ?? 1, "number", { min: 1, max: 16, required: true });
|
||||
@@ -128,7 +128,7 @@
|
||||
$("stream-editing").textContent = t(editing ? "streamEditLabel" : "streamNewLabel");
|
||||
let g = group("streamEditor", "details");
|
||||
const streamName = field(g, "name", "streamName", value.name, "text", { maxLength: 96 });
|
||||
const streamIdInput = field(g, "id", "streamId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}", readOnly: Boolean(editing) });
|
||||
const streamIdInput = field(g, "id", "streamId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_\\-]{1,64}", readOnly: Boolean(editing) });
|
||||
window.rustProxyAutoId.attach(streamIdInput, { elements: [streamName], taken: () => (envelope?.config.streams || []).map(item => item.id) });
|
||||
const protocol = field(g, "protocol", "streamProtocol", value.protocol || "tcp", "select", { values: [["tcp", "streamTcp"], ["udp", "streamUdp"]] });
|
||||
const enabledControl = field(g, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
|
||||
|
||||
@@ -203,7 +203,7 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
identity.append(title, endpoint); head.append(identity); const remove = button("remove", () => { row.remove(); backendHealthEditorControl?.refresh(); changed(); }); head.append(remove); row.append(head);
|
||||
|
||||
const main = el("div", undefined, "vh-grid vh-backend-main"); row.append(main);
|
||||
const id = field(main, "member-id", "vhMemberId", value.id, "text", { pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
|
||||
const id = field(main, "member-id", "vhMemberId", value.id, "text", { pattern: "[A-Za-z0-9_\\-]{1,64}", maxLength: 64 });
|
||||
const url = field(main, "member-url", "vhUrl", value.url, "url", { maxLength: 512 });
|
||||
field(main, "member-weight", "vhWeight", value.weight ?? 1, "number", { required: true, min: 1, max: 256 });
|
||||
const protocol = field(main, "member-protocol", "vhProtocol", value.protocol || "http1", "select", { values: [["http1", "vhProtocolHttp1"], ["auto", "vhProtocolAuto"], ["http2", "vhProtocolHttp2"], ["h2", "vhProtocolH2Strict"]] });
|
||||
@@ -379,7 +379,7 @@ function locationsEditor(parent, values) {
|
||||
const row = el("details", undefined, "vh-location"); row.open = true;
|
||||
const summary = el("summary", undefined, "location-summary"), body = el("div", undefined, "vh-grid location-body"); row.append(summary, body);
|
||||
const used = new Set([...rows.children].map(r => str("location-id", r))); let suffix = 1; while (used.has(`location-${suffix}`)) suffix++;
|
||||
field(body, "location-id", "locationId", value.id || `location-${suffix}`, "text", { required: true, pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
|
||||
field(body, "location-id", "locationId", value.id || `location-${suffix}`, "text", { required: true, pattern: "[A-Za-z0-9_\\-]{1,64}", maxLength: 64 });
|
||||
const match = field(body, "location-match", "locationMatchType", value.match_type || "prefix", "select", { values: [["prefix", "locationMatchPrefix"], ["prefix_priority", "locationMatchPriority"], ["exact", "locationMatchExact"], ["regex", "locationMatchRegex"], ["regex_insensitive", "locationMatchRegexI"]] });
|
||||
const path = field(body, "location-path", "locationPathPattern", value.path_prefix || "/", "text", { required: true, maxLength: 1024 }); path.placeholder = "/api or ^/api/v[0-9]+/";
|
||||
const internal = field(body, "location-internal", "locationInternal", value.internal === true, "checkbox");
|
||||
@@ -391,7 +391,7 @@ function locationsEditor(parent, values) {
|
||||
const cache = field(body, "location-cache", "cacheEnabled", value.cache === true ? "on" : value.cache === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
|
||||
|
||||
const handling = el("details", undefined, "vh-wide location-section"), handlingBody = el("div", undefined, "vh-grid"); handling.append(text("summary", "locationHandling"), handlingBody); body.append(handling);
|
||||
field(handlingBody, "location-error-target", "locationErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_-]{0,64}" });
|
||||
field(handlingBody, "location-error-target", "locationErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_\\-]{0,64}" });
|
||||
field(handlingBody, "location-error-statuses", "locationErrorFallbackStatuses", (value.error_fallback?.statuses || [404]).join(", "), "text", { maxLength: 64 });
|
||||
note(handlingBody, "locationErrorFallbackHelp");
|
||||
const rewriteMode = field(handlingBody, "location-rewrite-mode", "locationRewriteMode", value.rewrite?.mode || "preserve", "select", { values: [["preserve", "locationRewritePreserve"], ["strip_prefix", "locationRewriteStrip"], ["replace_prefix", "locationRewriteReplace"], ["regex", "locationRewriteRegex"]] });
|
||||
|
||||
@@ -69,7 +69,7 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
editing = value.id || null; revision = envelope?.revision ?? null; dirty = false; initialized = true;
|
||||
container.replaceChildren(); $("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
|
||||
let g = group("vhEditor"); const basicFields = subsection(g, "vhGeneralDetailsSection");
|
||||
const vhostIdInput = field(basicFields, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_-]{1,64}" }); if (editing) note(basicFields, "vhIdRenameHelp");
|
||||
const vhostIdInput = field(basicFields, "id", "vhId", value.id, "text", { required: true, maxLength: 64, pattern: "[A-Za-z0-9_\\-]{1,64}" }); if (editing) note(basicFields, "vhIdRenameHelp");
|
||||
let refreshCanonical = () => { }, vhostAutoId = null, vhostAutoCertificate = null;
|
||||
vhostAutoId = window.rustProxyAutoId.attach(vhostIdInput, { text: () => lines(read("hosts")?.value || "")[0] || "", taken: () => (envelope?.config.vhosts || []).map(item => item.id), resettable: !editing });
|
||||
const enabledControl = field(basicFields, "enabled", "vhEnabled", value.enabled !== false, "checkbox");
|
||||
@@ -271,7 +271,7 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
passiveSlow.addEventListener("change", syncPassive);
|
||||
syncPassive();
|
||||
g = group("vhSticky"); field(g, "sticky-mode", "vhStickyMode", sticky.mode || "off", "select", { values: [["off", "vhOff"], ["cookie", "vhCookie"], ["ip_hash", "vhIpHash"]] });
|
||||
field(g, "cookie-name", "vhCookieName", sticky.cookie_name || "", "text", { maxLength: 96, pattern: "[A-Za-z0-9_-]*" });
|
||||
field(g, "cookie-name", "vhCookieName", sticky.cookie_name || "", "text", { maxLength: 96, pattern: "[A-Za-z0-9_\\-]*" });
|
||||
field(g, "cookie-ttl", "vhCookieTtl", sticky.ttl_seconds ?? 3600, "number", { required: true, min: 60, max: 604800 }); note(g, "vhStickyNote");
|
||||
const rate = value.rate_limit || {}, access = rate.country_access || {}, methodPolicy = value.method_policy || {};
|
||||
g = group("vhTrafficPolicy");
|
||||
@@ -342,7 +342,7 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
field(g, "acl", "vhAcl", JSON.stringify(value.acl ?? null, null, 2), "textarea", { rows: 5, maxLength: 32768 });
|
||||
field(g, "headers", "vhHeaders", JSON.stringify(value.headers || {}, null, 2), "textarea", { rows: 5, maxLength: 65536 });
|
||||
g = group("vhLocations");
|
||||
field(g, "main-error-target", "vhErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_-]{0,64}" });
|
||||
field(g, "main-error-target", "vhErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_\\-]{0,64}" });
|
||||
field(g, "main-error-statuses", "vhErrorFallbackStatuses", (value.error_fallback?.statuses || [404]).join(", "), "text", { maxLength: 64 });
|
||||
note(g, "vhErrorFallbackHelp");
|
||||
locationsReader = locationsEditor(g, value.routes || []);
|
||||
|
||||
+148
-10
@@ -202,7 +202,7 @@ async function savePoolTablePrefs() {
|
||||
if (!authenticated) return;
|
||||
if (!poolPreferenceSaveQueued) poolTablePrefs = normalizePoolTablePrefs(saved);
|
||||
}
|
||||
syncPoolColumns(); syncPoolSortDirection();
|
||||
syncPoolColumns(); syncPoolSortDirection(); applyBackendOverviewView();
|
||||
} catch (error) { if (error.status !== 401) toast(error.message, true); }
|
||||
finally { poolPreferenceSaveRunning = false; }
|
||||
}
|
||||
@@ -212,11 +212,15 @@ function syncPoolColumns() {
|
||||
document.querySelectorAll("[data-pool-column-toggle]").forEach(input => { input.checked = visible.has(input.dataset.poolColumnToggle); });
|
||||
}
|
||||
function syncPoolSortDirection() {
|
||||
const control = $("pool-sort-direction"); if (!control) return;
|
||||
control.textContent = poolTablePrefs.descending ? "↓" : "↑";
|
||||
control.setAttribute("aria-label", t(poolTablePrefs.descending ? "backendSortDescending" : "backendSortAscending"));
|
||||
for (const id of ["pool-sort-direction", "bo-sort-direction"]) {
|
||||
const control = $(id); if (!control) continue;
|
||||
control.textContent = poolTablePrefs.descending ? "↓" : "↑";
|
||||
control.setAttribute("aria-label", t(poolTablePrefs.descending ? "backendSortDescending" : "backendSortAscending"));
|
||||
}
|
||||
}
|
||||
function applyPoolTableView() {
|
||||
for (const id of ["pool-sort", "bo-sort"]) { const control = $(id); if (control && control.value !== poolTablePrefs.sort) control.value = poolTablePrefs.sort; }
|
||||
applyBackendOverviewView();
|
||||
const body = $("pool-rows"); if (!body) return;
|
||||
const query = ($("pool-search")?.value || "").trim().toLowerCase();
|
||||
const key = $("pool-sort")?.value || poolTablePrefs.sort;
|
||||
@@ -234,18 +238,142 @@ function applyPoolTableView() {
|
||||
if (empty) { empty.textContent = query && rows.length && !visible ? t("backendSearchEmpty") : t("vhHealthEmpty"); empty.hidden = visible > 0 || (!rows.length && !document.querySelector("#backend-diagnostics-card:not([hidden])")); }
|
||||
}
|
||||
function initPoolTableTools() {
|
||||
const sort = $("pool-sort"), direction = $("pool-sort-direction"), search = $("pool-search");
|
||||
if (sort) { sort.value = poolTablePrefs.sort; sort.addEventListener("change", () => { poolTablePrefs.sort = sort.value; void savePoolTablePrefs(); applyPoolTableView(); }); }
|
||||
direction?.addEventListener("click", () => { poolTablePrefs.descending = !poolTablePrefs.descending; void savePoolTablePrefs(); applyPoolTableView(); });
|
||||
search?.addEventListener("input", applyPoolTableView);
|
||||
buildBackendOverviewTools();
|
||||
for (const id of ["pool-sort", "bo-sort"]) {
|
||||
const sort = $(id); if (!sort) continue;
|
||||
sort.value = poolTablePrefs.sort;
|
||||
sort.addEventListener("change", () => { poolTablePrefs.sort = sort.value; void savePoolTablePrefs(); applyPoolTableView(); });
|
||||
}
|
||||
for (const id of ["pool-sort-direction", "bo-sort-direction"]) $(id)?.addEventListener("click", () => { poolTablePrefs.descending = !poolTablePrefs.descending; void savePoolTablePrefs(); applyPoolTableView(); });
|
||||
$("pool-search")?.addEventListener("input", applyPoolTableView);
|
||||
$("bo-search")?.addEventListener("input", applyBackendOverviewView);
|
||||
$("bo-open")?.addEventListener("click", () => setPanel("vhosts"));
|
||||
document.querySelectorAll("[data-pool-column-toggle]").forEach(input => input.addEventListener("change", () => {
|
||||
const visible = new Set(poolTablePrefs.columns);
|
||||
if (input.checked) visible.add(input.dataset.poolColumnToggle); else visible.delete(input.dataset.poolColumnToggle);
|
||||
if (!visible.size) { input.checked = true; toast(t("backendColumnsRequired"), true); return; }
|
||||
poolTablePrefs.columns = poolColumns.filter(key => visible.has(key)); void savePoolTablePrefs(); syncPoolColumns();
|
||||
poolTablePrefs.columns = poolColumns.filter(key => visible.has(key)); void savePoolTablePrefs(); syncPoolColumns(); applyBackendOverviewView();
|
||||
}));
|
||||
syncPoolColumns(); syncPoolSortDirection();
|
||||
}
|
||||
// Backends overview (dashboard card "backend_table"): a richer view of the same backend data. It shares the sort,
|
||||
// direction and column preferences with the compact "Tiny backends" table and follows table density and corner style.
|
||||
const BO_SORT_LABELS = { vhost: "vhVhost", backend: "backend", status: "status", active: "vhActive", requests: "vhRequests", errors: "vhErrors", latency: "vhLatency", traffic: "vhTraffic", generation: "vhClientGeneration", generationRequests: "vhGenerationRequests", generationAge: "vhGenerationAge", rotations: "vhClientRotations", lastFailure: "vhLastFailure", lastRecovery: "vhLastRecovery" };
|
||||
const BO_DETAIL_COLUMNS = ["generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
|
||||
const BO_LATENCY_SAMPLES = 24;
|
||||
const boOpen = new Set(), boLatency = new Map();
|
||||
let boItems = [], boFilter = "all", boHasProxy = true;
|
||||
function buildBackendOverviewTools() {
|
||||
const sort = $("bo-sort"), options = $("bo-column-options");
|
||||
if (sort && !sort.options.length) for (const key of poolColumns) { const option = el("option", t(BO_SORT_LABELS[key])); option.value = key; option.dataset.i18n = BO_SORT_LABELS[key]; sort.append(option); }
|
||||
if (options && !options.children.length) for (const key of poolColumns) {
|
||||
const label = el("label", undefined, "check"), input = el("input"), text = el("span", t(BO_SORT_LABELS[key]));
|
||||
input.type = "checkbox"; input.dataset.poolColumnToggle = key; text.dataset.i18n = BO_SORT_LABELS[key]; label.append(input, text); options.append(label);
|
||||
}
|
||||
}
|
||||
function boAgo(ms) {
|
||||
const seconds = Math.max(0, Math.floor((Date.now() - Number(ms)) / 1000));
|
||||
const value = seconds < 60 ? `${seconds} s` : seconds < 3600 ? `${Math.floor(seconds / 60)} min` : seconds < 86400 ? `${Math.floor(seconds / 3600)} h` : `${Math.floor(seconds / 86400)} d`;
|
||||
return t("backendAgo").replace("{value}", value);
|
||||
}
|
||||
function boSparkline(history, current) {
|
||||
const ns = "http://www.w3.org/2000/svg", svg = document.createElementNS(ns, "svg"), samples = history.filter(value => value > 0);
|
||||
svg.setAttribute("viewBox", "0 0 64 20"); svg.setAttribute("class", "bo-spark"); svg.setAttribute("aria-hidden", "true");
|
||||
if (!(current > 0) || samples.length < 3) return svg;
|
||||
const min = Math.min(...samples), max = Math.max(...samples), flat = max - min < max * 0.05, step = 64 / (samples.length - 1), line = document.createElementNS(ns, "polyline");
|
||||
line.setAttribute("points", samples.map((value, index) => `${(index * step).toFixed(1)},${(flat ? 10 : 17 - ((value - min) / (max - min)) * 14).toFixed(1)}`).join(" "));
|
||||
svg.append(line); return svg;
|
||||
}
|
||||
function renderBackendOverview(items, hasProxy) {
|
||||
boItems = items; boHasProxy = hasProxy !== false;
|
||||
const seen = new Set();
|
||||
for (const item of items) {
|
||||
seen.add(item.key);
|
||||
const history = boLatency.get(item.key) || []; history.push(item.latency); if (history.length > BO_LATENCY_SAMPLES) history.shift(); boLatency.set(item.key, history);
|
||||
}
|
||||
for (const key of boLatency.keys()) if (!seen.has(key)) boLatency.delete(key);
|
||||
for (const key of boOpen) if (!seen.has(key)) boOpen.delete(key);
|
||||
$("backend-overview-card")?.classList.toggle("is-no-proxy", !boHasProxy);
|
||||
applyBackendOverviewView();
|
||||
}
|
||||
function applyBackendOverviewView() {
|
||||
const body = $("bo-rows"); if (!body) return;
|
||||
const visibleColumns = new Set(poolTablePrefs.columns);
|
||||
const query = ($("bo-search")?.value || "").trim().toLowerCase();
|
||||
const focusKey = document.activeElement?.dataset?.boKey, hadFocus = Boolean(focusKey) && body.contains(document.activeElement);
|
||||
const counts = { all: boItems.length, up: 0, down: 0, other: 0 };
|
||||
for (const item of boItems) counts[item.group]++;
|
||||
const summary = $("bo-summary"); if (summary) summary.textContent = boItems.length ? t("backendSummary").replace("{up}", counts.up).replace("{total}", boItems.length) : "";
|
||||
const filters = $("bo-filters");
|
||||
if (filters) {
|
||||
filters.setAttribute("aria-label", t("backendFilterLabel"));
|
||||
filters.replaceChildren(...[["all", "backendFilterAll"], ["up", "vhUp"], ["down", "vhDown"], ["other", "backendFilterOther"]].map(([key, label]) => {
|
||||
const chip = el("button", undefined, `bo-chip is-${key}`), count = el("b", String(counts[key]), counts[key] ? "" : "is-zero");
|
||||
chip.type = "button"; chip.dataset.uiIconName = "none"; chip.setAttribute("aria-pressed", String(boFilter === key));
|
||||
chip.append(el("span", t(label)), count); chip.addEventListener("click", () => { boFilter = key; applyBackendOverviewView(); }); return chip;
|
||||
}));
|
||||
}
|
||||
const key = poolTablePrefs.sort;
|
||||
const list = boItems.filter(item => (boFilter === "all" || item.group === boFilter) && (!query || item.search.includes(query)));
|
||||
list.sort((left, right) => {
|
||||
const a = left.sort[key] ?? "", b = right.sort[key] ?? "";
|
||||
const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" });
|
||||
return poolTablePrefs.descending ? -result : result;
|
||||
});
|
||||
const showIdentity = visibleColumns.has("vhost") || visibleColumns.has("backend"), showEvent = visibleColumns.has("lastFailure") || visibleColumns.has("lastRecovery");
|
||||
const detailColumns = BO_DETAIL_COLUMNS.filter(column => visibleColumns.has(column)), expandable = detailColumns.length > 0;
|
||||
const columns = [
|
||||
expandable && ["toggle", ""], showIdentity && ["identity", `${visibleColumns.has("vhost") ? t("vhVhost") : ""}${visibleColumns.has("vhost") && visibleColumns.has("backend") ? " / " : ""}${visibleColumns.has("backend") ? t("backend") : ""}`],
|
||||
visibleColumns.has("status") && ["status", t("status")], visibleColumns.has("active") && ["active", t("vhActive"), true], visibleColumns.has("requests") && ["requests", t("vhRequests"), true],
|
||||
visibleColumns.has("errors") && ["errors", t("vhErrors")], visibleColumns.has("latency") && ["latency", t("vhLatency")], visibleColumns.has("traffic") && ["traffic", t("vhTraffic"), true], showEvent && ["event", t("backendLastEvent")],
|
||||
].filter(Boolean);
|
||||
const head = $("bo-head"); if (head) head.replaceChildren(...columns.map(([id, label, numeric]) => { const th = el("th", label, numeric ? "is-num" : ""); th.dataset.boColumn = id; if (id === "toggle") th.setAttribute("aria-label", t("backendDetailsToggle")); return th; }));
|
||||
const rows = [];
|
||||
for (const item of list) {
|
||||
const row = el("tr", undefined, `bo-row is-${item.tone}${item.disabled ? " is-disabled" : ""}`), open = expandable && boOpen.has(item.key);
|
||||
for (const [id] of columns) {
|
||||
const cell = el("td"); cell.dataset.boColumn = id;
|
||||
if (id === "toggle") {
|
||||
const button = el("button", undefined, "bo-expand"); button.type = "button"; button.dataset.uiIconName = "none"; button.dataset.boKey = item.key;
|
||||
button.setAttribute("aria-expanded", String(open)); button.setAttribute("aria-label", t("backendDetailsToggle")); button.title = t("backendDetailsToggle");
|
||||
button.addEventListener("click", () => { if (boOpen.has(item.key)) boOpen.delete(item.key); else boOpen.add(item.key); applyBackendOverviewView(); });
|
||||
cell.append(button);
|
||||
} else if (id === "identity") {
|
||||
cell.className = "bo-identity"; const name = el("div", visibleColumns.has("vhost") ? item.vhost : item.backend, "bo-name"); name.title = item.vhost; cell.append(name);
|
||||
if (visibleColumns.has("vhost") && visibleColumns.has("backend")) { const sub = el("div", item.backend, "bo-sub"); sub.title = item.backend; cell.append(sub); }
|
||||
} else if (id === "status") {
|
||||
const badge = el("span", item.label, `bo-badge is-${item.tone}`); badge.title = item.title; badge.prepend(el("i")); cell.append(badge);
|
||||
} else if (id === "active") { cell.className = "is-num"; cell.textContent = item.activeText; }
|
||||
else if (id === "requests") { cell.className = "is-num"; cell.textContent = String(item.requests); }
|
||||
else if (id === "errors") {
|
||||
const pct = item.requests ? item.errors / item.requests * 100 : 0, top = el("div", undefined, "bo-meter-top"), track = el("div", undefined, "bo-meter"), fill = el("i");
|
||||
top.append(el("span", String(item.errors), item.errors ? "" : "is-zero"), el("small", item.errors ? (pct < 0.1 ? "<0.1%" : `${pct.toFixed(pct < 10 ? 1 : 0)}%`) : "0%"));
|
||||
fill.style.width = `${item.errors ? Math.min(100, Math.max(4, pct)) : 0}%`; if (pct >= 5) fill.className = "is-high"; track.append(fill); cell.className = "bo-errors"; cell.append(top, track);
|
||||
} else if (id === "latency") {
|
||||
cell.className = "bo-latency"; cell.append(el("span", item.latency > 0 ? `${item.latency.toFixed(1)} ms` : "-", item.latency > 0 ? "" : "is-zero"), boSparkline(boLatency.get(item.key) || [], item.latency));
|
||||
} else if (id === "traffic") { cell.className = "is-num"; cell.textContent = `${rateBytes(item.traffic)}/s`; }
|
||||
else if (id === "event") {
|
||||
cell.className = "bo-event";
|
||||
if (item.lastFailureMs && item.lastFailureMs >= item.lastRecoveryMs) { const line = el("span", item.lastFailureClass || t("backendEventFailed"), "is-failed"); line.title = item.lastFailureDetail || ""; cell.append(line, el("small", boAgo(item.lastFailureMs))); }
|
||||
else if (item.lastRecoveryMs) cell.append(el("span", t("backendEventRecovered"), "is-recovered"), el("small", boAgo(item.lastRecoveryMs)));
|
||||
else cell.append(el("span", t("backendEventNone"), "is-zero"));
|
||||
}
|
||||
row.append(cell);
|
||||
}
|
||||
rows.push(row);
|
||||
if (open) {
|
||||
const detail = el("tr", undefined, "bo-detail-row"), cell = el("td"), grid = el("div", undefined, "bo-detail-grid"); cell.colSpan = Math.max(1, columns.length);
|
||||
const values = { generation: item.generation, generationRequests: item.generationRequests, generationAge: item.generationAgeText, rotations: item.rotations, lastFailure: item.lastFailureText, lastRecovery: item.lastRecoveryText };
|
||||
for (const column of detailColumns) { const entry = el("div", undefined, "bo-detail-item"), value = el("b", String(values[column] ?? "-")); if (column === "lastFailure" && item.lastFailureMs) { value.className = "is-failed"; value.title = item.lastFailureDetail || ""; } else if (column === "lastRecovery" && item.lastRecoveryMs) value.className = "is-recovered"; entry.append(el("span", t(BO_SORT_LABELS[column])), value); grid.append(entry); }
|
||||
cell.append(grid); detail.append(cell); rows.push(detail);
|
||||
}
|
||||
}
|
||||
body.replaceChildren(...rows);
|
||||
if (hadFocus) [...body.querySelectorAll(".bo-expand")].find(button => button.dataset.boKey === focusKey)?.focus();
|
||||
const empty = $("bo-empty");
|
||||
if (empty) { empty.textContent = boItems.length ? t("backendSearchEmpty") : t("vhHealthEmpty"); empty.hidden = list.length > 0 || !boHasProxy; }
|
||||
const toggleAll = document.querySelectorAll("#bo-column-options [data-pool-column-toggle]"); toggleAll.forEach(input => { input.checked = visibleColumns.has(input.dataset.poolColumnToggle); });
|
||||
}
|
||||
async function toggleVhostEnabled(vhost) {
|
||||
if (!envelope || busy) return;
|
||||
const next = structuredClone(envelope.config), target = (next.vhosts || []).find(item => item.id === vhost.id);
|
||||
@@ -287,6 +415,7 @@ function render() {
|
||||
);
|
||||
const diagnosticsCard = $("backend-diagnostics-card");
|
||||
if (diagnosticsCard) diagnosticsCard.hidden = !configuredHasProxySource;
|
||||
$("backend-overview-card")?.classList.toggle("is-no-proxy", !configuredHasProxySource);
|
||||
const query = ($("vhost-search")?.value || "").trim().toLowerCase();
|
||||
const visibleVhosts = (config.vhosts || []).filter(v => {
|
||||
if (!query) return true;
|
||||
@@ -459,7 +588,7 @@ function renderStatus(status) {
|
||||
renderUpstreamProtocolBadges(status);
|
||||
refreshVhostBackendPopup(status);
|
||||
form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.());
|
||||
const rows = [], summaries = new Map(), aggregate = new Map();
|
||||
const rows = [], summaries = new Map(), aggregate = new Map(), overviewItems = [];
|
||||
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set();
|
||||
const statusKeys = { up: "vhUp", down: "vhDown", checking: "vhChecking", draining: "vhDraining", disabled: "vhDisabled" };
|
||||
const configuredVhosts = new Map((envelope?.config.vhosts || []).map(vhost => [vhost.id, vhost]));
|
||||
@@ -481,6 +610,14 @@ function renderStatus(status) {
|
||||
const sortValues = { vhost: poolLabel.toLowerCase(), backend: String(member.id || "").toLowerCase(), status: label.toLowerCase(), active: Number(member.active) || 0, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic, generation: Number(member.upstream_client_generation) || 0, generationRequests: Number(member.upstream_client_generation_requests) || 0, generationAge: Number.isFinite(generationAge) ? generationAge : -1, rotations: Number(member.upstream_client_rotations_total) || 0, lastFailure: Number(member.last_failure_at_ms) || 0, lastRecovery: Number(member.last_recovered_ms) || 0 };
|
||||
row.poolSort = sortValues; row.poolSearchValue = [poolLabel, member.id, label, member.status, member.last_failure_class, member.last_failure_detail].filter(Boolean).join(" ").toLowerCase();
|
||||
cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); cell.dataset.poolColumn = poolColumns[index]; cell.title = index === 2 ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : index === 12 ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (index === 2 && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (index === 12 && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row);
|
||||
const tone = vhostDisabled ? "muted" : member.health_mode === "off" && member.status === "up" ? "muted" : member.status === "up" ? "up" : member.status === "down" ? "down" : member.status === "checking" || member.status === "draining" ? "warn" : "muted";
|
||||
overviewItems.push({
|
||||
key: `${pool.id}\u0000${member.id}`, vhost: poolLabel, backend: `${member.id}${member.backup ? " / " + t("vhBackup") : ""}`, disabled: vhostDisabled, tone, group: tone === "up" ? "up" : tone === "down" ? "down" : "other",
|
||||
label, title: vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`, activeText: activeLimit, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic,
|
||||
generation: member.upstream_client_generation ?? "-", generationRequests: member.upstream_client_generation_requests ?? "-", generationAgeText, rotations: member.upstream_client_rotations_total ?? "-",
|
||||
lastFailureMs: Number(member.last_failure_at_ms) || 0, lastFailureClass: member.last_failure_class || "", lastFailureDetail: member.last_failure_detail || "", lastFailureText: lastFailure, lastRecoveryMs: Number(member.last_recovered_ms) || 0, lastRecoveryText: lastRecovery,
|
||||
sort: sortValues, search: row.poolSearchValue,
|
||||
});
|
||||
if (!vhostDisabled && member.status === "up") up++; active += Number(member.active) || 0; requests += Number(member.requests) || 0; errors += Number(member.errors) || 0; traffic += memberTraffic;
|
||||
const latency = Number(member.ewma_latency_ms || member.latency_ms) || 0; if (latency > 0) { const weight = Math.max(1, Number(member.requests) || 1); weightedLatency += latency * weight; latencyWeight += weight; }
|
||||
}
|
||||
@@ -524,6 +661,7 @@ function renderStatus(status) {
|
||||
vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy")
|
||||
);
|
||||
$("pool-rows").replaceChildren(...rows);
|
||||
renderBackendOverview(overviewItems, hasConfiguredProxySource);
|
||||
const backendDiagnosticsCard = $("backend-diagnostics-card");
|
||||
if (backendDiagnosticsCard) backendDiagnosticsCard.hidden = !hasConfiguredProxySource;
|
||||
applyPoolTableView();
|
||||
|
||||
Reference in new issue
Block a user