v0.34.12
This commit is contained in:
1 parent
37c197c095
commit
5ac3617947
48 files changed
+1049
-255
No files matched your search
@@ -18,7 +18,6 @@ window.rustProxyBackendDashboard = (() => {
|
||||
}
|
||||
function syncVisibility(hiddenOverride = null) {
|
||||
const prefs = window.rustProxyPreferences?.current?.();
|
||||
// While unlocked, currentDraft is authoritative until Save or Cancel.
|
||||
const hidden = new Set(hiddenOverride ?? window.rustProxyInlineLayout?.currentDraft?.()?.hidden
|
||||
?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
|
||||
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
|
||||
@@ -30,7 +29,13 @@ window.rustProxyBackendDashboard = (() => {
|
||||
}
|
||||
function paintSeries(name, id, scale) {
|
||||
const node = $(id);
|
||||
if (node) node.setAttribute("points", line(samples[name], scale));
|
||||
if (node) {
|
||||
node.setAttribute("points", line(samples[name], scale));
|
||||
const unit = name === "latency" ? "ms" : ["inbound", "outbound"].includes(name) ? "bytes/s"
|
||||
: name === "active" ? "count" : "rate";
|
||||
window.rustProxyChartScale.axes(node, scale, unit, samples[name].length,
|
||||
{ top: 5 / 60, bottom: 55 / 60 });
|
||||
}
|
||||
}
|
||||
function push(name, value) {
|
||||
samples[name].push(Math.max(0, value));
|
||||
@@ -50,8 +55,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
window.rustProxyExtraOverview?.meter?.("backend-dashboard-capacity", null);
|
||||
reset();
|
||||
}
|
||||
// Counter deltas are matched to stable (pool, member) IDs, not aggregate
|
||||
// totals: removed/rebuilt pools must never become negative or spike rates.
|
||||
|
||||
function memberCounters(members) {
|
||||
const values = new Map();
|
||||
for (const { pool, member } of members) {
|
||||
@@ -73,7 +77,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
for (const [id, next] of current) {
|
||||
const old = earlier.get(id);
|
||||
if (!old) continue;
|
||||
// A generation reset on a member invalidates its entire interval.
|
||||
|
||||
if (tracked.some(field => next[field] < old[field])) continue;
|
||||
for (const field of tracked) total[field] += next[field] - old[field];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// 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,17 +16,12 @@ 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) => {
|
||||
@@ -40,5 +34,107 @@ window.rustProxyChartScale = (() => {
|
||||
function reset(prefix) {
|
||||
for (const key of ceilings.keys()) if (key.startsWith(prefix)) ceilings.delete(key);
|
||||
}
|
||||
return { peak, liveRate, reset };
|
||||
|
||||
function axisValue(value, unit) {
|
||||
if (value === 0) return "0";
|
||||
const n = Number(value);
|
||||
const format = v => v.toLocaleString(typeof locale === "function" ? locale() : undefined,
|
||||
{ maximumFractionDigits: v < 1 ? 3 : v < 10 ? 2 : 1 });
|
||||
if (unit === "bytes" || unit === "bytes/s") {
|
||||
let magnitude = n, suffix = "B";
|
||||
for (const next of ["KiB", "MiB", "GiB", "TiB"]) {
|
||||
if (magnitude < 1024) break;
|
||||
magnitude /= 1024; suffix = next;
|
||||
}
|
||||
return `${format(magnitude)} ${suffix}${unit === "bytes/s" ? "/s" : ""}`;
|
||||
}
|
||||
return `${format(n)}${unit === "percent" ? "%" : unit === "ms" ? " ms" : unit === "rate" ? "/s" : ""}`;
|
||||
}
|
||||
function setAxisText(node, value) {
|
||||
if (node.textContent !== value) node.textContent = value;
|
||||
}
|
||||
function renderAxes(svg) {
|
||||
if (!svg?.isConnected || !svg.rustProxyAxisData) return;
|
||||
let host = svg.parentElement;
|
||||
if (!host?.classList.contains("rp-chart-axis-host")) {
|
||||
host = document.createElement("div");
|
||||
host.className = "rp-chart-axis-host";
|
||||
svg.before(host);
|
||||
host.append(svg);
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "rp-chart-axis-y";
|
||||
overlay.setAttribute("aria-hidden", "true");
|
||||
for (const name of ["top", "middle", "bottom"]) {
|
||||
const tick = document.createElement("span");
|
||||
tick.className = `rp-chart-axis-${name}`;
|
||||
overlay.append(tick);
|
||||
}
|
||||
const perSeries = document.createElement("div");
|
||||
perSeries.className = "rp-chart-axis-series";
|
||||
perSeries.hidden = true;
|
||||
const x = document.createElement("div");
|
||||
x.className = "rp-chart-axis-x";
|
||||
x.setAttribute("aria-label", "X");
|
||||
for (const name of ["start", "caption", "end"]) {
|
||||
const tick = document.createElement("span");
|
||||
tick.className = `rp-chart-axis-${name}`;
|
||||
x.append(tick);
|
||||
}
|
||||
host.append(overlay, perSeries, x);
|
||||
}
|
||||
const { peak: maximum, unit, count, top, bottom, clipped, seriesScales } = svg.rustProxyAxisData;
|
||||
const compact = svg.closest(".stat-chart") !== null;
|
||||
host.classList.toggle("rp-chart-axis-compact", compact);
|
||||
const multi = Array.isArray(seriesScales) && seriesScales.length > 0;
|
||||
host.classList.toggle("rp-chart-axis-multiple", multi);
|
||||
const perSeries = host.querySelector(".rp-chart-axis-series");
|
||||
perSeries.hidden = !multi;
|
||||
if (multi) {
|
||||
const values = seriesScales.map(([name, value]) => `${name}: 0–${axisValue(value, unit)}`);
|
||||
if (perSeries.dataset.values !== values.join("|")) {
|
||||
perSeries.replaceChildren(...values.map(value => {
|
||||
const label = document.createElement("span");
|
||||
label.textContent = value;
|
||||
return label;
|
||||
}));
|
||||
perSeries.dataset.values = values.join("|");
|
||||
}
|
||||
}
|
||||
host.style.setProperty("--rp-axis-top", `${Math.round(top * 1000) / 10}%`);
|
||||
host.style.setProperty("--rp-axis-bottom", `${Math.round(bottom * 1000) / 10}%`);
|
||||
setAxisText(host.querySelector(".rp-chart-axis-top"), `Y ${axisValue(maximum, unit)}${clipped ? " ↑" : ""}`);
|
||||
setAxisText(host.querySelector(".rp-chart-axis-middle"), axisValue(maximum / 2, unit));
|
||||
setAxisText(host.querySelector(".rp-chart-axis-bottom"), "0");
|
||||
setAxisText(host.querySelector(".rp-chart-axis-start"), compact && !multi
|
||||
? `Y 0–${axisValue(maximum, unit)}` : count ? "1" : "–");
|
||||
setAxisText(host.querySelector(".rp-chart-axis-caption"), `X · ${typeof t === "function" ? t("chartAxisSamples") : "samples"}`);
|
||||
setAxisText(host.querySelector(".rp-chart-axis-end"), count ? (compact ? `1–${count}` : String(count)) : "–");
|
||||
host.querySelector(".rp-chart-axis-x").title = typeof t === "function" ? t("chartAxisSamplesHint") : "Sample indices, not elapsed time";
|
||||
}
|
||||
function axes(target, peakValue, unit, count,
|
||||
{ top = 0.08, bottom = 0.92, clipped = false, seriesScales = null } = {}) {
|
||||
const node = typeof target === "string" ? document.getElementById(target) : target;
|
||||
const svg = node?.tagName?.toLowerCase() === "svg" ? node : node?.ownerSVGElement;
|
||||
if (!svg) return;
|
||||
svg.rustProxyAxisData = {
|
||||
peak: Number.isFinite(peakValue) && peakValue > 0 ? peakValue : 1,
|
||||
unit, count: Number.isInteger(count) && count > 0 ? count : 0,
|
||||
top: Math.min(1, Math.max(0, top)), bottom: Math.min(1, Math.max(0, bottom)), clipped: clipped === true,
|
||||
seriesScales: Array.isArray(seriesScales) ? seriesScales.filter(row =>
|
||||
Array.isArray(row) && typeof row[0] === "string" && Number.isFinite(row[1]) && row[1] >= 0) : null,
|
||||
};
|
||||
if (document.documentElement.dataset.chartAxes === "true") renderAxes(svg);
|
||||
}
|
||||
function refreshAxes() {
|
||||
const show = document.documentElement.dataset.chartAxes === "true";
|
||||
for (const svg of document.querySelectorAll("svg")) {
|
||||
if (show && svg.rustProxyAxisData) renderAxes(svg);
|
||||
else if (!show && svg.parentElement?.classList.contains("rp-chart-axis-host")) {
|
||||
const host = svg.parentElement;
|
||||
host.before(svg);
|
||||
host.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return { peak, liveRate, reset, axes, refreshAxes, axisValue };
|
||||
})();
|
||||
@@ -201,6 +201,8 @@ function redrawOverviewHistoryCharts() {
|
||||
const connectionPoints = values => values.map((value, index) => `${index * 300 / Math.max(1, values.length - 1)},${95 - value / connectionPeak * 85}`).join(" ");
|
||||
$("connection-open-sparkline").setAttribute("points", connectionPoints(connectionOpenSamples));
|
||||
$("connection-exchange-sparkline").setAttribute("points", connectionPoints(connectionExchangeSamples));
|
||||
window.rustProxyChartScale.axes("connection-chart", connectionPeak, "count", connectionOpenSamples.length,
|
||||
{ top: 10 / 100, bottom: 95 / 100 });
|
||||
const peak = Math.max(1, ...samples, ...transformSamples);
|
||||
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
|
||||
$("sparkline").setAttribute("points", points(samples));
|
||||
@@ -208,10 +210,19 @@ function redrawOverviewHistoryCharts() {
|
||||
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
|
||||
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
|
||||
$("sparkline-transform").setAttribute("points", points(transformSamples));
|
||||
window.rustProxyChartScale.axes("traffic-chart", peak, "rate", samples.length,
|
||||
{ top: 15 / 130, bottom: 120 / 130 });
|
||||
for (const key of ["h1", "h2", "h3"]) {
|
||||
$("http-protocol-" + key).setAttribute("points", sparklinePoints(protocolSamples[key], 300, 60, 5));
|
||||
$("ws-protocol-" + key).setAttribute("points", sparklinePoints(websocketProtocolSamples[key], 300, 60, 5));
|
||||
}
|
||||
const httpValues = Object.values(protocolSamples), websocketValues = Object.values(websocketProtocolSamples);
|
||||
window.rustProxyChartScale.axes("http-protocol-chart", Math.max(1, ...httpValues.flat()), "rate",
|
||||
protocolSamples.h1.length, { seriesScales: Object.entries(protocolSamples).map(([name, values]) =>
|
||||
[name.toUpperCase(), Math.max(1, ...values)]) });
|
||||
window.rustProxyChartScale.axes("ws-protocol-chart", Math.max(1, ...websocketValues.flat()), "count",
|
||||
websocketProtocolSamples.h1.length, { seriesScales: Object.entries(websocketProtocolSamples).map(([name, values]) =>
|
||||
[name.toUpperCase(), Math.max(1, ...values)]) });
|
||||
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
|
||||
}
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// Opt-in compact metrics and bounded visualizations. All readings reuse the
|
||||
// existing Overview live-status payload (no additional requests or timers).
|
||||
|
||||
window.rustProxyExtraOverview = (() => {
|
||||
const limit = () => window.rustProxyOverviewSamples?.getLimit?.() ?? 60;
|
||||
const names = [
|
||||
@@ -87,13 +86,17 @@ window.rustProxyExtraOverview = (() => {
|
||||
}
|
||||
if (id === "extra_backend_active") {
|
||||
const members = Array.isArray(status.backend_pools) ? status.backend_pools.flatMap(pool => pool.members || []) : null;
|
||||
return { value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
|
||||
unit: "count", detail: "" };
|
||||
return {
|
||||
value: members ? members.reduce((sum, member) => sum + (positive(member.active) ?? 0), 0) : null,
|
||||
unit: "count", detail: ""
|
||||
};
|
||||
}
|
||||
if (id === "extra_backend_unhealthy") {
|
||||
return { value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
|
||||
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
|
||||
unit: "count", detail: "" };
|
||||
return {
|
||||
value: Array.isArray(status.backend_pools) ? status.backend_pools.reduce((sum, pool) =>
|
||||
sum + (pool.members || []).filter(member => member.status === "down").length, 0) : null,
|
||||
unit: "count", detail: ""
|
||||
};
|
||||
}
|
||||
if (id === "extra_cache_memory") return ramBudget(c.resident_bytes, c.hot_limit_bytes);
|
||||
if (id === "extra_cold_queue_memory") return ramBudget(c.cold_queue_bytes, c.cold_queue_limit_bytes);
|
||||
@@ -123,8 +126,10 @@ window.rustProxyExtraOverview = (() => {
|
||||
const { deltas, seconds } = result;
|
||||
if (unit === "rate") return { value: deltas[0] / seconds, unit, detail: "" };
|
||||
const sum = id === "extra_cache_hit_rate" ? deltas[0] + deltas[1] : deltas[1];
|
||||
return { value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
|
||||
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null };
|
||||
return {
|
||||
value: sum > 0 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "",
|
||||
slices: id === "extra_cache_hit_rate" ? [deltas[0], deltas[1]] : null
|
||||
};
|
||||
}
|
||||
function format(value, unit) {
|
||||
if (value == null) return "-";
|
||||
@@ -145,7 +150,6 @@ window.rustProxyExtraOverview = (() => {
|
||||
for (const id of names) {
|
||||
const tile = node(id);
|
||||
if (!tile) { lastCounter.delete(id); continue; }
|
||||
// Visibility is only a presentation concern; retain live history.
|
||||
const metric = read(id, status, now, record);
|
||||
const value = metric.value;
|
||||
if (lastUnits.get(id) !== metric.unit) {
|
||||
@@ -161,6 +165,9 @@ window.rustProxyExtraOverview = (() => {
|
||||
histories.set(id, history);
|
||||
}
|
||||
$(`overview-extra-${id}-line`)?.setAttribute("points", points(id, history, metric.unit));
|
||||
window.rustProxyChartScale.axes(`overview-extra-${id}-line`,
|
||||
window.rustProxyChartScale.peak(`extra:${id}`, metric.unit, [history]), metric.unit,
|
||||
history.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
}
|
||||
if (record) {
|
||||
lastValue.set(id, value);
|
||||
|
||||
@@ -72,7 +72,6 @@ window.rustProxyWideOverview = (() => {
|
||||
const value = values[i];
|
||||
if (value === null) { drawing = false; continue; }
|
||||
const x = Math.round(600 * i / Math.max(1, count - 1));
|
||||
// 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;
|
||||
@@ -106,6 +105,8 @@ window.rustProxyWideOverview = (() => {
|
||||
if (field) field.textContent = format(value, config.unit);
|
||||
$(`overview-wide-${id}-${key}-line`)?.setAttribute("d", path(series[i], peak));
|
||||
});
|
||||
window.rustProxyChartScale.axes(`overview-wide-${id}-${config.names[0]}-line`,
|
||||
peak, config.unit, series[0].length, { top: 17 / 150, bottom: 137 / 150 });
|
||||
const summary = $(`overview-wide-${id}-summary`);
|
||||
if (summary) summary.textContent = config.total
|
||||
? format(values.every(value => value !== null) ? values.reduce((sum, value) => sum + value, 0) : null, config.unit)
|
||||
|
||||
@@ -13,6 +13,7 @@ const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
||||
])]);
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
||||
chart_axes: false,
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null,
|
||||
});
|
||||
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
|
||||
@@ -35,14 +36,12 @@ function normalizeDashboardWidgets(value) {
|
||||
function normalizeHiddenDashboardWidgets(value, savedOrder = ADMIN_DASHBOARD_WIDGETS) {
|
||||
const seen = new Set();
|
||||
const hidden = (Array.isArray(value) ? value : []).filter(id => ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id) && seen.add(id));
|
||||
// Also protect preferences written by 0.31.x with an explicit empty hidden list.
|
||||
for (const id of ADMIN_DEFAULT_HIDDEN_WIDGETS) {
|
||||
if (!savedOrder?.includes(id) && !seen.has(id)) { hidden.push(id); seen.add(id); }
|
||||
}
|
||||
return hidden;
|
||||
}
|
||||
// An absent map preserves the previous grouped visual order after upgrading.
|
||||
// An explicit object (even empty) opts into free ordering and card resizing.
|
||||
|
||||
const DASHBOARD_WIDTHS = Object.freeze([2, 3, 4, 6, 12]);
|
||||
function dashboardAllowedWidths(id) {
|
||||
switch (dashboardWidgetType(id)) {
|
||||
@@ -76,7 +75,6 @@ function dashboardWidgetWidth(id, sizes) {
|
||||
function dashboardVisualOrder(order, sizes = null) {
|
||||
const normalized = normalizeDashboardWidgets(order);
|
||||
if (sizes !== null) return normalized;
|
||||
// Preserve the old grouped appearance until the first intentional edit.
|
||||
return normalized.map((id, index) => ({ id, index }))
|
||||
.sort((a, b) => (window.rustProxyMasonry?.groupFor?.(a.id) ?? 0) -
|
||||
(window.rustProxyMasonry?.groupFor?.(b.id) ?? 0) || a.index - b.index)
|
||||
@@ -118,6 +116,13 @@ function applyAdminChartStyle(value) {
|
||||
const style = normalizeAdminChartStyle(value); document.documentElement.dataset.chartStyle = style;
|
||||
const select = $("ui-chart-style"); if (select) select.value = style; refreshChartStylePalette(style); return style;
|
||||
}
|
||||
function applyAdminChartAxes(value) {
|
||||
const enabled = value === true;
|
||||
document.documentElement.dataset.chartAxes = String(enabled);
|
||||
const input = $("ui-chart-axes"); if (input) input.checked = enabled;
|
||||
window.rustProxyChartScale?.refreshAxes?.();
|
||||
return enabled;
|
||||
}
|
||||
function applyAdminButtonStyle(value) {
|
||||
const style = normalizeAdminButtonStyle(value); document.documentElement.dataset.buttonStyle = style;
|
||||
const select = $("ui-button-style"); if (select) select.value = style; return style;
|
||||
@@ -271,8 +276,7 @@ function formatDateTime(value) { const date = value instanceof Date ? value : ne
|
||||
function formatTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleTimeString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
|
||||
function applyAdminPreferences(value, cache = true) {
|
||||
if (!value) return value;
|
||||
// A live inline edit is a draft, not a saved preference. Background updates
|
||||
// (including an unrelated preference save) must not repaint the old layout.
|
||||
|
||||
const editing = window.rustProxyInlineLayout?.currentDraft?.();
|
||||
const savedOrder = normalizeDashboardWidgets(value.dashboard_widgets);
|
||||
const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets);
|
||||
@@ -283,7 +287,7 @@ function applyAdminPreferences(value, cache = true) {
|
||||
...value,
|
||||
refresh: normalizeRefreshPreferences(value.refresh),
|
||||
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
@@ -331,7 +335,7 @@ function updateAdminPreferences(patch) {
|
||||
const next = { ...current, ...patch };
|
||||
next.refresh = normalizeRefreshPreferences(next.refresh);
|
||||
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes);
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
@@ -407,6 +411,14 @@ $("ui-chart-style")?.addEventListener("change", () => {
|
||||
const control = $("ui-chart-style"), previous = adminPreferences?.chart_style || "default", next = normalizeAdminChartStyle(control.value);
|
||||
applyAdminChartStyle(next); if (adminPreferences) adminPreferences.chart_style = next; void saveAppearancePreference({ chart_style: next }, control, () => { if (adminPreferences) adminPreferences.chart_style = previous; applyAdminChartStyle(previous); });
|
||||
});
|
||||
$("ui-chart-axes")?.addEventListener("change", () => {
|
||||
const control = $("ui-chart-axes"), previous = adminPreferences?.chart_axes === true, next = control.checked;
|
||||
applyAdminChartAxes(next); if (adminPreferences) adminPreferences.chart_axes = next;
|
||||
void saveAppearancePreference({ chart_axes: next }, control, () => {
|
||||
if (adminPreferences) adminPreferences.chart_axes = previous;
|
||||
applyAdminChartAxes(previous);
|
||||
});
|
||||
});
|
||||
$("ui-button-style")?.addEventListener("change", () => {
|
||||
const control = $("ui-button-style"), previous = adminPreferences?.button_style || "normal", next = normalizeAdminButtonStyle(control.value);
|
||||
applyAdminButtonStyle(next); if (adminPreferences) adminPreferences.button_style = next; void saveAppearancePreference({ button_style: next }, control, () => { if (adminPreferences) adminPreferences.button_style = previous; applyAdminButtonStyle(previous); });
|
||||
@@ -438,7 +450,7 @@ async function resetAppearancePatch(patch, control, messageKey = "appearanceSect
|
||||
}
|
||||
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
|
||||
const patches = {
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal", corner_style: "default" }, charts: { chart_style: "default" }, tables: { table_density: "normal" },
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
||||
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
|
||||
@@ -346,6 +346,8 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
setChartText(row.querySelector(".network-interface-hide"), t("hideMetric"));
|
||||
row.querySelector(".network-rx-line").setAttribute("points", sparklinePoints(history.rx, 300, 60, 5, peak) || "0,55 300,55");
|
||||
row.querySelector(".network-tx-line").setAttribute("points", sparklinePoints(history.tx, 300, 60, 5, peak) || "0,55 300,55");
|
||||
window.rustProxyChartScale.axes(row.querySelector("svg"), peak, "bytes/s", history.rx.length,
|
||||
{ top: 5 / 60, bottom: 55 / 60 });
|
||||
row.querySelector("svg").setAttribute("aria-label", `${name} ${t("networkInterfacesHistory")}`);
|
||||
children.push(row);
|
||||
}
|
||||
@@ -379,6 +381,8 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
|
||||
if (children.length !== target.children.length || children.some((node, index) => node !== target.children[index])) {
|
||||
target.replaceChildren(...children);
|
||||
|
||||
if (document.documentElement.dataset.chartAxes === "true") window.rustProxyChartScale.refreshAxes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +461,8 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
}
|
||||
$("system-chart").hidden = live.cpu_percent == null; if (appendSample && live.cpu_percent != null) { systemSamples.push(Number(live.cpu_percent)); systemSamples = systemSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
||||
$("system-sparkline").setAttribute("points", systemSamples.map((n, i) => `${i * 300 / Math.max(1, systemSamples.length - 1)},${95 - Math.max(0, Math.min(100, n)) * .9}`).join(" "));
|
||||
window.rustProxyChartScale.axes("system-chart", 100, "percent", systemSamples.length,
|
||||
{ top: 5 / 100, bottom: 95 / 100 });
|
||||
$("cache-state").textContent = t(envelope?.config.cache?.enabled ? "enabled" : "notEnabled");
|
||||
$("cache-counters").textContent = [c.hits, c.misses, c.bypasses].map(n => fmt(n)).join(" / ");
|
||||
$("cache-objects").textContent = `${fmt(c.entries)} / ${fmtBytes(c.resident_bytes)}`;
|
||||
@@ -496,10 +502,14 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
$("cache-hit-sparkline").setAttribute("points", sparklinePoints(cacheHitSamples, 300, 60, 5, cacheOutcomePeak));
|
||||
$("cache-miss-sparkline").setAttribute("points", sparklinePoints(cacheMissSamples, 300, 60, 5, cacheOutcomePeak));
|
||||
$("cache-bypass-sparkline").setAttribute("points", sparklinePoints(cacheBypassSamples, 300, 60, 5, cacheOutcomePeak));
|
||||
window.rustProxyChartScale.axes("cache-hit-sparkline", cacheOutcomePeak, "count", cacheHitSamples.length,
|
||||
{ top: 5 / 60, bottom: 55 / 60 });
|
||||
const latestCacheDelta = [cacheHitSamples.at(-1), cacheMissSamples.at(-1), cacheBypassSamples.at(-1)];
|
||||
setChartText($("cache-outcome-chart-value"), latestCacheDelta.every(value => value != null) ? latestCacheDelta.map(value => fmt(value)).join(" / ") : "-");
|
||||
const hotLimit = Number(c.hot_limit_bytes), hotPeak = Number.isFinite(hotLimit) && hotLimit > 0 ? Math.max(hotLimit, ...cacheMemorySamples) : null;
|
||||
$("cache-memory-sparkline").setAttribute("points", sparklinePoints(cacheMemorySamples, 300, 60, 5, hotPeak));
|
||||
window.rustProxyChartScale.axes("cache-memory-sparkline", Math.max(1, hotPeak ?? Math.max(0, ...cacheMemorySamples)),
|
||||
"bytes", cacheMemorySamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("cache-chart-value"), occupancyText(c.resident_bytes, c.hot_limit_bytes));
|
||||
const transientLimit = Number(c.transient_limit_bytes);
|
||||
const transientPeak = Number.isFinite(transientLimit) && transientLimit > 0
|
||||
@@ -509,10 +519,14 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
$("cache-fill-sparkline").setAttribute("points", sparklinePoints(cacheFillSamples, 300, 60, 5, transientPeak));
|
||||
$("cache-queue-sparkline").setAttribute("points", sparklinePoints(cacheColdQueueSamples, 300, 60, 5, transientPeak));
|
||||
$("cache-io-sparkline").setAttribute("points", sparklinePoints(cacheColdIoSamples, 300, 60, 5, transientPeak));
|
||||
window.rustProxyChartScale.axes("cache-transient-sparkline", transientPeak, "bytes", cacheTransientSamples.length,
|
||||
{ top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("cache-transient-chart-value"), occupancyText(c.transient_bytes, c.transient_limit_bytes));
|
||||
const coldEnabled = envelope?.config.cache?.cold_enabled === true;
|
||||
const coldLimit = Number(c.cold_limit_bytes), coldPeak = Number.isFinite(coldLimit) && coldLimit > 0 ? Math.max(coldLimit, ...cacheColdStorageSamples) : null;
|
||||
$("cache-cold-sparkline").setAttribute("points", sparklinePoints(cacheColdStorageSamples, 300, 60, 5, coldPeak));
|
||||
window.rustProxyChartScale.axes("cache-cold-sparkline", Math.max(1, coldPeak ?? Math.max(0, ...cacheColdStorageSamples)),
|
||||
"bytes", cacheColdStorageSamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("cache-cold-chart-value"), coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled"));
|
||||
if (systemSample) {
|
||||
const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");
|
||||
@@ -545,8 +559,12 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory);
|
||||
}
|
||||
$("process-cpu-sparkline").setAttribute("points", sparklinePoints(processCpuSamples));
|
||||
window.rustProxyChartScale.axes("process-cpu-sparkline", Math.max(1, ...processCpuSamples), "percent",
|
||||
processCpuSamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("process-cpu-chart-value"), live.process_cpu_percent == null ? "" : `${fmt(live.process_cpu_percent, 1)}%`);
|
||||
$("process-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples));
|
||||
window.rustProxyChartScale.axes("process-rss-sparkline", Math.max(1, ...processRssSamples), "bytes",
|
||||
processRssSamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
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;
|
||||
@@ -564,10 +582,14 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
const networkPeak = Math.max(1, ...networkRxSamples, ...networkTxSamples);
|
||||
$("network-rx-sparkline").setAttribute("points", sparklinePoints(networkRxSamples, 300, 60, 5, networkPeak));
|
||||
$("network-tx-sparkline").setAttribute("points", sparklinePoints(networkTxSamples, 300, 60, 5, networkPeak));
|
||||
window.rustProxyChartScale.axes("network-rx-sparkline", networkPeak, "bytes/s", networkRxSamples.length,
|
||||
{ top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(live.network_rx_bytes_per_second)} / ${fmtBytes(live.network_tx_bytes_per_second)}` : "");
|
||||
$("network-rx-sparkline").closest(".mini-live-chart").hidden = !networkRateSupported;
|
||||
renderNetworkInterfaceCharts(live.network_interfaces || [], appendSample);
|
||||
$("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples));
|
||||
window.rustProxyChartScale.axes("latency-sparkline", Math.max(1, ...responseLatencySamples), "ms",
|
||||
responseLatencySamples.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
setChartText($("latency-chart-value"), m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`);
|
||||
$("latency-sparkline").closest(".mini-live-chart").hidden = m.mean_duration_ms == null;
|
||||
$("compression-counters").textContent = [m.compression_gzip, m.compression_br, m.compression_zstd, m.compression_deflate].map(n => fmt(n)).join(" / ");
|
||||
|
||||
@@ -16,7 +16,6 @@ window.rustProxyStreamDashboard = (() => {
|
||||
}
|
||||
function syncVisibility(hiddenOverride = null) {
|
||||
const prefs = window.rustProxyPreferences?.current?.();
|
||||
// While unlocked, currentDraft is authoritative until Save or Cancel.
|
||||
const hidden = new Set(hiddenOverride ?? window.rustProxyInlineLayout?.currentDraft?.()?.hidden
|
||||
?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
|
||||
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
|
||||
@@ -103,6 +102,8 @@ window.rustProxyStreamDashboard = (() => {
|
||||
trafficRates(pausedRate ? "-" : `${bytes(Math.round(rx))}/s`, pausedRate ? "-" : `${bytes(Math.round(tx))}/s`);
|
||||
$("stream-dashboard-rx")?.setAttribute("points", line(history.rx));
|
||||
$("stream-dashboard-tx")?.setAttribute("points", line(history.tx));
|
||||
window.rustProxyChartScale.axes("stream-dashboard-rx", Math.max(1, ...history.rx, ...history.tx),
|
||||
"bytes/s", history.rx.length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
}
|
||||
function reset() { clearTraffic(); wasVisible = false; }
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
|
||||
@@ -160,15 +160,17 @@ window.rustProxyWebOverview = (() => {
|
||||
histories.set("www_php_requests", values);
|
||||
}
|
||||
$(`www_php_requests-line`)?.setAttribute("points", smallPath(histories.get("www_php_requests") || []));
|
||||
const phpSamples = histories.get("www_php_requests") || [];
|
||||
window.rustProxyChartScale.axes("www_php_requests-line",
|
||||
window.rustProxyChartScale.peak("web:php-small", "rate", [phpSamples]), "rate",
|
||||
phpSamples.filter(value => value !== null).length, { top: 5 / 60, bottom: 55 / 60 });
|
||||
const set = {
|
||||
www_requests: r?.rps, www_latency: r?.latency, www_failures: r?.failures,
|
||||
www_traffic: r?.traffic, www_status_trend: r?.status,
|
||||
};
|
||||
for (const [id, names] of Object.entries(seriesNames)) {
|
||||
const series = histories.get(id) || names.map(() => []);
|
||||
// A resumed/overdue interval has no rate; skipping that entire
|
||||
// snapshot keeps existing paths connected without inventing 0/s.
|
||||
// A genuinely unavailable series inside a valid interval stays null.
|
||||
|
||||
if (record && r) {
|
||||
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
|
||||
histories.set(id, series);
|
||||
@@ -182,6 +184,8 @@ window.rustProxyWebOverview = (() => {
|
||||
show(`${id}-${name}-value`, label);
|
||||
$(`${id}-${name}-line`)?.setAttribute("d", chartPath(series[i], peak));
|
||||
});
|
||||
window.rustProxyChartScale.axes(`${id}-${names[0]}-line`, peak, unit,
|
||||
series[0].length, { top: 17 / 150, bottom: 137 / 150 });
|
||||
const summary = id === "www_latency" ? (set[id]?.[0] == null ? "-" : `${fmt(set[id][0], 1)} ms`)
|
||||
: id === "www_traffic" ? (set[id] ? `${fmtBytes(set[id][0] + set[id][1])}/s` : "-")
|
||||
: fmtRate(set[id]?.[0]);
|
||||
|
||||
Reference in new issue
Block a user