This commit is contained in:
Mateusz Gruszczyński committed 2026-10-15 16:15:49 +02:00
1 parent 5dd802ae83
commit a286355b0e
22 files changed
+447 -70

No files matched your search

+1
View File
@@ -64,6 +64,7 @@ fn admin_app_javascript() -> &'static str {
include_str!("web/app/session-resume.js"),
include_str!("web/app/refresh.js"),
include_str!("web/app/rate-sampling.js"),
include_str!("web/app/chart-scales.js"),
"const ADMIN_DASHBOARD_WIDGET_REGISTRY = Object.freeze(",
include_str!("web/dashboard-widgets.json"),
");",
+24
View File
@@ -0,0 +1,24 @@
// Stable plot ranges: percentages are absolute 0-100; unbounded metrics have
// unit-appropriate zero-based floors, expanding only for real higher readings.
window.rustProxyChartScale = (() => {
const floors = { rate: 100, count: 10, ms: 100, bytes: 64 * 1024 * 1024, "bytes/s": 64 * 1024 };
const ceilings = new Map();
function peak(key, unit, series) {
if (unit === "percent") return 100;
const floor = floors[unit] ?? 1;
let maximum = floor;
for (const values of series) for (const value of values) {
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
maximum = Math.max(maximum, value);
}
const magnitude = 10 ** Math.floor(Math.log10(maximum));
const tick = [1, 2, 5, 10].find(n => n * magnitude >= maximum) ?? 10;
const result = Math.max(ceilings.get(key) ?? 0, tick * magnitude);
ceilings.set(key, result);
return result;
}
function reset(prefix) {
for (const key of ceilings.keys()) if (key.startsWith(prefix)) ceilings.delete(key);
}
return { peak, reset };
})();
+6 -5
View File
@@ -134,10 +134,10 @@ window.rustProxyExtraOverview = (() => {
if (unit === "rate") return `${fmt(value, 1)}/s`;
return fmt(value);
}
function points(history, unit) {
function points(id, history, unit) {
if (!history.length) return "0,55 300,55";
const peak = unit === "percent" ? Math.max(100, ...history) : Math.max(1, ...history);
return history.map((v, i) => `${i * 300 / Math.max(1, history.length - 1)},${55 - v / peak * 50}`).join(" ");
const peak = window.rustProxyChartScale.peak(`extra:${id}`, unit, [history]);
return history.map((v, i) => `${i * 300 / Math.max(1, history.length - 1)},${55 - Math.min(v, peak) / peak * 50}`).join(" ");
}
function render(status, record = true) {
if (!status || currentPanel !== "overview") return;
@@ -150,6 +150,7 @@ window.rustProxyExtraOverview = (() => {
const value = metric.value;
if (lastUnits.get(id) !== metric.unit) {
histories.delete(id);
window.rustProxyChartScale.reset(`extra:${id}`);
lastUnits.set(id, metric.unit);
}
if (!bounded.has(id) && id !== "extra_cache_hit_rate") {
@@ -159,7 +160,7 @@ window.rustProxyExtraOverview = (() => {
if (history.length > limit()) history.shift();
histories.set(id, history);
}
$(`overview-extra-${id}-line`)?.setAttribute("points", points(history, metric.unit));
$(`overview-extra-${id}-line`)?.setAttribute("points", points(id, history, metric.unit));
}
if (record) {
lastValue.set(id, value);
@@ -177,7 +178,7 @@ window.rustProxyExtraOverview = (() => {
if (detail && id !== "extra_cache_hit_rate") detail.textContent = metric.detail || t("overviewExtraRecentSamples");
}
}
function reset() { histories.clear(); lastCounter.clear(); lastValue.clear(); lastUnits.clear(); lastSegments.clear(); }
function reset() { histories.clear(); lastCounter.clear(); lastValue.clear(); lastUnits.clear(); lastSegments.clear(); window.rustProxyChartScale.reset("extra:"); }
window.rustProxyOverviewSamples?.onChange?.(() => {
for (const history of histories.values()) window.rustProxyOverviewSamples.trim(history);
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, false);
+5 -4
View File
@@ -6,7 +6,7 @@ window.rustProxyWideOverview = (() => {
[s.metrics?.http1_requests, s.metrics?.http2_requests, s.metrics?.http3_requests]
},
wide_cpu: {
unit: "percent", names: ["system", "process"], values: s =>
unit: "percent", names: ["system", "process"], max: 100, values: s =>
[(s.live_system || s.system)?.cpu_percent, (s.live_system || s.system)?.process_cpu_percent]
},
wide_memory: {
@@ -72,7 +72,8 @@ window.rustProxyWideOverview = (() => {
const value = values[i];
if (value === null) { drawing = false; continue; }
const x = Math.round(600 * i / Math.max(1, count - 1));
const y = Math.round(137 - 120 * value / peak);
// Fixed-range charts stop at the upper bound; the legend keeps the real reading.
const y = Math.round(137 - 120 * Math.min(value, peak) / peak);
result += `${drawing ? " L" : " M"}${x} ${y}`;
drawing = true;
}
@@ -98,7 +99,7 @@ window.rustProxyWideOverview = (() => {
}
last.set(id, values);
}
const peak = Math.max(1, ...series.flat().filter(n => n !== null));
const peak = config.max ?? window.rustProxyChartScale.peak(`wide:${id}`, config.unit, series);
config.names.forEach((key, i) => {
const value = values[i];
const field = $(`overview-wide-${id}-${key}-value`);
@@ -111,7 +112,7 @@ window.rustProxyWideOverview = (() => {
: format(values[0], config.unit);
}
}
function reset() { history.clear(); baselines.clear(); last.clear(); revision = null; }
function reset() { history.clear(); baselines.clear(); last.clear(); revision = null; window.rustProxyChartScale.reset("wide:"); }
window.rustProxyOverviewSamples?.onChange?.(() => {
for (const series of history.values()) for (const values of series) window.rustProxyOverviewSamples.trim(values);
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, false);
+4 -3
View File
@@ -87,7 +87,7 @@ window.rustProxyWebOverview = (() => {
return output;
}
function smallPath(values) {
const peak = Math.max(1, ...values.filter(v => v != null));
const peak = window.rustProxyChartScale.peak("web:php-small", "rate", [values]);
return values.filter(value => value !== null).map((value, i, present) => `${i * 300 / Math.max(1, present.length - 1)},${55 - value / peak * 50}`).join(" ");
}
function ring(id, amounts) {
@@ -173,7 +173,8 @@ window.rustProxyWebOverview = (() => {
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
histories.set(id, series);
}
const peak = Math.max(1, ...series.flat().filter(v => v !== null));
const unit = id === "www_latency" ? "ms" : id === "www_traffic" ? "bytes/s" : "rate";
const peak = window.rustProxyChartScale.peak(`web:${id}`, unit, series);
names.forEach((name, i) => {
const value = set[id]?.[i];
const label = id === "www_latency" ? (value == null ? "-" : `${fmt(value, 1)} ms`)
@@ -216,7 +217,7 @@ window.rustProxyWebOverview = (() => {
}
}
}
function reset() { histories.clear(); serviceBaselines.clear(); baseline = null; last = null; revision = null; }
function reset() { histories.clear(); serviceBaselines.clear(); baseline = null; last = null; revision = null; window.rustProxyChartScale.reset("web:"); }
window.rustProxyOverviewSamples.onChange(() => {
for (const history of histories.values()) {
if (Array.isArray(history[0])) history.forEach(window.rustProxyOverviewSamples.trim);
+2 -1
View File
@@ -70,12 +70,13 @@
function resetLiveChart(selected = scope()) {
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null;
for (const values of Object.values(liveChartSamples)) values.length = 0;
window.rustProxyChartScale.reset("cache-live");
for (const id of ["cache-live-hit-line", "cache-live-miss-line", "cache-live-bypass-line"]) $(id)?.setAttribute("points", "0,170 900,170");
for (const id of ["cache-live-hit-rate", "cache-live-miss-rate", "cache-live-bypass-rate"]) if ($(id)) $(id).textContent = "0.0/s";
if ($("cache-live-chart-value")) $("cache-live-chart-value").textContent = "-";
}
function redrawLiveChart() {
const peak = Math.max(1, ...liveChartSamples.hits, ...liveChartSamples.misses, ...liveChartSamples.bypasses);
const peak = window.rustProxyChartScale.peak("cache-live", "rate", Object.values(liveChartSamples));
$("cache-live-hit-line")?.setAttribute("points", sparklinePoints(liveChartSamples.hits, 900, 180, 10, peak));
$("cache-live-miss-line")?.setAttribute("points", sparklinePoints(liveChartSamples.misses, 900, 180, 10, peak));
$("cache-live-bypass-line")?.setAttribute("points", sparklinePoints(liveChartSamples.bypasses, 900, 180, 10, peak));
+16 -16
View File
@@ -651,7 +651,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="HTTP/1.1 · HTTP/2 · HTTP/3 traffic">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_protocol_rates-h1-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -675,7 +675,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="System and process CPU">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_cpu-system-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -697,7 +697,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Process RAM and cache memory">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_memory-process-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -720,7 +720,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Cache hits / misses per second">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_cache_activity-hits-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -741,7 +741,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Connections and WebSocket tunnels">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_connections-open-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -764,7 +764,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Backend availability over time">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_backend_health-up-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -786,7 +786,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Response and backend probe latency">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_latency-response-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -809,7 +809,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
data-i18n-aria="overviewWideTransformActivity" aria-label="Transformation activity">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_transform_activity-applied-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -842,7 +842,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
data-i18n-aria="overviewWideCompressionActivity" aria-label="Compression activity">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0"
id="overview-wide-wide_compression_activity-gzip-line" d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -954,7 +954,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server request rate" data-i18n-aria="webMetricRequests">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_requests-all-line"
d=""></path>
<path class="overview-wide-series overview-wide-series-1" id="www_requests-php-line"
@@ -975,7 +975,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server header latency" data-i18n-aria="webMetricLatency">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_latency-web-line"
d=""></path>
<path class="overview-wide-series overview-wide-series-1" id="www_latency-php-line"
@@ -996,7 +996,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="PHP FastCGI failures" data-i18n-aria="webMetricFailures">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_failures-timeout-line"
d=""></path>
<path class="overview-wide-series overview-wide-series-1"
@@ -1027,7 +1027,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server body byte rate" data-i18n-aria="webMetricTraffic">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_traffic-nonphp-line"
d=""></path>
<path class="overview-wide-series overview-wide-series-1" id="www_traffic-php-line"
@@ -1048,7 +1048,7 @@
</div>
<svg preserveAspectRatio="none" class="overview-wide-svg" viewBox="0 0 600 150" role="img"
aria-label="Web Server HTTP statuses" data-i18n-aria="webMetricStatusTrend">
<path class="chart-grid" d="M0 37H600 M0 75H600 M0 113H600"></path>
<path class="chart-grid" d="M0 47H600 M0 77H600 M0 107H600"></path>
<path class="overview-wide-series overview-wide-series-0" id="www_status_trend-2xx-line"
d=""></path>
<path class="overview-wide-series overview-wide-series-1" id="www_status_trend-3xx-line"
@@ -2293,7 +2293,7 @@
<svg aria-label="Live cache activity" data-i18n-aria="cacheLiveChartTitle"
id="cache-live-activity-chart" role="img" viewBox="0 0 900 180"
preserveAspectRatio="none">
<path class="chart-grid" d="M0 45H900 M0 90H900 M0 135H900"></path>
<path class="chart-grid" d="M0 50H900 M0 90H900 M0 130H900"></path>
<polyline class="function-live-line cache-hit-line" id="cache-live-hit-line"
points="0,170 900,170"></polyline>
<polyline class="function-live-line cache-miss-line" id="cache-live-miss-line"
@@ -2446,7 +2446,7 @@
<svg aria-label="Live transformation activity" data-i18n-aria="transformLiveChartTitle"
id="transform-live-activity-chart" role="img" viewBox="0 0 900 180"
preserveAspectRatio="none">
<path class="chart-grid" d="M0 45H900 M0 90H900 M0 135H900"></path>
<path class="chart-grid" d="M0 50H900 M0 90H900 M0 130H900"></path>
<polyline class="function-live-line transform-applied-line"
id="transform-live-applied-line" points="0,170 900,170"></polyline>
<polyline class="function-live-line transform-cache-line" id="transform-live-cache-line"
+2 -5
View File
@@ -7177,7 +7177,7 @@ body[data-density=compact] .page-footer {
.function-live-chart svg {
display: block;
width: 100%;
height: calc(clamp(180px, 22vw, 270px));
height: 180px;
overflow: visible
}
@@ -7270,9 +7270,6 @@ body[data-density=compact] .page-footer {
white-space: normal
}
.function-live-chart svg {
height: 190px
}
.function-live-chart-footer {
align-items: flex-start;
@@ -10889,7 +10886,7 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
#panel-overview .overview-wide-svg {
display: block;
width: 100%;
height: calc(clamp(125px, 13vw, 190px));
height: 125px;
margin-block: 13px 9px;
}
+2 -1
View File
@@ -22,12 +22,13 @@
function resetLiveChart(selected = scope()) {
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null;
for (const values of Object.values(liveChartSamples)) values.length = 0;
window.rustProxyChartScale.reset("transform-live");
for (const id of ["transform-live-applied-line", "transform-live-cache-line", "transform-live-skipped-line", "transform-live-error-line"]) $(id)?.setAttribute("points", "0,170 900,170");
for (const id of ["transform-live-applied-rate", "transform-live-cache-rate", "transform-live-skipped-rate", "transform-live-error-rate"]) if ($(id)) $(id).textContent = "0.0/s";
if ($("transform-live-chart-value")) $("transform-live-chart-value").textContent = "-";
}
function redrawLiveChart() {
const peak = Math.max(1, ...liveChartSamples.applied, ...liveChartSamples.cache_hits, ...liveChartSamples.skipped, ...liveChartSamples.errors);
const peak = window.rustProxyChartScale.peak("transform-live", "rate", Object.values(liveChartSamples));
$("transform-live-applied-line")?.setAttribute("points", sparklinePoints(liveChartSamples.applied, 900, 180, 10, peak));
$("transform-live-cache-line")?.setAttribute("points", sparklinePoints(liveChartSamples.cache_hits, 900, 180, 10, peak));
$("transform-live-skipped-line")?.setAttribute("points", sparklinePoints(liveChartSamples.skipped, 900, 180, 10, peak));