This commit is contained in:
Mateusz Gruszczyński committed 2026-10-24 00:05:09 +02:00
1 parent 63cdecea6c
commit 37c197c095
17 files changed
+96 -35

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.34.10",
"version": "0.34.11",
"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": [
+23 -3
View File
@@ -1,5 +1,5 @@
// Stable plot ranges: percentages are absolute 0-100; unbounded metrics have
// unit-appropriate zero-based floors, expanding only for real higher readings.
// Overview charts retain stable, expanding ranges (and fixed 0-100% axes).
// Vhost request-rate sparklines use a separate, adaptive plotting policy.
window.rustProxyChartScale = (() => {
const floors = { rate: 100, count: 10, ms: 100, bytes: 64 * 1024 * 1024, "bytes/s": 64 * 1024 };
const ceilings = new Map();
@@ -17,8 +17,28 @@ window.rustProxyChartScale = (() => {
ceilings.set(key, result);
return result;
}
// Vhost live charts show the entire session history, but their Y range
// follows the recent typical rate. A one-off surge must not flatten the
// subsequent request activity; the real (unclipped) peak stays in the legend.
// The recent quantile also prevents a range that only grows over time.
function liveRate(values) {
const samples = (Array.isArray(values) ? values : []).map(value =>
typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0);
const maximum = Math.max(0, ...samples);
const recent = samples.slice(-24).sort((a, b) => a - b);
// Interpolation would allow a single extreme point to dominate small
// histories. Nearest lower rank excludes that isolated maximum.
const typical = recent.length ? recent[Math.floor((recent.length - 1) * 0.85)] : 0;
const scale = Math.max(1, typical * 1.25);
const points = samples.map((value, index) => {
const x = index * 320 / Math.max(1, samples.length - 1);
const y = 50 - Math.min(value / scale, 1) * 44;
return `${x},${y}`;
}).join(" ");
return { points, peak: maximum, scale, clipped: maximum > scale };
}
function reset(prefix) {
for (const key of ceilings.keys()) if (key.startsWith(prefix)) ceilings.delete(key);
}
return { peak, reset };
return { peak, liveRate, reset };
})();
+11 -5
View File
@@ -278,12 +278,18 @@
view.fields.vhErrors.textContent = metrics ? fmt(metrics.errors) : "-";
view.fields.vhLatency.textContent = Number(metrics?.ewma_latency_ms) > 0 ? `${fmt(metrics.ewma_latency_ms, 1)} ms` : "-";
view.fields.vhTraffic.textContent = !history || history.paused ? "-" : `${rateBytes(history.traffic)}/s`;
if (!history) { view.chartValue.textContent = "-"; view.line.setAttribute("points", ""); continue; }
if (!history) {
view.chartValue.textContent = "-"; view.chartValue.removeAttribute("title");
view.line.setAttribute("points", ""); view.line.parentNode?.setAttribute("aria-label", t("vhLifetimeChart"));
continue;
}
const data = history.pendingCount ? [...history.points, history.pendingSum / history.pendingCount] : history.points;
const peak = Math.max(0, ...data), scale = Math.max(1, peak);
const points = data.map((value, index) => `${index * 320 / Math.max(1, data.length - 1)},${50 - value / scale * 44}`).join(" ");
view.line.setAttribute("points", points);
view.chartValue.textContent = `${history.paused ? "-" : history.rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${peak.toFixed(1)}`;
const plot = window.rustProxyChartScale.liveRate(data);
view.line.setAttribute("points", plot.points);
const scaleHint = `${t("vhChartAdaptiveScale")}: 0–${plot.scale.toFixed(1)} ${t("vhReqPerSecond")}${plot.clipped ? ` · ${t("vhChartClippedPeak")}` : ""}`;
view.line.parentNode?.setAttribute("aria-label", `${t("vhLifetimeChart")} · ${scaleHint}`);
view.chartValue.textContent = `${history.paused ? "-" : history.rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${plot.peak.toFixed(1)}`;
view.chartValue.title = scaleHint;
}
}
function selectHealthMode(value) { const control = el("select", undefined, "docker-health-mode"); for (const [name, key] of [["http", "dockerHealthHttp"], ["tcp", "dockerHealthTcp"], ["off", "dockerHealthOff"]]) { const option = el("option", t(key)); option.value = name; control.append(option); } control.value = value; return control; }
+11 -2
View File
@@ -536,8 +536,17 @@ function renderStatus(status) {
}
if (history.points.length >= 120) { const compacted = []; for (let i = 0; i < history.points.length; i += 2) compacted.push((history.points[i] + history.points[i + 1]) / 2); history.points = compacted; history.bucketSize *= 2; }
chartHistory.set(id, history);
const displayPoints = history.pendingCount ? [...history.points, history.pendingSum / history.pendingCount] : history.points; const actualPeak = Math.max(0, ...displayPoints), scalePeak = Math.max(1, actualPeak); const points = displayPoints.map((value, index) => `${index * 320 / Math.max(1, displayPoints.length - 1)},${50 - value / scalePeak * 44}`).join(" ");
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`); svg?.querySelector("polyline")?.setAttribute("points", points); const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`); if (label) label.textContent = `${pausedRate ? "-" : rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${actualPeak.toFixed(1)}`;
const displayPoints = history.pendingCount ? [...history.points, history.pendingSum / history.pendingCount] : history.points;
const plot = window.rustProxyChartScale.liveRate(displayPoints);
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`);
svg?.querySelector("polyline")?.setAttribute("points", plot.points);
const scaleHint = `${t("vhChartAdaptiveScale")}: 0–${plot.scale.toFixed(1)} ${t("vhReqPerSecond")}${plot.clipped ? ` · ${t("vhChartClippedPeak")}` : ""}`;
svg?.setAttribute("aria-label", `${t("vhLifetimeChart")} · ${scaleHint}`);
const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`);
if (label) {
label.textContent = `${pausedRate ? "-" : rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${plot.peak.toFixed(1)}`;
label.title = scaleHint;
}
}
const editingVhost = editing ? configuredVhosts.get(editing) : null;
const editingHasProxy = editingVhost && (editingVhost.source?.type !== "web_server" || (editingVhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy"));