v0.32.2
This commit is contained in:
1 parent
bb025dcf44
commit
b0f211555a
74 files changed
+6089
-556
No files matched your search
@@ -0,0 +1,219 @@
|
||||
// Optional HTTP origin diagnostics. Reuse status.backend_pools already present
|
||||
// in live SSE/polling; never issue one request per tile or per member.
|
||||
window.rustProxyBackendDashboard = (() => {
|
||||
const ids = ["backend_health", "backend_activity", "backend_errors", "backend_latency", "backend_traffic", "backend_protocols", "backend_requests", "backend_capacity", "backend_http5xx", "backend_timeouts", "backend_network", "backend_retries", "backend_queue", "backend_rotations"];
|
||||
const charts = new Set(["backend_activity", "backend_errors", "backend_latency", "backend_traffic", "backend_requests", "backend_capacity", "backend_http5xx", "backend_timeouts", "backend_network", "backend_retries", "backend_queue"]);
|
||||
const samples = { active: [], errors: [], latency: [], inbound: [], outbound: [], requests: [], capacity: [], http5xx: [], timeouts: [], network: [], retries: [], queue: [] };
|
||||
const tracked = ["requests", "errors", "inbound", "outbound", "http5xx", "timeouts", "network", "retries", "queue"];
|
||||
let previous = null;
|
||||
let previousRevision = null;
|
||||
const card = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
|
||||
const visible = id => card(id) && !card(id).hidden;
|
||||
const set = (id, value) => { const node = $(id); if (node) node.textContent = String(value); };
|
||||
const count = value => Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : 0;
|
||||
const counter = value => Math.max(0, Math.trunc(count(value)));
|
||||
const shortRate = value => `${value.toFixed(1)}/s`;
|
||||
function reset() {
|
||||
previous = null;
|
||||
previousRevision = null;
|
||||
for (const series of Object.values(samples)) series.length = 0;
|
||||
}
|
||||
function syncVisibility(hiddenOverride = null) {
|
||||
const prefs = window.rustProxyPreferences?.current?.();
|
||||
const hidden = new Set(hiddenOverride ?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
|
||||
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
|
||||
if (![...charts].some(visible)) reset();
|
||||
}
|
||||
function line(series, peak) {
|
||||
if (!series.length) return "0,55 300,55";
|
||||
return series.map((value, i) => `${i * 300 / Math.max(1, series.length - 1)},${55 - value / peak * 50}`).join(" ");
|
||||
}
|
||||
function paintSeries(name, id, scale) {
|
||||
const node = $(id);
|
||||
if (node) node.setAttribute("points", line(samples[name], scale));
|
||||
}
|
||||
function push(name, value) {
|
||||
samples[name].push(Math.max(0, value));
|
||||
if (samples[name].length > 30) samples[name].shift();
|
||||
}
|
||||
function unavailable() {
|
||||
for (const id of ids) {
|
||||
if (!visible(id)) continue;
|
||||
set(`backend-dashboard-${id.slice("backend_".length)}`, "-");
|
||||
set(`backend-dashboard-${id.slice("backend_".length)}-detail`, t("backendWidgetUnavailable"));
|
||||
}
|
||||
for (const id of ["backend-dashboard-in-rate", "backend-dashboard-out-rate"]) set(id, "-");
|
||||
for (const id of ["backend-dashboard-active-line", "backend-dashboard-error-line", "backend-dashboard-latency-line", "backend-dashboard-in-line", "backend-dashboard-out-line", "backend-dashboard-request-line", "backend-dashboard-capacity-line", "backend-dashboard-5xx-line", "backend-dashboard-timeout-line", "backend-dashboard-network-line", "backend-dashboard-retry-line", "backend-dashboard-queue-line"])
|
||||
$(id)?.setAttribute("points", "0,55 300,55");
|
||||
const bar = $("backend-mode-h1");
|
||||
if (bar) for (const item of bar.parentElement.children) item.style.flexGrow = "0";
|
||||
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) {
|
||||
const key = JSON.stringify([pool.id, member.id]);
|
||||
values.set(key, {
|
||||
requests: counter(member.requests), errors: counter(member.errors),
|
||||
inbound: counter(member.bytes_in), outbound: counter(member.bytes_out),
|
||||
http5xx: counter(member.http_5xx_total), timeouts: counter(member.timeouts_total),
|
||||
network: ["connect_errors_total", "dns_errors_total", "tls_errors_total", "io_errors_total"].reduce((n, key) => n + counter(member[key]), 0),
|
||||
retries: counter(member.retry_attempts_total),
|
||||
queue: counter(member.queue_timeouts_total) + counter(member.capacity_rejections_total),
|
||||
});
|
||||
}
|
||||
return values;
|
||||
}
|
||||
function deltas(current, earlier) {
|
||||
const total = Object.fromEntries(tracked.map(field => [field, 0]));
|
||||
if (!earlier) return total;
|
||||
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];
|
||||
}
|
||||
return total;
|
||||
}
|
||||
function render(status, hiddenOverride = null, recordSample = true) {
|
||||
syncVisibility(hiddenOverride);
|
||||
if (!status || currentPanel !== "overview" || ids.every(id => !visible(id))) return;
|
||||
if (!Array.isArray(status.backend_pools)) { unavailable(); return; }
|
||||
const members = status.backend_pools.flatMap(pool => (pool.members || []).map(member => ({ pool, member })));
|
||||
if (!members.length) { unavailable(); return; }
|
||||
|
||||
if (visible("backend_health")) {
|
||||
const statuses = { up: 0, down: 0, checking: 0, draining: 0, disabled: 0 };
|
||||
for (const { member } of members) if (Object.hasOwn(statuses, member.status)) statuses[member.status]++;
|
||||
const eligible = statuses.up + statuses.down + statuses.checking;
|
||||
set("backend-dashboard-health", `${statuses.up} / ${eligible}`);
|
||||
set("backend-dashboard-health-detail", `${t("backendWidgetDown")}: ${statuses.down} · ${t("backendWidgetChecking")}: ${statuses.checking} · ${t("backendWidgetOther")}: ${statuses.draining + statuses.disabled}`);
|
||||
}
|
||||
if (visible("backend_protocols")) {
|
||||
const modes = { http1: 0, auto: 0, http2: 0, h2: 0, unknown: 0 };
|
||||
let h2c = 0;
|
||||
for (const { member } of members) {
|
||||
const mode = member.protocol;
|
||||
if (Object.hasOwn(modes, mode)) modes[mode]++;
|
||||
else modes.unknown++;
|
||||
if ((mode === "http2" || mode === "h2") && /^http:\/\//i.test(member.url || "")) h2c++;
|
||||
}
|
||||
set("backend-dashboard-protocols", fmt(members.length));
|
||||
set("backend-dashboard-protocols-detail", `H1 ${modes.http1} · Auto ${modes.auto} · ${t("backendWidgetModeCompat")} ${modes.http2} · ${t("backendWidgetModeStrict")} ${modes.h2} · ${t("backendWidgetH2c")}: ${h2c}${modes.unknown ? ` · ${t("backendWidgetModeUnknown")}: ${modes.unknown}` : ""}`);
|
||||
const bar = $("backend-mode-h1")?.parentElement;
|
||||
if (bar) {
|
||||
for (const [name, count] of [["h1", modes.http1], ["auto", modes.auto], ["compat", modes.http2], ["h2", modes.h2]])
|
||||
$("backend-mode-" + name).style.flexGrow = String(count);
|
||||
bar.setAttribute("aria-label", `${t("backendWidgetProtocols")}: H1 ${modes.http1}, Auto ${modes.auto}, ${t("backendWidgetModeCompat")} ${modes.http2}, ${t("backendWidgetModeStrict")} ${modes.h2}`);
|
||||
}
|
||||
}
|
||||
if (visible("backend_rotations")) {
|
||||
const rotations = members.reduce((n, { member }) => n + counter(member.upstream_client_rotations_total), 0);
|
||||
const generation = Math.max(0, ...members.map(({ member }) => counter(member.upstream_client_generation)));
|
||||
const age = Math.max(0, ...members.map(({ member }) => counter(member.upstream_client_generation_age_seconds)));
|
||||
set("backend-dashboard-rotations", fmt(rotations));
|
||||
set("backend-dashboard-rotations-detail", `${t("backendWidgetPoolGeneration")}: ${fmt(generation)} · ${t("backendWidgetPoolAge")}: ${fmt(age)}s`);
|
||||
}
|
||||
if (![...charts].some(visible)) return;
|
||||
const active = members.reduce((total, { member }) => total + counter(member.active), 0);
|
||||
const probes = members.map(({ member }) => member).filter(member => member.status === "up" && member.health_mode !== "off" && count(member.last_probe_ms) > 0 && Number.isFinite(Number(member.latency_ms)));
|
||||
const latency = probes.length ? probes.reduce((total, member) => total + count(member.latency_ms), 0) / probes.length : null;
|
||||
const totalErrors = members.reduce((total, { member }) => total + counter(member.errors), 0);
|
||||
const totalRequests = members.reduce((total, { member }) => total + counter(member.requests), 0);
|
||||
const limited = members.map(({ member }) => member)
|
||||
.filter(member => counter(member.max_concurrent_requests) > 0);
|
||||
const capacityUsed = limited.reduce((sum, member) => sum + counter(member.active), 0);
|
||||
const capacityLimit = limited.reduce((sum, member) => sum + counter(member.max_concurrent_requests), 0);
|
||||
const capacityPercent = capacityLimit ? capacityUsed * 100 / capacityLimit : null;
|
||||
const capacityRejects = limited.reduce((sum, member) => sum + counter(member.capacity_rejections_total), 0);
|
||||
const revision = status.revision ?? null;
|
||||
if (previousRevision !== revision) { reset(); previousRevision = revision; }
|
||||
let requestsPerSecond = samples.requests.at(-1) ?? 0;
|
||||
let errorsPerSecond = samples.errors.at(-1) ?? 0;
|
||||
let inPerSecond = samples.inbound.at(-1) ?? 0;
|
||||
let outPerSecond = samples.outbound.at(-1) ?? 0;
|
||||
const additional = { http5xx: 0, timeouts: 0, network: 0, retries: 0, queue: 0 };
|
||||
let fraction = null;
|
||||
if (recordSample) {
|
||||
const now = performance.now();
|
||||
const next = memberCounters(members);
|
||||
const elapsed = previous ? (now - previous.when) / 1000 : 0;
|
||||
if (elapsed > 0) {
|
||||
const diff = deltas(next, previous.counters);
|
||||
requestsPerSecond = diff.requests / elapsed;
|
||||
errorsPerSecond = diff.errors / elapsed;
|
||||
inPerSecond = diff.inbound / elapsed;
|
||||
outPerSecond = diff.outbound / elapsed;
|
||||
fraction = diff.requests ? 100 * diff.errors / diff.requests : null;
|
||||
for (const name of Object.keys(additional)) additional[name] = diff[name] / elapsed;
|
||||
} else { requestsPerSecond = 0; errorsPerSecond = 0; inPerSecond = 0; outPerSecond = 0; }
|
||||
previous = { when: now, counters: next };
|
||||
if (visible("backend_activity")) push("active", active);
|
||||
if (visible("backend_requests")) push("requests", requestsPerSecond);
|
||||
if (visible("backend_capacity") && capacityPercent !== null) push("capacity", capacityPercent);
|
||||
if (visible("backend_errors")) push("errors", errorsPerSecond);
|
||||
if (visible("backend_latency") && latency !== null) push("latency", latency);
|
||||
if (visible("backend_traffic")) { push("inbound", inPerSecond); push("outbound", outPerSecond); }
|
||||
for (const name of Object.keys(additional)) if (visible(`backend_${name === "http5xx" ? "http5xx" : name}`)) push(name, additional[name]);
|
||||
}
|
||||
if (!recordSample) for (const name of Object.keys(additional)) additional[name] = samples[name].at(-1) ?? 0;
|
||||
if (visible("backend_activity")) {
|
||||
set("backend-dashboard-activity", fmt(active));
|
||||
set("backend-dashboard-activity-detail", t("backendWidgetInFlight"));
|
||||
paintSeries("active", "backend-dashboard-active-line", Math.max(1, ...samples.active));
|
||||
}
|
||||
if (visible("backend_requests")) {
|
||||
set("backend-dashboard-requests", shortRate(requestsPerSecond));
|
||||
set("backend-dashboard-requests-detail", `${t("backendWidgetRequestsTotal")}: ${fmt(totalRequests)}`);
|
||||
paintSeries("requests", "backend-dashboard-request-line", Math.max(1, ...samples.requests));
|
||||
}
|
||||
if (visible("backend_capacity")) {
|
||||
set("backend-dashboard-capacity", capacityPercent === null ? "-" : `${fmt(capacityPercent, 1)}%`);
|
||||
set("backend-dashboard-capacity-detail", capacityPercent === null ? t("backendWidgetCapacityNone") :
|
||||
`${t("backendWidgetCapacityLimits")}: ${limited.length} · ${capacityUsed}/${capacityLimit} · ${t("backendWidgetCapacityRejects")}: ${fmt(capacityRejects)}`);
|
||||
paintSeries("capacity", "backend-dashboard-capacity-line", Math.max(100, ...samples.capacity));
|
||||
}
|
||||
if (visible("backend_errors")) {
|
||||
set("backend-dashboard-errors", fmt(totalErrors));
|
||||
set("backend-dashboard-errors-detail", `${t("backendWidgetErrorsRate")}: ${shortRate(errorsPerSecond)}${fraction === null ? "" : ` · ${fraction.toFixed(1)}% ${t("backendWidgetErrorPercent")}`}`);
|
||||
paintSeries("errors", "backend-dashboard-error-line", Math.max(1, ...samples.errors));
|
||||
}
|
||||
if (visible("backend_latency")) {
|
||||
set("backend-dashboard-latency", latency === null ? "-" : `${fmt(latency, 1)} ms`);
|
||||
set("backend-dashboard-latency-detail", `${t("backendWidgetProbeCount")}: ${probes.length}`);
|
||||
paintSeries("latency", "backend-dashboard-latency-line", Math.max(1, ...samples.latency));
|
||||
}
|
||||
if (visible("backend_traffic")) {
|
||||
set("backend-dashboard-traffic", `${bytes(Math.round(inPerSecond + outPerSecond))}/s`);
|
||||
set("backend-dashboard-in-rate", `${bytes(Math.round(inPerSecond))}/s`);
|
||||
set("backend-dashboard-out-rate", `${bytes(Math.round(outPerSecond))}/s`);
|
||||
const peak = Math.max(1, ...samples.inbound, ...samples.outbound);
|
||||
paintSeries("inbound", "backend-dashboard-in-line", peak);
|
||||
paintSeries("outbound", "backend-dashboard-out-line", peak);
|
||||
}
|
||||
const diagnostics = [
|
||||
["http5xx", "http_5xx_total", "backend-dashboard-http5xx", "backend-dashboard-5xx-line"],
|
||||
["timeouts", "timeouts_total", "backend-dashboard-timeouts", "backend-dashboard-timeout-line"],
|
||||
["network", null, "backend-dashboard-network", "backend-dashboard-network-line"],
|
||||
["retries", "retry_attempts_total", "backend-dashboard-retries", "backend-dashboard-retry-line"],
|
||||
["queue", null, "backend-dashboard-queue", "backend-dashboard-queue-line"],
|
||||
];
|
||||
for (const [name, counterName, field, lineId] of diagnostics) {
|
||||
if (!visible(`backend_${name}`)) continue;
|
||||
const total = members.reduce((n, { member }) => n + (counterName ? counter(member[counterName]) :
|
||||
name === "network" ? ["connect_errors_total", "dns_errors_total", "tls_errors_total", "io_errors_total"].reduce((v, key) => v + counter(member[key]), 0) :
|
||||
counter(member.queue_timeouts_total) + counter(member.capacity_rejections_total)), 0);
|
||||
set(field, fmt(total));
|
||||
let detail = `${t("backendWidgetRateDetail")}: ${shortRate(additional[name])}`;
|
||||
if (name === "retries") detail += ` · ${t("backendWidgetRetriesSuccess")}: ${fmt(members.reduce((n, { member }) => n + counter(member.retry_successes_total), 0))}`;
|
||||
if (name === "queue") detail += ` · ${t("backendWidgetQueueRejected")}: ${fmt(members.reduce((n, { member }) => n + counter(member.capacity_rejections_total), 0))}`;
|
||||
if (name === "network") detail += ` · ${t("backendWidgetNetworkBreakdown")}`;
|
||||
set(`${field}-detail`, detail);
|
||||
paintSeries(name, lineId, Math.max(1, ...samples[name]));
|
||||
}
|
||||
}
|
||||
return { syncVisibility, render, reset };
|
||||
})();
|
||||
@@ -59,7 +59,7 @@ function translate() {
|
||||
window.rustProxyDrafts?.refresh();
|
||||
window.rustProxyPreferences?.refreshAppearanceUi?.();
|
||||
$("connection-label").textContent = t(connected ? "connected" : "disconnected");
|
||||
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); } if (lastLogs) renderLogs(lastLogs);
|
||||
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); window.rustProxyBackendDashboard?.render(latestStatus, null, false); window.rustProxyExtraOverview?.render(latestStatus, false); window.rustProxyWideOverview?.render(latestStatus, false); } if (lastLogs) renderLogs(lastLogs);
|
||||
syncSettingsDirtyIndicators();
|
||||
}
|
||||
function hideToast() { const node = $("toast"); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
|
||||
@@ -151,9 +151,10 @@ async function api(path, options = {}) {
|
||||
function setPanel(name, navigate = true) {
|
||||
if (name === "security") { setPanel("settings", navigate); window.rustProxySettings?.select("security"); return; }
|
||||
if (!Object.hasOwn(PANEL_PATHS, name)) name = "overview";
|
||||
const previousPanel = currentPanel;
|
||||
const previousPanel = currentPanel; if (name !== "overview" && previousPanel === "overview") window.rustProxyInlineLayout?.cancel?.();
|
||||
currentPanel = name;
|
||||
$("overview-refresh").hidden = name !== "overview";
|
||||
for (const id of ["overview-layout-open", "overview-layout-unlock"]) if ($(id)) $(id).hidden = name !== "overview";
|
||||
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
|
||||
document.querySelectorAll(".panel").forEach(e => e.hidden = e.id !== `panel-${name}`);
|
||||
$("page-label").textContent = t(name); $("page-title").textContent = t(`${name}Title`); $("page-description").textContent = t(`${name}Description`);
|
||||
@@ -163,7 +164,7 @@ function setPanel(name, navigate = true) {
|
||||
// Paint Stream tiles immediately on return from Appearance using cached status.
|
||||
// Do not create an extra sample/poll on navigation.
|
||||
if (name === "overview" && previousPanel !== name && typeof latestStatus !== "undefined" && latestStatus)
|
||||
window.rustProxyStreamDashboard?.render?.(latestStatus, null, false);
|
||||
{ window.rustProxyStreamDashboard?.render?.(latestStatus, null, false); window.rustProxyBackendDashboard?.render?.(latestStatus, null, false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); }
|
||||
// Read the cached Docker status on each visit, independent of the Live-view toggle.
|
||||
// This is GET /api/docker, not the POST that triggers an agent poll.
|
||||
if (name === "docker-vhosts" && previousPanel !== name && authenticated && envelope) void window.rustProxyRefresh?.manual("docker");
|
||||
@@ -266,7 +267,7 @@ function renderStatus(s, systemSample = false) {
|
||||
$("ws-protocol-h2").setAttribute("points", sparklinePoints(websocketProtocolSamples.h2, 300, 60, 5));
|
||||
$("ws-protocol-h3").setAttribute("points", sparklinePoints(websocketProtocolSamples.h3, 300, 60, 5));
|
||||
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
|
||||
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s);
|
||||
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s); window.rustProxyBackendDashboard?.render(s); window.rustProxyExtraOverview?.render(s); window.rustProxyWideOverview?.render(s);
|
||||
}
|
||||
function renderFooter(s) {
|
||||
const apiLatency = lastApiLatencyMs == null ? "API -" : `API ${lastApiLatencyMs.toFixed(lastApiLatencyMs < 10 ? 1 : 0)} ms`;
|
||||
@@ -371,10 +372,10 @@ function authReturnDestination() {
|
||||
const tab = window.rustProxySettings?.current?.() || new URLSearchParams(location.search).get("tab");
|
||||
return tab ? `/settings?tab=${encodeURIComponent(tab)}` : "/settings";
|
||||
}
|
||||
function lock(notify = true) {
|
||||
function lock(notify = true) { window.rustProxyInlineLayout?.cancel?.();
|
||||
window.rustProxyRefresh?.stop();
|
||||
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
||||
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
||||
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
window.rustProxyInlineLayout = (() => {
|
||||
const grid = $("dashboard-overview-grid");
|
||||
const unlock = $("overview-layout-unlock");
|
||||
const actions = $("overview-inline-actions");
|
||||
const saveButton = $("overview-inline-save");
|
||||
const cancelButton = $("overview-inline-cancel");
|
||||
const hint = $("overview-inline-hint");
|
||||
const hiddenShelf = $("overview-inline-hidden");
|
||||
const hiddenList = $("overview-inline-hidden-list");
|
||||
const hiddenCount = $("overview-inline-hidden-count");
|
||||
const modalButton = $("overview-layout-open");
|
||||
let draft = null;
|
||||
let original = null;
|
||||
let dragging = null;
|
||||
let opening = false;
|
||||
let saving = false;
|
||||
|
||||
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
|
||||
const visibleIds = () => draft.order.filter(id => !draft.hidden.has(id))
|
||||
.sort((a, b) => groupFor(a) - groupFor(b));
|
||||
const nodes = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
|
||||
const nodeFor = id => nodes().find(node => node.dataset.dashboardWidget === id);
|
||||
const label = id => dashboardWidgetLabel(id);
|
||||
const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i])
|
||||
|| [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id)));
|
||||
|
||||
function renderHidden() {
|
||||
if (!hiddenList || !draft) return;
|
||||
const entries = draft.order.filter(id => draft.hidden.has(id));
|
||||
if (hiddenCount) hiddenCount.textContent = `(${entries.length})`;
|
||||
hiddenList.replaceChildren(...(entries.length ? entries.map(id => {
|
||||
const add = control("+", `${t("overviewLayoutInlineRestore")}: ${label(id)}`, "overview-inline-restore", () => {
|
||||
draft.hidden.delete(id);
|
||||
paint();
|
||||
nodeFor(id)?.querySelector(".overview-inline-grip")?.focus();
|
||||
});
|
||||
const item = el("div", undefined, "overview-inline-hidden-item");
|
||||
item.append(el("span", label(id)), dashboardWidgetTypeBadge(id), add);
|
||||
return item;
|
||||
}) : [el("span", t("overviewLayoutInlineHiddenEmpty"), "fine")]));
|
||||
}
|
||||
function hide(id) {
|
||||
if (!draft || !visibleIds().includes(id)) return;
|
||||
draft.hidden.add(id);
|
||||
paint();
|
||||
if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); }
|
||||
}
|
||||
function paint() {
|
||||
applyDashboardLayout(draft.order, [...draft.hidden]);
|
||||
renderHidden();
|
||||
const visible = visibleIds();
|
||||
for (const node of nodes()) {
|
||||
const i = visible.indexOf(node.dataset.dashboardWidget);
|
||||
const up = node.querySelector(".overview-inline-up");
|
||||
const down = node.querySelector(".overview-inline-down");
|
||||
if (up) up.disabled = i <= 0 || groupFor(visible[i - 1]) !== groupFor(node.dataset.dashboardWidget);
|
||||
if (down) down.disabled = i < 0 || i === visible.length - 1 ||
|
||||
groupFor(visible[i + 1]) !== groupFor(node.dataset.dashboardWidget);
|
||||
}
|
||||
}
|
||||
function clearDrag() {
|
||||
dragging = null;
|
||||
for (const node of nodes()) node.classList.remove("is-overview-dragging", "is-overview-drop-before", "is-overview-drop-after");
|
||||
}
|
||||
function move(id, target, after = false) {
|
||||
if (!draft || id === target || groupFor(id) !== groupFor(target)) return;
|
||||
const visible = visibleIds();
|
||||
if (!visible.includes(id) || !visible.includes(target)) return;
|
||||
visible.splice(visible.indexOf(id), 1);
|
||||
visible.splice(visible.indexOf(target) + (after ? 1 : 0), 0, id);
|
||||
const iter = visible.values();
|
||||
draft.order = draft.order.map(item => draft.hidden.has(item) ? item : iter.next().value);
|
||||
paint();
|
||||
}
|
||||
function nudge(id, delta) {
|
||||
const visible = visibleIds();
|
||||
const index = visible.indexOf(id), target = visible[index + delta];
|
||||
if (!target || groupFor(id) !== groupFor(target)) return;
|
||||
move(id, target, delta > 0);
|
||||
nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-up" : ".overview-inline-down")?.focus();
|
||||
}
|
||||
function control(text, title, className, run) {
|
||||
const button = el("button", text, `quiet ${className}`);
|
||||
button.type = "button";
|
||||
button.setAttribute("aria-label", title);
|
||||
button.title = title;
|
||||
button.addEventListener("click", run);
|
||||
return button;
|
||||
}
|
||||
function toolbar(node) {
|
||||
const id = node.dataset.dashboardWidget;
|
||||
const title = label(id);
|
||||
const bar = el("div", undefined, "overview-inline-toolbar");
|
||||
const grip = control("⠿", `${t("overviewLayoutInlineDrag")}: ${title}`, "overview-inline-grip", () => { });
|
||||
grip.draggable = true;
|
||||
grip.dataset.overviewDragHandle = id;
|
||||
bar.append(grip, dashboardWidgetTypeBadge(id),
|
||||
control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)),
|
||||
control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)),
|
||||
control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id)));
|
||||
node.append(bar);
|
||||
}
|
||||
function showEditing() {
|
||||
grid.classList.add("is-unlocked");
|
||||
unlock.setAttribute("aria-pressed", "true");
|
||||
unlock.dataset.i18n = "overviewLayoutLock";
|
||||
unlock.textContent = t("overviewLayoutLock");
|
||||
actions.hidden = false;
|
||||
hint.hidden = false;
|
||||
hint.textContent = t("overviewLayoutInlineHint");
|
||||
if (hiddenShelf) { hiddenShelf.hidden = false; hiddenShelf.open = false; }
|
||||
if (modalButton) modalButton.disabled = true;
|
||||
for (const node of nodes()) toolbar(node);
|
||||
paint();
|
||||
}
|
||||
function finish(preserveSaved = false) {
|
||||
clearDrag();
|
||||
for (const node of nodes()) node.querySelector(".overview-inline-toolbar")?.remove();
|
||||
grid.classList.remove("is-unlocked");
|
||||
unlock.setAttribute("aria-pressed", "false");
|
||||
unlock.dataset.i18n = "overviewLayoutUnlock";
|
||||
unlock.textContent = t("overviewLayoutUnlock");
|
||||
actions.hidden = true;
|
||||
hint.hidden = true;
|
||||
if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; }
|
||||
if (hiddenList) hiddenList.replaceChildren();
|
||||
if (modalButton) modalButton.disabled = false;
|
||||
if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden]);
|
||||
draft = original = null;
|
||||
}
|
||||
async function begin() {
|
||||
if (draft || opening || saving || $("overview-layout-dialog")?.open) return;
|
||||
opening = true;
|
||||
unlock.disabled = true;
|
||||
try {
|
||||
await window.rustProxyPreferences?.load?.();
|
||||
const saved = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
const order = normalizeDashboardWidgets(saved.dashboard_widgets);
|
||||
const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets));
|
||||
original = { order: [...order], hidden: new Set(hidden) };
|
||||
draft = { order: [...order], hidden: new Set(hidden) };
|
||||
showEditing();
|
||||
} catch (error) { if (error?.status !== 401) toast(error.message, true); }
|
||||
finally { opening = false; unlock.disabled = false; }
|
||||
}
|
||||
async function save() {
|
||||
if (!draft || saving) return;
|
||||
if (!dirty()) { finish(true); return; }
|
||||
saving = true;
|
||||
saveButton.disabled = cancelButton.disabled = unlock.disabled = true;
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
|
||||
});
|
||||
finish(true);
|
||||
toast(t("overviewLayoutEditorSaved"));
|
||||
} catch (error) { hint.textContent = error.message || t("overviewLayoutInlineError"); }
|
||||
finally { saving = false; saveButton.disabled = cancelButton.disabled = unlock.disabled = false; }
|
||||
}
|
||||
function cancel() { if (draft && !saving) finish(false); }
|
||||
function dropTarget(event) {
|
||||
const widget = event.target?.closest?.("[data-dashboard-widget]");
|
||||
return widget && grid.contains(widget) && !widget.hidden && visibleIds().includes(widget.dataset.dashboardWidget) ? widget : null;
|
||||
}
|
||||
function positionAfter(widget, event) {
|
||||
const rect = widget.getBoundingClientRect();
|
||||
return event.clientX >= rect.left + rect.width / 2;
|
||||
}
|
||||
grid?.addEventListener("dragstart", event => {
|
||||
const handle = event.target?.closest?.("[data-overview-drag-handle]");
|
||||
if (!draft || !handle || !grid.contains(handle)) { event.preventDefault(); return; }
|
||||
dragging = handle.dataset.overviewDragHandle;
|
||||
event.dataTransfer?.setData("text/plain", dragging);
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
||||
nodeFor(dragging)?.classList.add("is-overview-dragging");
|
||||
});
|
||||
grid?.addEventListener("dragover", event => {
|
||||
if (!draft || !dragging) return;
|
||||
const target = dropTarget(event);
|
||||
if (!target || target.dataset.dashboardWidget === dragging ||
|
||||
groupFor(target.dataset.dashboardWidget) !== groupFor(dragging)) return;
|
||||
event.preventDefault();
|
||||
for (const node of nodes()) node.classList.remove("is-overview-drop-before", "is-overview-drop-after");
|
||||
target.classList.add(positionAfter(target, event) ? "is-overview-drop-after" : "is-overview-drop-before");
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
});
|
||||
grid?.addEventListener("drop", event => {
|
||||
if (!draft || !dragging) return;
|
||||
event.preventDefault();
|
||||
const target = dropTarget(event), from = dragging;
|
||||
clearDrag();
|
||||
if (target && target.dataset.dashboardWidget !== from)
|
||||
move(from, target.dataset.dashboardWidget, positionAfter(target, event));
|
||||
});
|
||||
grid?.addEventListener("dragend", clearDrag);
|
||||
unlock?.addEventListener("click", () => { if (draft) void save(); else void begin(); });
|
||||
saveButton?.addEventListener("click", () => { void save(); });
|
||||
cancelButton?.addEventListener("click", cancel);
|
||||
return { isEditing: () => !!draft, cancel };
|
||||
})();
|
||||
@@ -0,0 +1,239 @@
|
||||
window.rustProxyDashboardLayout = (() => {
|
||||
const dialog = $("overview-layout-dialog");
|
||||
const visibleRoot = $("overview-layout-visible");
|
||||
const hiddenRoot = $("overview-layout-hidden");
|
||||
const saveButton = $("overview-layout-save");
|
||||
const cancelButton = $("overview-layout-cancel");
|
||||
const search = $("overview-layout-search");
|
||||
const typeFilter = $("overview-layout-type-filter");
|
||||
const message = $("overview-layout-message");
|
||||
const defaultButton = $("overview-layout-default");
|
||||
const resetButton = $("overview-layout-reset");
|
||||
const resetConfirm = $("overview-reset-confirm");
|
||||
const resetCancel = $("overview-reset-cancel");
|
||||
const resetApply = $("overview-reset-apply");
|
||||
let draft = null;
|
||||
let dragging = null;
|
||||
let saving = false;
|
||||
let opening = false;
|
||||
|
||||
function initialDraft() {
|
||||
const prefs = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS;
|
||||
const order = normalizeDashboardWidgets(prefs.dashboard_widgets);
|
||||
return { order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)) };
|
||||
}
|
||||
function defaultDraft() {
|
||||
return {
|
||||
order: [...ADMIN_DASHBOARD_WIDGETS],
|
||||
hidden: new Set(ADMIN_DEFAULT_HIDDEN_WIDGETS),
|
||||
};
|
||||
}
|
||||
function label(id) {
|
||||
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
|
||||
return widget ? t(widget.label_key) : id;
|
||||
}
|
||||
const groupFor = id => window.rustProxyMasonry?.groupFor?.(id) ?? 0;
|
||||
function visibleIds() {
|
||||
return draft.order.filter(id => !draft.hidden.has(id))
|
||||
.sort((a, b) => groupFor(a) - groupFor(b));
|
||||
}
|
||||
function reorderVisible(nextVisible) {
|
||||
const iter = nextVisible.values();
|
||||
draft.order = draft.order.map(id => draft.hidden.has(id) ? id : iter.next().value);
|
||||
render();
|
||||
}
|
||||
function move(id, target, after = false) {
|
||||
const next = visibleIds();
|
||||
if (id === target || !next.includes(id) || !next.includes(target) || groupFor(id) !== groupFor(target)) return;
|
||||
next.splice(next.indexOf(id), 1);
|
||||
next.splice(next.indexOf(target) + (after ? 1 : 0), 0, id);
|
||||
reorderVisible(next);
|
||||
}
|
||||
function nudge(id, direction) {
|
||||
const next = visibleIds(), index = next.indexOf(id), other = index + direction;
|
||||
if (index < 0 || other < 0 || other >= next.length || groupFor(id) !== groupFor(next[other])) return;
|
||||
[next[index], next[other]] = [next[other], next[index]];
|
||||
reorderVisible(next);
|
||||
visibleRoot.querySelectorAll(".overview-layout-tile").forEach(node => {
|
||||
if (node.dataset.previewWidget === id) node.querySelector(".overview-layout-up")?.focus();
|
||||
});
|
||||
}
|
||||
function preview(id) {
|
||||
const original = document.querySelector(`#panel-overview [data-dashboard-widget="${id}"]`);
|
||||
if (!original) return el("article", label(id), "stat");
|
||||
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");
|
||||
if (chart) {
|
||||
const graphic = chart.cloneNode(true);
|
||||
for (const node of [graphic, ...graphic.querySelectorAll("[id]")]) node.removeAttribute("id");
|
||||
graphic.removeAttribute("data-i18n-aria");
|
||||
graphic.setAttribute("aria-hidden", "true");
|
||||
card.append(graphic);
|
||||
}
|
||||
const detailKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
|
||||
if (detailKey) card.append(el("small", t(detailKey)));
|
||||
card.setAttribute("aria-hidden", "true");
|
||||
return card;
|
||||
}
|
||||
const copy = original.cloneNode(true);
|
||||
copy.hidden = false;
|
||||
copy.removeAttribute("data-dashboard-widget");
|
||||
for (const node of [copy, ...copy.querySelectorAll("[id]")]) node.removeAttribute("id");
|
||||
for (const node of copy.querySelectorAll("[aria-labelledby], [aria-describedby]")) {
|
||||
node.removeAttribute("aria-labelledby"); node.removeAttribute("aria-describedby");
|
||||
}
|
||||
copy.classList.add("overview-layout-card-preview");
|
||||
copy.setAttribute("aria-hidden", "true");
|
||||
return copy;
|
||||
}
|
||||
function render() {
|
||||
if (!draft || !dialog) return;
|
||||
const visible = visibleIds();
|
||||
const tiles = visible.map((id, index) => {
|
||||
const tile = el("div", undefined, "overview-layout-tile");
|
||||
tile.dataset.previewWidget = id;
|
||||
if (ADMIN_DASHBOARD_BY_ID.get(id)?.category === "panels") tile.classList.add("is-panel");
|
||||
tile.draggable = true;
|
||||
tile.setAttribute("role", "listitem");
|
||||
tile.setAttribute("aria-label", label(id));
|
||||
const toolbar = el("div", undefined, "overview-layout-tile-toolbar");
|
||||
const grip = el("span", "⠿", "overview-layout-grip");
|
||||
grip.setAttribute("aria-hidden", "true");
|
||||
const up = el("button", "←", "quiet overview-layout-up");
|
||||
up.type = "button"; up.disabled = index === 0 || groupFor(id) !== groupFor(visible[index - 1]);
|
||||
up.setAttribute("aria-label", `${t("moveUp")}: ${label(id)}`);
|
||||
up.addEventListener("click", () => nudge(id, -1));
|
||||
const down = el("button", "→", "quiet overview-layout-down");
|
||||
down.type = "button"; down.disabled = index === visible.length - 1 || groupFor(id) !== groupFor(visible[index + 1]);
|
||||
down.setAttribute("aria-label", `${t("moveDown")}: ${label(id)}`);
|
||||
down.addEventListener("click", () => nudge(id, 1));
|
||||
const hide = el("button", t("overviewLayoutEditorHide"), "quiet overview-layout-hide");
|
||||
hide.type = "button";
|
||||
hide.setAttribute("aria-label", `${t("overviewLayoutEditorHide")}: ${label(id)}`);
|
||||
hide.addEventListener("click", () => { draft.hidden.add(id); render(); });
|
||||
const controls = el("span", undefined, "overview-layout-controls");
|
||||
controls.append(up, down, hide);
|
||||
toolbar.append(grip, dashboardWidgetTypeBadge(id), controls);
|
||||
tile.append(toolbar, preview(id));
|
||||
tile.addEventListener("dragstart", event => {
|
||||
if (event.target !== tile) return;
|
||||
dragging = id;
|
||||
event.dataTransfer?.setData("text/plain", id);
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
||||
tile.classList.add("is-dragging");
|
||||
});
|
||||
tile.addEventListener("dragover", event => {
|
||||
if (!dragging || dragging === id || groupFor(dragging) !== groupFor(id)) return;
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
||||
tile.classList.add("is-drop-target");
|
||||
});
|
||||
tile.addEventListener("dragleave", () => tile.classList.remove("is-drop-target"));
|
||||
tile.addEventListener("drop", event => {
|
||||
event.preventDefault();
|
||||
tile.classList.remove("is-drop-target");
|
||||
if (dragging && dragging !== id) {
|
||||
const startIndex = visibleIds().indexOf(dragging);
|
||||
const targetIndex = visibleIds().indexOf(id);
|
||||
move(dragging, id, startIndex < targetIndex);
|
||||
}
|
||||
dragging = null;
|
||||
});
|
||||
tile.addEventListener("dragend", () => { dragging = null; tile.classList.remove("is-dragging", "is-drop-target"); });
|
||||
return tile;
|
||||
});
|
||||
visibleRoot.replaceChildren(...(tiles.length ? tiles : [el("p", t("overviewLayoutEditorEmpty"), "fine")]));
|
||||
const query = (search?.value || "").trim().toLocaleLowerCase();
|
||||
const hidden = draft.order.filter(id => draft.hidden.has(id)
|
||||
&& (typeFilter?.value === "all" || !typeFilter?.value || dashboardWidgetType(id) === typeFilter.value)
|
||||
&& (`${label(id)} ${dashboardWidgetTypeLabel(id)}`).toLocaleLowerCase().includes(query));
|
||||
const hiddenItems = hidden.map(id => {
|
||||
const item = el("div", undefined, "overview-layout-hidden-item");
|
||||
item.setAttribute("role", "listitem");
|
||||
const name = el("span", label(id));
|
||||
const meta = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
|
||||
if (meta) name.title = t(meta);
|
||||
const add = el("button", t("overviewLayoutEditorShow"), "outline");
|
||||
add.type = "button";
|
||||
add.setAttribute("aria-label", `${t("overviewLayoutEditorShow")}: ${label(id)}`);
|
||||
add.addEventListener("click", () => { draft.hidden.delete(id); render(); });
|
||||
item.append(name, dashboardWidgetTypeBadge(id), add);
|
||||
return item;
|
||||
});
|
||||
hiddenRoot.replaceChildren(...(hiddenItems.length || query ? hiddenItems : [el("p", t("overviewLayoutEditorHiddenEmpty"), "fine")]));
|
||||
}
|
||||
async function open() {
|
||||
if (dialog?.open || opening || window.rustProxyInlineLayout?.isEditing?.()) return;
|
||||
opening = true;
|
||||
try { await window.rustProxyPreferences?.load?.(); }
|
||||
catch (error) { if (error?.status === 401) { opening = false; return; } }
|
||||
opening = false;
|
||||
draft = initialDraft();
|
||||
dragging = null;
|
||||
search.value = "";
|
||||
if (typeFilter) typeFilter.value = "all";
|
||||
message.textContent = "";
|
||||
render();
|
||||
dialog.showModal();
|
||||
cancelButton.focus();
|
||||
}
|
||||
function setSaving(value) {
|
||||
saving = value;
|
||||
for (const control of [saveButton, cancelButton, defaultButton, resetButton, resetCancel, resetApply])
|
||||
if (control) control.disabled = value;
|
||||
}
|
||||
function restoreDefaultPreview() {
|
||||
if (saving || !draft) return;
|
||||
draft = defaultDraft();
|
||||
dragging = null;
|
||||
if (search) search.value = "";
|
||||
if (typeFilter) typeFilter.value = "all";
|
||||
message.textContent = t("overviewLayoutDefaultLoaded");
|
||||
render();
|
||||
}
|
||||
async function resetToDefault() {
|
||||
if (saving || !draft) return;
|
||||
setSaving(true);
|
||||
message.textContent = t("overviewLayoutEditorSaving");
|
||||
const defaults = defaultDraft();
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...defaults.order], dashboard_hidden_widgets: [...defaults.hidden],
|
||||
});
|
||||
resetConfirm.close();
|
||||
dialog.close();
|
||||
toast(t("overviewLayoutResetSaved"));
|
||||
} catch (error) { message.textContent = error.message; }
|
||||
finally { setSaving(false); }
|
||||
}
|
||||
async function save() {
|
||||
if (saving || !draft) return;
|
||||
setSaving(true);
|
||||
message.textContent = t("overviewLayoutEditorSaving");
|
||||
try {
|
||||
await window.rustProxyPreferences.update({
|
||||
dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden],
|
||||
});
|
||||
dialog.close();
|
||||
toast(t("overviewLayoutEditorSaved"));
|
||||
} catch (error) { message.textContent = error.message; }
|
||||
finally { setSaving(false); }
|
||||
}
|
||||
for (const id of ["overview-layout-open", "overview-layout-open-settings"]) $(id)?.addEventListener("click", () => { void open(); });
|
||||
cancelButton.addEventListener("click", () => { if (!saving) dialog.close(); });
|
||||
saveButton.addEventListener("click", () => { void save(); });
|
||||
defaultButton?.addEventListener("click", restoreDefaultPreview);
|
||||
resetButton?.addEventListener("click", () => {
|
||||
if (!saving && draft && !resetConfirm.open) resetConfirm.showModal();
|
||||
});
|
||||
resetCancel?.addEventListener("click", () => { if (!saving) resetConfirm.close(); });
|
||||
resetApply?.addEventListener("click", () => { void resetToDefault(); });
|
||||
resetConfirm?.addEventListener("cancel", event => { if (saving) event.preventDefault(); });
|
||||
search.addEventListener("input", render);
|
||||
typeFilter?.addEventListener("change", render);
|
||||
dialog.addEventListener("cancel", event => { if (saving || resetConfirm.open) event.preventDefault(); });
|
||||
dialog.addEventListener("close", () => { draft = null; dragging = null; message.textContent = ""; });
|
||||
return { open, refresh: () => { if (dialog.open) render(); } };
|
||||
})();
|
||||
@@ -0,0 +1,128 @@
|
||||
window.rustProxyExtraOverview = (() => {
|
||||
const limit = 30;
|
||||
const names = [
|
||||
"extra_process_cpu", "extra_process_rss", "extra_process_fds", "extra_open_connections",
|
||||
"extra_websocket_active", "extra_tls_error_rate", "extra_request_error_rate",
|
||||
"extra_cache_hit_rate", "extra_cache_memory", "extra_h2_share",
|
||||
"extra_response_latency", "extra_backend_unhealthy",
|
||||
"extra_total_rps", "extra_active_exchanges", "extra_h1_share", "extra_h3_share",
|
||||
"extra_cache_miss_rate", "extra_backend_active",
|
||||
];
|
||||
const histories = new Map();
|
||||
const lastCounter = new Map();
|
||||
const lastValue = new Map();
|
||||
const lastUnits = new Map();
|
||||
const positive = value => value != null && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : null;
|
||||
const node = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
|
||||
function elapsedDelta(id, counters, now) {
|
||||
const old = lastCounter.get(id);
|
||||
const next = { values: counters, at: now };
|
||||
lastCounter.set(id, next);
|
||||
if (!old || now <= old.at || counters.some((v, i) => v < old.values[i])) return null;
|
||||
return { deltas: counters.map((v, i) => v - old.values[i]), seconds: (now - old.at) / 1000 };
|
||||
}
|
||||
function read(id, status, now, record) {
|
||||
const m = status.metrics || {}, d = status.system || {}, c = status.cache || {};
|
||||
const direct = {
|
||||
extra_process_cpu: d.process_cpu_percent,
|
||||
extra_process_rss: d.process_rss_bytes,
|
||||
extra_process_fds: d.open_fds,
|
||||
extra_open_connections: m.active_connections,
|
||||
extra_websocket_active: m.websocket_active,
|
||||
extra_response_latency: m.mean_duration_ms,
|
||||
extra_active_exchanges: m.active,
|
||||
};
|
||||
if (Object.hasOwn(direct, id)) {
|
||||
const numeric = positive(direct[id]);
|
||||
return {
|
||||
value: numeric, unit: id === "extra_process_cpu" ? "percent" : id === "extra_process_rss" ? "bytes" : id === "extra_response_latency" ? "ms" : "count",
|
||||
detail: id === "extra_process_fds" && positive(d.max_fds) ? `${t("overviewExtraFdLimit")}: ${fmt(d.max_fds)}` : ""
|
||||
};
|
||||
}
|
||||
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: ""
|
||||
};
|
||||
}
|
||||
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: ""
|
||||
};
|
||||
}
|
||||
if (id === "extra_cache_memory") {
|
||||
const bytes = positive(c.resident_bytes), budget = positive(c.hot_limit_bytes);
|
||||
const percent = budget && bytes !== null ? bytes / budget * 100 : null;
|
||||
return {
|
||||
value: percent === null ? bytes : percent, unit: percent === null ? "bytes" : "percent",
|
||||
detail: bytes === null ? "" : `${fmtBytes(bytes)}${budget ? ` / ${fmtBytes(budget)}` : ""}`
|
||||
};
|
||||
}
|
||||
if (!["extra_tls_error_rate", "extra_request_error_rate", "extra_cache_hit_rate", "extra_h2_share",
|
||||
"extra_total_rps", "extra_h1_share", "extra_h3_share", "extra_cache_miss_rate"].includes(id))
|
||||
return { value: null, unit: "count", detail: "" };
|
||||
const fields = ({
|
||||
extra_tls_error_rate: [m.tls_handshake_errors],
|
||||
extra_request_error_rate: [m.errors],
|
||||
extra_cache_hit_rate: [c.hits, c.misses],
|
||||
extra_h2_share: [m.http2_requests, m.requests],
|
||||
extra_h1_share: [m.http1_requests, m.requests],
|
||||
extra_h3_share: [m.http3_requests, m.requests],
|
||||
extra_total_rps: [m.requests],
|
||||
extra_cache_miss_rate: [c.misses],
|
||||
})[id].map(positive);
|
||||
const unit = ["extra_cache_hit_rate", "extra_h2_share", "extra_h1_share", "extra_h3_share"].includes(id)
|
||||
? "percent" : "rate";
|
||||
if (fields.some(v => v === null)) { lastCounter.delete(id); return { value: null, unit, detail: "" }; }
|
||||
if (!record) return { value: lastValue.get(id) ?? null, unit, detail: "" };
|
||||
const result = elapsedDelta(id, fields, now);
|
||||
if (!result) return { value: null, unit, detail: "" };
|
||||
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 ? Math.min(100, deltas[0] / sum * 100) : null, unit, detail: "" };
|
||||
}
|
||||
function format(value, unit) {
|
||||
if (value == null) return "-";
|
||||
if (unit === "percent") return `${fmt(value, 1)}%`;
|
||||
if (unit === "bytes") return fmtBytes(value);
|
||||
if (unit === "ms") return `${fmt(value, 1)} ms`;
|
||||
if (unit === "rate") return `${fmt(value, 1)}/s`;
|
||||
return fmt(value);
|
||||
}
|
||||
function points(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(" ");
|
||||
}
|
||||
function render(status, record = true) {
|
||||
if (!status || currentPanel !== "overview") return;
|
||||
const now = performance.now();
|
||||
for (const id of names) {
|
||||
const tile = node(id);
|
||||
if (!tile || tile.hidden) { lastCounter.delete(id); continue; }
|
||||
const metric = read(id, status, now, record);
|
||||
const value = metric.value;
|
||||
if (lastUnits.get(id) !== metric.unit) {
|
||||
histories.delete(id);
|
||||
lastUnits.set(id, metric.unit);
|
||||
}
|
||||
const history = histories.get(id) || [];
|
||||
if (record && value !== null) {
|
||||
history.push(value);
|
||||
if (history.length > limit) history.shift();
|
||||
histories.set(id, history);
|
||||
}
|
||||
if (record) lastValue.set(id, value);
|
||||
const field = $(`overview-extra-${id}`);
|
||||
if (field) field.textContent = format(value, metric.unit);
|
||||
const detail = $(`overview-extra-${id}-detail`);
|
||||
if (detail) detail.textContent = metric.detail || t("overviewExtraRecentSamples");
|
||||
$(`overview-extra-${id}-line`)?.setAttribute("points", points(history, metric.unit));
|
||||
}
|
||||
}
|
||||
function reset() { histories.clear(); lastCounter.clear(); lastValue.clear(); lastUnits.clear(); }
|
||||
return { render, reset };
|
||||
})();
|
||||
@@ -0,0 +1,38 @@
|
||||
window.rustProxyMasonry = (() => {
|
||||
const grid = document.getElementById("dashboard-overview-grid");
|
||||
if (!grid) return { refresh() { } };
|
||||
const widgets = () => [...grid.children].filter(node => node.dataset.dashboardWidget);
|
||||
const group = node => node.classList.contains("pool-card") ? 4
|
||||
: node.classList.contains("stat") ? 0
|
||||
: node.classList.contains("overview-wide-chart") ? 2
|
||||
: node.dataset.dashboardWidget?.endsWith("_diagnostics") ? 3 : 1;
|
||||
|
||||
function refresh() {
|
||||
const previous = widgets();
|
||||
const ordered = previous.map((node, index) => ({ node, index }))
|
||||
.sort((a, b) => group(a.node) - group(b.node) || a.index - b.index)
|
||||
.map(item => item.node);
|
||||
if (ordered.some((node, i) => node !== previous[i])) grid.append(...ordered);
|
||||
let lastGroup = -1;
|
||||
for (const node of ordered) {
|
||||
for (const key of ["position", "left", "top", "width", "minHeight", "gridColumnStart",
|
||||
"gridColumnEnd", "gridRowStart", "gridRowEnd", "alignSelf"]) node.style[key] = "";
|
||||
node.classList.remove("overview-row-start");
|
||||
if (node.hidden) continue;
|
||||
const current = group(node);
|
||||
if (current !== lastGroup) node.classList.add("overview-row-start");
|
||||
lastGroup = current;
|
||||
}
|
||||
grid.classList.remove("is-packed");
|
||||
grid.classList.add("is-aligned");
|
||||
grid.style.height = "";
|
||||
}
|
||||
refresh();
|
||||
return {
|
||||
refresh,
|
||||
groupFor(id) {
|
||||
const node = widgets().find(item => item.dataset.dashboardWidget === id);
|
||||
return node ? group(node) : -1;
|
||||
},
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,106 @@
|
||||
window.rustProxyWideOverview = (() => {
|
||||
const limit = 60;
|
||||
const configurations = {
|
||||
wide_protocol_rates: {
|
||||
unit: "rate", names: ["h1", "h2", "h3"], counters: s =>
|
||||
[s.metrics?.http1_requests, s.metrics?.http2_requests, s.metrics?.http3_requests]
|
||||
},
|
||||
wide_cpu: {
|
||||
unit: "percent", names: ["system", "process"], values: s =>
|
||||
[s.system?.cpu_percent, s.system?.process_cpu_percent]
|
||||
},
|
||||
wide_memory: {
|
||||
unit: "bytes", names: ["process", "cache"], values: s =>
|
||||
[s.system?.process_rss_bytes, s.cache?.resident_bytes]
|
||||
},
|
||||
wide_cache_activity: {
|
||||
unit: "rate", names: ["hits", "misses"], counters: s =>
|
||||
[s.cache?.hits, s.cache?.misses]
|
||||
},
|
||||
wide_connections: {
|
||||
unit: "count", names: ["open", "websockets"], values: s =>
|
||||
[s.metrics?.active_connections, s.metrics?.websocket_active]
|
||||
},
|
||||
wide_backend_health: {
|
||||
unit: "count", names: ["up", "down"], values: s => {
|
||||
if (!Array.isArray(s.backend_pools)) return [null, null];
|
||||
const members = s.backend_pools.flatMap(pool => pool.members || []);
|
||||
return [members.filter(m => m.status === "up").length, members.filter(m => m.status === "down").length];
|
||||
}
|
||||
},
|
||||
wide_latency: {
|
||||
unit: "ms", names: ["response", "probe"], values: s => {
|
||||
if (!Array.isArray(s.backend_pools)) return [s.metrics?.mean_duration_ms, null];
|
||||
const members = s.backend_pools.flatMap(pool => pool.members || []);
|
||||
const probes = members.filter(m => m.status === "up" && m.health_mode !== "off"
|
||||
&& finite(m.last_probe_ms) !== null && finite(m.latency_ms) !== null
|
||||
&& Number(m.last_probe_ms) > 0);
|
||||
return [s.metrics?.mean_duration_ms,
|
||||
probes.length ? probes.reduce((total, m) => total + Number(m.latency_ms), 0) / probes.length : null];
|
||||
}
|
||||
},
|
||||
};
|
||||
const history = new Map(), baselines = new Map(), last = new Map();
|
||||
let revision = null;
|
||||
const finite = value => value != null && Number.isFinite(Number(value)) && Number(value) >= 0 ? Number(value) : null;
|
||||
const format = (n, unit) => n === null ? "-" : unit === "bytes" ? fmtBytes(n)
|
||||
: unit === "percent" ? `${fmt(n, 1)}%` : unit === "ms" ? `${fmt(n, 1)} ms`
|
||||
: unit === "rate" ? `${fmt(n, 1)}/s` : fmt(n);
|
||||
const tile = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
|
||||
function counterRates(id, values, now, record) {
|
||||
const counters = values.map(finite);
|
||||
if (counters.some(n => n === null)) { baselines.delete(id); return counters.map(() => null); }
|
||||
if (!record) return last.get(id) || counters.map(() => null);
|
||||
const previous = baselines.get(id);
|
||||
baselines.set(id, { counters, now });
|
||||
if (!previous || now <= previous.now || counters.some((n, i) => n < previous.counters[i]))
|
||||
return counters.map(() => null);
|
||||
const elapsed = (now - previous.now) / 1000;
|
||||
return counters.map((n, i) => (n - previous.counters[i]) / elapsed);
|
||||
}
|
||||
function path(values, peak) {
|
||||
const count = values.length;
|
||||
let result = "", drawing = false;
|
||||
for (let i = 0; i < count; i++) {
|
||||
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);
|
||||
result += `${drawing ? " L" : " M"}${x} ${y}`;
|
||||
drawing = true;
|
||||
}
|
||||
return result.trim();
|
||||
}
|
||||
function render(status, record = true) {
|
||||
if (!status || currentPanel !== "overview") return;
|
||||
if (revision !== status.revision) { reset(); revision = status.revision; }
|
||||
const now = performance.now();
|
||||
for (const [id, config] of Object.entries(configurations)) {
|
||||
const node = tile(id);
|
||||
if (!node || node.hidden) { baselines.delete(id); continue; }
|
||||
const values = config.counters ? counterRates(id, config.counters(status), now, record)
|
||||
: config.values(status).map(finite);
|
||||
const series = history.get(id) || config.names.map(() => []);
|
||||
if (record) {
|
||||
values.forEach((value, i) => {
|
||||
series[i].push(value);
|
||||
if (series[i].length > limit) series[i].shift();
|
||||
});
|
||||
history.set(id, series);
|
||||
last.set(id, values);
|
||||
}
|
||||
const peak = Math.max(config.unit === "percent" ? 100 : 1,
|
||||
...series.flat().filter(n => n !== null));
|
||||
config.names.forEach((key, i) => {
|
||||
const value = values[i];
|
||||
const field = $(`overview-wide-${id}-${key}-value`);
|
||||
if (field) field.textContent = format(value, config.unit);
|
||||
$(`overview-wide-${id}-${key}-line`)?.setAttribute("d", path(series[i], peak));
|
||||
});
|
||||
const summary = $(`overview-wide-${id}-summary`);
|
||||
if (summary) summary.textContent = format(values[0], config.unit);
|
||||
}
|
||||
}
|
||||
function reset() { history.clear(); baselines.clear(); last.clear(); revision = null; }
|
||||
return { render, reset };
|
||||
})();
|
||||
@@ -1,12 +1,15 @@
|
||||
const ADMIN_THEMES = new Set(["dark", "light"]);
|
||||
const ADMIN_THEMES = new Set(["dark", "ultra_dark", "light", "cream"]);
|
||||
const ADMIN_TIME_FORMATS = new Set(["24h", "12h"]);
|
||||
const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool"]);
|
||||
const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]);
|
||||
const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]);
|
||||
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(["requests", "active", "bandwidth", "errors", "cache", "memory", "websocket", "http2", "http3", "tls", "http_protocol", "websocket_protocol", "stream_overview", "stream_traffic", "stream_sessions", "stream_backend_health"]);
|
||||
const ADMIN_CORNER_STYLES = new Set(["default", "square", "subtle", "rounded", "extra"]);
|
||||
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => widget.id));
|
||||
const ADMIN_DASHBOARD_BY_ID = new Map(ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => [widget.id, widget]));
|
||||
const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze(ADMIN_DASHBOARD_WIDGET_REGISTRY.filter(widget => widget.default_hidden).map(widget => widget.id));
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
||||
button_style: "normal", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: ["stream_overview", "stream_traffic", "stream_sessions", "stream_backend_health"],
|
||||
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS],
|
||||
});
|
||||
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
|
||||
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
|
||||
@@ -17,6 +20,7 @@ function normalizeAdminUiScale(value) { const numeric = Number(value); return Nu
|
||||
function normalizeAdminTimeFormat(value) { return ADMIN_TIME_FORMATS.has(value) ? value : "24h"; }
|
||||
function normalizeAdminChartStyle(value) { return ADMIN_CHART_STYLES.has(value) ? value : "default"; }
|
||||
function normalizeAdminButtonStyle(value) { return ADMIN_BUTTON_STYLES.has(value) ? value : "normal"; }
|
||||
function normalizeAdminCornerStyle(value) { return ADMIN_CORNER_STYLES.has(value) ? value : "default"; }
|
||||
function normalizeAdminDensity(value, fallback = "normal") { return ADMIN_DENSITIES.has(value) ? value : fallback; }
|
||||
function normalizeDashboardWidgets(value) {
|
||||
const result = [], seen = new Set();
|
||||
@@ -28,7 +32,7 @@ function normalizeHiddenDashboardWidgets(value, savedOrder = ADMIN_DASHBOARD_WID
|
||||
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_DASHBOARD_WIDGETS.filter(id => id.startsWith("stream_"))) {
|
||||
for (const id of ADMIN_DEFAULT_HIDDEN_WIDGETS) {
|
||||
if (!savedOrder?.includes(id) && !seen.has(id)) { hidden.push(id); seen.add(id); }
|
||||
}
|
||||
return hidden;
|
||||
@@ -73,6 +77,13 @@ function applyAdminButtonStyle(value) {
|
||||
const style = normalizeAdminButtonStyle(value); document.documentElement.dataset.buttonStyle = style;
|
||||
const select = $("ui-button-style"); if (select) select.value = style; return style;
|
||||
}
|
||||
function applyAdminCornerStyle(value, cache = true) {
|
||||
const style = normalizeAdminCornerStyle(value);
|
||||
document.documentElement.dataset.cornerStyle = style;
|
||||
const select = $("ui-corner-style"); if (select) select.value = style;
|
||||
if (cache) { try { localStorage.setItem("rp-corner-style", style); } catch { } }
|
||||
return style;
|
||||
}
|
||||
function applyAdminTableDensity(value) {
|
||||
const density = normalizeAdminDensity(value); document.documentElement.dataset.tableDensity = density;
|
||||
const select = $("ui-table-density"); if (select) select.value = density; return density;
|
||||
@@ -87,13 +98,29 @@ function applyDashboardLayout(order, hidden) {
|
||||
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
|
||||
for (const id of normalizedOrder) { const node = nodes.get(id); if (!node) continue; node.hidden = hiddenSet.has(id); grid.append(node); }
|
||||
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview")
|
||||
window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false);
|
||||
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
|
||||
window.rustProxyMasonry?.refresh?.();
|
||||
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); }
|
||||
return { order: normalizedOrder, hidden: [...hiddenSet] };
|
||||
}
|
||||
function dashboardWidgetLabel(id) {
|
||||
const node = document.querySelector(`[data-dashboard-widget="${CSS.escape(id)}"]`);
|
||||
return node?.querySelector("[data-i18n]")?.textContent?.trim() || id;
|
||||
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
|
||||
return widget ? t(widget.label_key) : id;
|
||||
}
|
||||
const DASHBOARD_DISPLAY_TYPE_KEYS = Object.freeze({
|
||||
value: "overviewBlockTypeValue", value_chart: "overviewBlockTypeValueChart",
|
||||
chart: "overviewBlockTypeChart", panel: "overviewBlockTypePanel", table: "overviewBlockTypeTable",
|
||||
});
|
||||
function dashboardWidgetType(id) {
|
||||
return ADMIN_DASHBOARD_BY_ID.get(id)?.display_type || "value";
|
||||
}
|
||||
function dashboardWidgetTypeLabel(id) {
|
||||
return t(DASHBOARD_DISPLAY_TYPE_KEYS[dashboardWidgetType(id)] || "overviewBlockTypeValue");
|
||||
}
|
||||
function dashboardWidgetTypeBadge(id) {
|
||||
const tag = el("span", dashboardWidgetTypeLabel(id), "overview-widget-type");
|
||||
tag.dataset.blockType = dashboardWidgetType(id);
|
||||
return tag;
|
||||
}
|
||||
function renderDashboardWidgetSettings() {
|
||||
const list = $("dashboard-widget-list"); if (!list) return;
|
||||
@@ -102,12 +129,14 @@ function renderDashboardWidgetSettings() {
|
||||
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
|
||||
const visible = !hidden.has(id);
|
||||
const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
|
||||
const descriptionKey = ({ stream_overview: "streamWidgetOverviewDescription", stream_traffic: "streamWidgetTrafficDescription", stream_sessions: "streamWidgetSessionsDescription", stream_backend_health: "streamWidgetHealthDescription" })[id];
|
||||
const descriptionKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
|
||||
if (descriptionKey) {
|
||||
const description = t(descriptionKey);
|
||||
row.title = description;
|
||||
toggle.setAttribute("aria-description", description);
|
||||
}
|
||||
// Type badges belong in the visual layout editor, not the long
|
||||
// Appearance settings list where they crowd the visibility controls.
|
||||
toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
|
||||
const actions = el("span", undefined, "dashboard-widget-setting-actions");
|
||||
const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp"));
|
||||
@@ -136,7 +165,7 @@ function refreshChartStylePalette(active = adminPreferences?.chart_style || "def
|
||||
});
|
||||
target.replaceChildren(...buttons);
|
||||
}
|
||||
function refreshAppearanceUi() { refreshChartStylePalette(); renderDashboardWidgetSettings(); renderRefreshPreferences(adminPreferences?.refresh); window.rustProxySettings?.refreshSearch?.(); }
|
||||
function refreshAppearanceUi() { refreshChartStylePalette(); renderDashboardWidgetSettings(); renderRefreshPreferences(adminPreferences?.refresh); window.rustProxyDashboardLayout?.refresh?.(); window.rustProxySettings?.refreshSearch?.(); }
|
||||
function formatDateTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
|
||||
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) {
|
||||
@@ -146,7 +175,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), table_density: applyAdminTableDensity(value.table_density),
|
||||
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),
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: dashboard.order, dashboard_hidden_widgets: dashboard.hidden,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
@@ -162,6 +191,7 @@ function applyAdminPreferences(value, cache = true) {
|
||||
}
|
||||
try { applyAdminTheme(localStorage.getItem("rp-theme") || "dark", false); } catch { applyAdminTheme("dark", false); }
|
||||
try { applyAdminUiScale(localStorage.getItem("rp-ui-scale") || 100, false); } catch { applyAdminUiScale(100, false); }
|
||||
try { applyAdminCornerStyle(localStorage.getItem("rp-corner-style") || "default", false); } catch { applyAdminCornerStyle("default", false); }
|
||||
|
||||
async function loadAdminPreferences(force = false) {
|
||||
if (!authenticated) return adminPreferences;
|
||||
@@ -193,7 +223,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.table_density = normalizeAdminDensity(next.table_density);
|
||||
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.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets);
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
@@ -273,6 +303,15 @@ $("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); });
|
||||
});
|
||||
$("ui-corner-style")?.addEventListener("change", () => {
|
||||
const control = $("ui-corner-style"), previous = adminPreferences?.corner_style || "default", next = normalizeAdminCornerStyle(control.value);
|
||||
applyAdminCornerStyle(next);
|
||||
if (adminPreferences) adminPreferences.corner_style = next;
|
||||
void saveAppearancePreference({ corner_style: next }, control, () => {
|
||||
if (adminPreferences) adminPreferences.corner_style = previous;
|
||||
applyAdminCornerStyle(previous);
|
||||
});
|
||||
});
|
||||
$("ui-table-density")?.addEventListener("change", () => {
|
||||
const control = $("ui-table-density"), previous = adminPreferences?.table_density || "normal", next = normalizeAdminDensity(control.value);
|
||||
applyAdminTableDensity(next); if (adminPreferences) adminPreferences.table_density = next; void saveAppearancePreference({ table_density: next }, control, () => { if (adminPreferences) adminPreferences.table_density = previous; applyAdminTableDensity(previous); });
|
||||
@@ -291,7 +330,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" }, 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" }, tables: { table_density: "normal" },
|
||||
dashboard: { dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets] },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
|
||||
@@ -260,7 +260,6 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
$("metric-h3").textContent = fmt(m.http3_requests);
|
||||
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
|
||||
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
|
||||
$("metric-rss").closest("article.stat").hidden = d.process_rss_bytes == null;
|
||||
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
|
||||
$("process-summary").hidden = d.process_threads == null;
|
||||
if (d.process_threads != null) $("process-summary").textContent = `${fmt(d.process_threads)} ${t("threads")}`;
|
||||
|
||||
@@ -0,0 +1,684 @@
|
||||
[
|
||||
{
|
||||
"id": "requests",
|
||||
"label_key": "requests",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"requests\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "active",
|
||||
"label_key": "activeExchanges",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"active\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "bandwidth",
|
||||
"label_key": "bandwidth",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"bandwidth\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "errors",
|
||||
"label_key": "errors",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"errors\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "cache",
|
||||
"label_key": "cacheHitRatio",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"cache\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "memory",
|
||||
"label_key": "processMemory",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"memory\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "websocket",
|
||||
"label_key": "wsActive",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"websocket\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "http2",
|
||||
"label_key": "h2Requests",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"http2\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "http3",
|
||||
"label_key": "h3Requests",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"http3\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "tls",
|
||||
"label_key": "tlsFailures",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"tls\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "http_protocol",
|
||||
"label_key": "httpProtocolMix",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"http_protocol\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "websocket_protocol",
|
||||
"label_key": "wsProtocolMix",
|
||||
"description_key": null,
|
||||
"element_selector": "[data-dashboard-widget=\"websocket_protocol\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "overview",
|
||||
"default_hidden": false,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "stream_overview",
|
||||
"label_key": "streamWidgetOverview",
|
||||
"description_key": "streamWidgetOverviewDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"stream_overview\"]",
|
||||
"data_source": "stream_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "streams",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "stream_traffic",
|
||||
"label_key": "streamWidgetTraffic",
|
||||
"description_key": "streamWidgetTrafficDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"stream_traffic\"]",
|
||||
"data_source": "stream_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "streams",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "stream_sessions",
|
||||
"label_key": "streamWidgetSessions",
|
||||
"description_key": "streamWidgetSessionsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"stream_sessions\"]",
|
||||
"data_source": "stream_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "streams",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "stream_backend_health",
|
||||
"label_key": "streamWidgetHealth",
|
||||
"description_key": "streamWidgetHealthDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"stream_backend_health\"]",
|
||||
"data_source": "stream_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "streams",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "backend_health",
|
||||
"label_key": "backendWidgetHealth",
|
||||
"description_key": "backendWidgetHealthDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_health\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "backend_activity",
|
||||
"label_key": "backendWidgetActivity",
|
||||
"description_key": "backendWidgetActivityDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_activity\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_errors",
|
||||
"label_key": "backendWidgetErrors",
|
||||
"description_key": "backendWidgetErrorsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_errors\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_latency",
|
||||
"label_key": "backendWidgetLatency",
|
||||
"description_key": "backendWidgetLatencyDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_latency\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_traffic",
|
||||
"label_key": "backendWidgetTraffic",
|
||||
"description_key": "backendWidgetTrafficDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_traffic\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_protocols",
|
||||
"label_key": "backendWidgetProtocols",
|
||||
"description_key": "backendWidgetProtocolsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_protocols\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_requests",
|
||||
"label_key": "backendWidgetRequests",
|
||||
"description_key": "backendWidgetRequestsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_requests\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_capacity",
|
||||
"label_key": "backendWidgetCapacity",
|
||||
"description_key": "backendWidgetCapacityDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_capacity\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_http5xx",
|
||||
"label_key": "backendWidgetHttp5xx",
|
||||
"description_key": "backendWidgetHttp5xxDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_http5xx\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_timeouts",
|
||||
"label_key": "backendWidgetTimeouts",
|
||||
"description_key": "backendWidgetTimeoutsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_timeouts\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_network",
|
||||
"label_key": "backendWidgetNetwork",
|
||||
"description_key": "backendWidgetNetworkDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_network\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_retries",
|
||||
"label_key": "backendWidgetRetries",
|
||||
"description_key": "backendWidgetRetriesDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_retries\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_queue",
|
||||
"label_key": "backendWidgetQueue",
|
||||
"description_key": "backendWidgetQueueDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_queue\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "backend_rotations",
|
||||
"label_key": "backendWidgetRotations",
|
||||
"description_key": "backendWidgetRotationsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_rotations\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "backends",
|
||||
"default_hidden": true,
|
||||
"display_type": "value"
|
||||
},
|
||||
{
|
||||
"id": "extra_process_cpu",
|
||||
"label_key": "overviewExtraProcessCpu",
|
||||
"description_key": "overviewExtraProcessCpuDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_process_cpu\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_process_rss",
|
||||
"label_key": "overviewExtraProcessRss",
|
||||
"description_key": "overviewExtraProcessRssDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_process_rss\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_process_fds",
|
||||
"label_key": "overviewExtraProcessFds",
|
||||
"description_key": "overviewExtraProcessFdsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_process_fds\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_open_connections",
|
||||
"label_key": "overviewExtraOpenConnections",
|
||||
"description_key": "overviewExtraOpenConnectionsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_open_connections\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_websocket_active",
|
||||
"label_key": "overviewExtraWebsocketActive",
|
||||
"description_key": "overviewExtraWebsocketActiveDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_websocket_active\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_tls_error_rate",
|
||||
"label_key": "overviewExtraTlsErrorRate",
|
||||
"description_key": "overviewExtraTlsErrorRateDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_tls_error_rate\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_request_error_rate",
|
||||
"label_key": "overviewExtraRequestErrorRate",
|
||||
"description_key": "overviewExtraRequestErrorRateDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_request_error_rate\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_cache_hit_rate",
|
||||
"label_key": "overviewExtraCacheHitRate",
|
||||
"description_key": "overviewExtraCacheHitRateDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_cache_hit_rate\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_cache_memory",
|
||||
"label_key": "overviewExtraCacheMemory",
|
||||
"description_key": "overviewExtraCacheMemoryDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_cache_memory\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_h2_share",
|
||||
"label_key": "overviewExtraH2Share",
|
||||
"description_key": "overviewExtraH2ShareDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_h2_share\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_response_latency",
|
||||
"label_key": "overviewExtraResponseLatency",
|
||||
"description_key": "overviewExtraResponseLatencyDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_response_latency\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_backend_unhealthy",
|
||||
"label_key": "overviewExtraBackendUnhealthy",
|
||||
"description_key": "overviewExtraBackendUnhealthyDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_backend_unhealthy\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_total_rps",
|
||||
"label_key": "overviewExtraTotalRps",
|
||||
"description_key": "overviewExtraTotalRpsDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_total_rps\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_active_exchanges",
|
||||
"label_key": "overviewExtraActiveExchanges",
|
||||
"description_key": "overviewExtraActiveExchangesDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_active_exchanges\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_h1_share",
|
||||
"label_key": "overviewExtraH1Share",
|
||||
"description_key": "overviewExtraH1ShareDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_h1_share\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_h3_share",
|
||||
"label_key": "overviewExtraH3Share",
|
||||
"description_key": "overviewExtraH3ShareDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_h3_share\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_cache_miss_rate",
|
||||
"label_key": "overviewExtraCacheMissRate",
|
||||
"description_key": "overviewExtraCacheMissRateDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_cache_miss_rate\"]",
|
||||
"data_source": "cache_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "extra_backend_active",
|
||||
"label_key": "overviewExtraBackendActive",
|
||||
"description_key": "overviewExtraBackendActiveDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"extra_backend_active\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "performance",
|
||||
"default_hidden": true,
|
||||
"display_type": "value_chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_protocol_rates",
|
||||
"label_key": "overviewWideProtocolRates",
|
||||
"description_key": "overviewWideProtocolRatesDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_protocol_rates\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_cpu",
|
||||
"label_key": "overviewWideCpu",
|
||||
"description_key": "overviewWideCpuDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_cpu\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_memory",
|
||||
"label_key": "overviewWideMemory",
|
||||
"description_key": "overviewWideMemoryDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_memory\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_cache_activity",
|
||||
"label_key": "overviewWideCacheActivity",
|
||||
"description_key": "overviewWideCacheActivityDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_cache_activity\"]",
|
||||
"data_source": "cache_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_connections",
|
||||
"label_key": "overviewWideConnections",
|
||||
"description_key": "overviewWideConnectionsDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_connections\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_backend_health",
|
||||
"label_key": "overviewWideBackendHealth",
|
||||
"description_key": "overviewWideBackendHealthDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_backend_health\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "wide_latency",
|
||||
"label_key": "overviewWideLatency",
|
||||
"description_key": "overviewWideLatencyDetail",
|
||||
"element_selector": "[data-dashboard-widget=\"wide_latency\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": true,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "traffic_chart",
|
||||
"label_key": "requestsPerSecond",
|
||||
"description_key": "overviewPanelTrafficDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"traffic_chart\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "system_chart",
|
||||
"label_key": "systemLoad",
|
||||
"description_key": "overviewPanelSystemDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"system_chart\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "connection_chart",
|
||||
"label_key": "connectionActivity",
|
||||
"description_key": "overviewPanelConnectionsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"connection_chart\"]",
|
||||
"data_source": "status_metrics",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "chart"
|
||||
},
|
||||
{
|
||||
"id": "cache_diagnostics",
|
||||
"label_key": "cacheOverview",
|
||||
"description_key": "overviewPanelCacheDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"cache_diagnostics\"]",
|
||||
"data_source": "cache_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "panel"
|
||||
},
|
||||
{
|
||||
"id": "process_diagnostics",
|
||||
"label_key": "processDiagnostics",
|
||||
"description_key": "overviewPanelProcessDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"process_diagnostics\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "panel"
|
||||
},
|
||||
{
|
||||
"id": "network_diagnostics",
|
||||
"label_key": "networkDiagnostics",
|
||||
"description_key": "overviewPanelNetworkDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"network_diagnostics\"]",
|
||||
"data_source": "system_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "panel"
|
||||
},
|
||||
{
|
||||
"id": "backend_table",
|
||||
"label_key": "vhHealthTitle",
|
||||
"description_key": "overviewPanelBackendsDescription",
|
||||
"element_selector": "[data-dashboard-widget=\"backend_table\"]",
|
||||
"data_source": "backend_status",
|
||||
"no_data": "neutral_dash",
|
||||
"category": "panels",
|
||||
"default_hidden": false,
|
||||
"display_type": "table"
|
||||
}
|
||||
]
|
||||
+665
-112
@@ -8,12 +8,10 @@
|
||||
<title>RustProxy / Control room</title>
|
||||
<link href="/favicon.svg" rel="icon" />
|
||||
<link href="/assets/style___RUSTPROXY_ASSET_ID__.css" rel="stylesheet" />
|
||||
<!-- One ordered JS payload: no race between module fetches and session/config API. -->
|
||||
<script defer="" src="/assets/app___RUSTPROXY_ASSET_ID__.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Visible even when the dashboard JavaScript fails to initialize. -->
|
||||
<div class="app-startup" id="app-startup" role="status" aria-live="polite">
|
||||
<div class="app-startup-card">
|
||||
<div class="app-startup-brand" aria-hidden="true">r<span>/</span>p</div>
|
||||
@@ -121,12 +119,30 @@
|
||||
<h1 id="page-title">Traffic, in focus.</h1>
|
||||
<button class="quiet overview-refresh" data-i18n="refreshNow" id="overview-refresh"
|
||||
type="button" hidden>Refresh now</button>
|
||||
<button class="quiet overview-layout-open" data-i18n="overviewLayoutEditorOpen"
|
||||
id="overview-layout-open" type="button" hidden>Arrange cards visually</button>
|
||||
<button class="outline" id="overview-layout-unlock" type="button" hidden
|
||||
data-i18n="overviewLayoutUnlock" aria-pressed="false">Unlock layout</button>
|
||||
<div class="overview-inline-actions" id="overview-inline-actions" hidden>
|
||||
<button class="primary" id="overview-inline-save" type="button"
|
||||
data-i18n="overviewLayoutInlineSave">Save and lock</button>
|
||||
<button class="quiet" id="overview-inline-cancel" type="button"
|
||||
data-i18n="overviewLayoutInlineCancel">Cancel changes</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="lead" id="page-description"></p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" id="panel-overview">
|
||||
<div class="stats-grid">
|
||||
<p class="fine overview-inline-hint" id="overview-inline-hint" role="status" aria-live="polite"
|
||||
hidden data-i18n="overviewLayoutInlineHint">Layout unlocked: reorder blocks within size groups,
|
||||
hide with × and save or cancel.</p>
|
||||
<details class="overview-inline-hidden" id="overview-inline-hidden" hidden>
|
||||
<summary><span data-i18n="overviewLayoutInlineHidden">Hidden blocks</span> <span
|
||||
id="overview-inline-hidden-count"></span></summary>
|
||||
<div class="overview-inline-hidden-list" id="overview-inline-hidden-list"></div>
|
||||
</details>
|
||||
<div class="stats-grid dashboard-overview-grid" id="dashboard-overview-grid">
|
||||
<article class="stat" data-dashboard-widget="requests"><span
|
||||
data-i18n="requests">Requests</span><strong id="metric-requests">0</strong><small
|
||||
id="request-rate">0 req/s</small></article>
|
||||
@@ -234,9 +250,457 @@
|
||||
<small id="stream-dashboard-health-detail" data-i18n="streamWidgetUnavailable">No stream
|
||||
data</small>
|
||||
</article>
|
||||
</div>
|
||||
<div class="overview-grid">
|
||||
<article class="card activity-card">
|
||||
<article class="stat" data-dashboard-widget="backend_health" hidden>
|
||||
<span data-i18n="backendWidgetHealth">Backend health</span><strong
|
||||
id="backend-dashboard-health">-</strong>
|
||||
<small id="backend-dashboard-health-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_activity" hidden>
|
||||
<span data-i18n="backendWidgetActivity">Active backend requests</span><strong
|
||||
id="backend-dashboard-activity">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetActivity" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-active-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-activity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_errors" hidden>
|
||||
<span data-i18n="backendWidgetErrors">Backend errors</span><strong
|
||||
id="backend-dashboard-errors">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetErrors" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h3-line" id="backend-dashboard-error-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-errors-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_latency" hidden>
|
||||
<span data-i18n="backendWidgetLatency">Health probe RTT</span><strong
|
||||
id="backend-dashboard-latency">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetLatency" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-latency-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-latency-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_traffic" hidden>
|
||||
<span data-i18n="backendWidgetTraffic">Backend traffic</span><strong
|
||||
id="backend-dashboard-traffic">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetTraffic" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line stream-chart-rx" id="backend-dashboard-in-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
<polyline class="diagnostic-line stream-chart-tx" id="backend-dashboard-out-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small class="stat-chart-legend stream-traffic-legend">
|
||||
<span class="legend-rx"><i></i>IN <b id="backend-dashboard-in-rate">-</b></span>
|
||||
<span class="legend-tx"><i></i>OUT <b id="backend-dashboard-out-rate">-</b></span>
|
||||
</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_protocols" hidden>
|
||||
<span data-i18n="backendWidgetProtocols">Configured backend modes</span><strong
|
||||
id="backend-dashboard-protocols">-</strong>
|
||||
<div class="backend-mode-bar" role="img" data-i18n-aria="backendWidgetProtocols">
|
||||
<span class="backend-mode-h1" id="backend-mode-h1"></span>
|
||||
<span class="backend-mode-auto" id="backend-mode-auto"></span>
|
||||
<span class="backend-mode-compat" id="backend-mode-compat"></span>
|
||||
<span class="backend-mode-h2" id="backend-mode-h2"></span>
|
||||
</div><small id="backend-dashboard-protocols-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_requests" hidden>
|
||||
<span data-i18n="backendWidgetRequests">Backend request rate</span><strong
|
||||
id="backend-dashboard-requests">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetRequests" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h1-line" id="backend-dashboard-request-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-requests-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_capacity" hidden>
|
||||
<span data-i18n="backendWidgetCapacity">Backend capacity</span><strong
|
||||
id="backend-dashboard-capacity">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetCapacity" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-capacity-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-capacity-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_http5xx" hidden>
|
||||
<span data-i18n="backendWidgetHttp5xx">backendWidgetHttp5xx</span><strong
|
||||
id="backend-dashboard-http5xx">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetHttp5xx" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-5xx-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-http5xx-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_timeouts" hidden>
|
||||
<span data-i18n="backendWidgetTimeouts">backendWidgetTimeouts</span><strong
|
||||
id="backend-dashboard-timeouts">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetTimeouts" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-timeout-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-timeouts-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_network" hidden>
|
||||
<span data-i18n="backendWidgetNetwork">backendWidgetNetwork</span><strong
|
||||
id="backend-dashboard-network">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetNetwork" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-network-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-network-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_retries" hidden>
|
||||
<span data-i18n="backendWidgetRetries">backendWidgetRetries</span><strong
|
||||
id="backend-dashboard-retries">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetRetries" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-retry-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-retries-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="backend_queue" hidden>
|
||||
<span data-i18n="backendWidgetQueue">backendWidgetQueue</span><strong
|
||||
id="backend-dashboard-queue">-</strong>
|
||||
<svg data-i18n-aria="backendWidgetQueue" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line proto-h2-line" id="backend-dashboard-queue-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="backend-dashboard-queue-detail">-</small>
|
||||
</article>
|
||||
<article class="stat" data-dashboard-widget="backend_rotations" hidden>
|
||||
<span data-i18n="backendWidgetRotations">Backend client pool rotations</span><strong
|
||||
id="backend-dashboard-rotations">-</strong>
|
||||
<small id="backend-dashboard-rotations-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_cpu" hidden>
|
||||
<span data-i18n="overviewExtraProcessCpu">overviewExtraProcessCpu</span><strong
|
||||
id="overview-extra-extra_process_cpu">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraProcessCpu" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_process_cpu-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_process_cpu-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_rss" hidden>
|
||||
<span data-i18n="overviewExtraProcessRss">overviewExtraProcessRss</span><strong
|
||||
id="overview-extra-extra_process_rss">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraProcessRss" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_process_rss-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_process_rss-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_process_fds" hidden>
|
||||
<span data-i18n="overviewExtraProcessFds">overviewExtraProcessFds</span><strong
|
||||
id="overview-extra-extra_process_fds">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraProcessFds" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_process_fds-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_process_fds-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_open_connections" hidden>
|
||||
<span data-i18n="overviewExtraOpenConnections">overviewExtraOpenConnections</span><strong
|
||||
id="overview-extra-extra_open_connections">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraOpenConnections" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_open_connections-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_open_connections-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_websocket_active" hidden>
|
||||
<span data-i18n="overviewExtraWebsocketActive">overviewExtraWebsocketActive</span><strong
|
||||
id="overview-extra-extra_websocket_active">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraWebsocketActive" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_websocket_active-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_websocket_active-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_tls_error_rate" hidden>
|
||||
<span data-i18n="overviewExtraTlsErrorRate">overviewExtraTlsErrorRate</span><strong
|
||||
id="overview-extra-extra_tls_error_rate">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraTlsErrorRate" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_tls_error_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_tls_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_request_error_rate" hidden>
|
||||
<span data-i18n="overviewExtraRequestErrorRate">overviewExtraRequestErrorRate</span><strong
|
||||
id="overview-extra-extra_request_error_rate">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraRequestErrorRate" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_request_error_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_request_error_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_hit_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheHitRate">overviewExtraCacheHitRate</span><strong
|
||||
id="overview-extra-extra_cache_hit_rate">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraCacheHitRate" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_cache_hit_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_cache_hit_rate-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_memory" hidden>
|
||||
<span data-i18n="overviewExtraCacheMemory">overviewExtraCacheMemory</span><strong
|
||||
id="overview-extra-extra_cache_memory">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraCacheMemory" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_cache_memory-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_cache_memory-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_h2_share" hidden>
|
||||
<span data-i18n="overviewExtraH2Share">overviewExtraH2Share</span><strong
|
||||
id="overview-extra-extra_h2_share">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraH2Share" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_h2_share-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_h2_share-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_response_latency" hidden>
|
||||
<span data-i18n="overviewExtraResponseLatency">overviewExtraResponseLatency</span><strong
|
||||
id="overview-extra-extra_response_latency">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraResponseLatency" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_response_latency-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_response_latency-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_backend_unhealthy" hidden>
|
||||
<span data-i18n="overviewExtraBackendUnhealthy">overviewExtraBackendUnhealthy</span><strong
|
||||
id="overview-extra-extra_backend_unhealthy">-</strong>
|
||||
<svg data-i18n-aria="overviewExtraBackendUnhealthy" role="img" viewBox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_backend_unhealthy-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_backend_unhealthy-detail">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_total_rps" hidden>
|
||||
<span data-i18n="overviewExtraTotalRps">HTTP requests / s</span><strong
|
||||
id="overview-extra-extra_total_rps">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="HTTP requests / s">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_total_rps-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_total_rps-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_active_exchanges" hidden>
|
||||
<span data-i18n="overviewExtraActiveExchanges">Active HTTP exchanges</span><strong
|
||||
id="overview-extra-extra_active_exchanges">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="Active HTTP exchanges">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_active_exchanges-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_active_exchanges-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_h1_share" hidden>
|
||||
<span data-i18n="overviewExtraH1Share">HTTP/1.1 share</span><strong
|
||||
id="overview-extra-extra_h1_share">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="HTTP/1.1 share">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_h1_share-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_h1_share-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_h3_share" hidden>
|
||||
<span data-i18n="overviewExtraH3Share">HTTP/3 share</span><strong
|
||||
id="overview-extra-extra_h3_share">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="HTTP/3 share">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_h3_share-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_h3_share-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_cache_miss_rate" hidden>
|
||||
<span data-i18n="overviewExtraCacheMissRate">Cache misses / s</span><strong
|
||||
id="overview-extra-extra_cache_miss_rate">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="Cache misses / s">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_cache_miss_rate-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_cache_miss_rate-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="stat stat-chart" data-dashboard-widget="extra_backend_active" hidden>
|
||||
<span data-i18n="overviewExtraBackendActive">In-flight backend requests</span><strong
|
||||
id="overview-extra-extra_backend_active">-</strong>
|
||||
<svg viewBox="0 0 300 60" role="img" aria-label="In-flight backend requests">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="overview-extra-extra_backend_active-line"
|
||||
points="0,55 300,55"></polyline>
|
||||
</svg><small id="overview-extra-extra_backend_active-detail"
|
||||
data-i18n="overviewExtraRecentSamples">-</small>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_protocol_rates" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideProtocolRates">HTTP/1.1 · HTTP/2 · HTTP/3 traffic</h2>
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_protocol_rates-summary">-</span>
|
||||
</div>
|
||||
<svg 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="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"
|
||||
id="overview-wide-wide_protocol_rates-h2-line" d=""></path>
|
||||
<path class="overview-wide-series overview-wide-series-2"
|
||||
id="overview-wide-wide_protocol_rates-h3-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span>H1</span><b
|
||||
id="overview-wide-wide_protocol_rates-h1-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span>H2</span><b
|
||||
id="overview-wide-wide_protocol_rates-h2-value">-</b></span>
|
||||
<span><i class="overview-wide-key-2"></i><span>H3</span><b
|
||||
id="overview-wide-wide_protocol_rates-h3-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_cpu" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCpu">System and process CPU</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_cpu-summary">-</span>
|
||||
</div>
|
||||
<svg 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="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"
|
||||
id="overview-wide-wide_cpu-process-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="overviewWideSystem">System</span><b
|
||||
id="overview-wide-wide_cpu-system-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="overviewWideProcess">Process</span><b
|
||||
id="overview-wide-wide_cpu-process-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_memory" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideMemory">Process RAM and cache memory</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_memory-summary">-</span>
|
||||
</div>
|
||||
<svg 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="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"
|
||||
id="overview-wide-wide_memory-cache-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="overviewWideProcess">Process</span><b
|
||||
id="overview-wide-wide_memory-process-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="overviewWideCache">Cache</span><b
|
||||
id="overview-wide-wide_memory-cache-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_cache_activity" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideCacheActivity">Cache hits / misses per second</h2>
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_cache_activity-summary">-</span>
|
||||
</div>
|
||||
<svg 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="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"
|
||||
id="overview-wide-wide_cache_activity-misses-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span data-i18n="overviewWideHits">HIT</span><b
|
||||
id="overview-wide-wide_cache_activity-hits-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="overviewWideMisses">MISS</span><b
|
||||
id="overview-wide-wide_cache_activity-misses-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_connections" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideConnections">Connections and WebSocket tunnels</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_connections-summary">-</span>
|
||||
</div>
|
||||
<svg 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="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"
|
||||
id="overview-wide-wide_connections-websockets-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="overviewWideOpen">Connections</span><b
|
||||
id="overview-wide-wide_connections-open-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="overviewWideWebsockets">WebSockets</span><b
|
||||
id="overview-wide-wide_connections-websockets-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_backend_health" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideBackendHealth">Backend availability over time</h2>
|
||||
<span class="overview-wide-summary"
|
||||
id="overview-wide-wide_backend_health-summary">-</span>
|
||||
</div>
|
||||
<svg 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="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"
|
||||
id="overview-wide-wide_backend_health-down-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="overviewWideHealthy">Healthy</span><b
|
||||
id="overview-wide-wide_backend_health-up-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="overviewWideUnhealthy">Unhealthy</span><b
|
||||
id="overview-wide-wide_backend_health-down-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card overview-wide-chart" data-dashboard-widget="wide_latency" hidden>
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="overviewWideLatency">Response and backend probe latency</h2>
|
||||
<span class="overview-wide-summary" id="overview-wide-wide_latency-summary">-</span>
|
||||
</div>
|
||||
<svg 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="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"
|
||||
id="overview-wide-wide_latency-probe-line" d=""></path>
|
||||
</svg>
|
||||
<div class="overview-wide-legend">
|
||||
<span><i class="overview-wide-key-0"></i><span
|
||||
data-i18n="overviewWideResponse">HTTP</span><b
|
||||
id="overview-wide-wide_latency-response-value">-</b></span>
|
||||
<span><i class="overview-wide-key-1"></i><span
|
||||
data-i18n="overviewWideProbe">Probes</span><b
|
||||
id="overview-wide-wide_latency-probe-value">-</b></span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card activity-card" data-dashboard-widget="traffic_chart">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="liveTraffic">LIVE TRAFFIC</span>
|
||||
<h2 data-i18n="requestsPerSecond">Requests per second</h2>
|
||||
@@ -269,7 +733,7 @@
|
||||
<div class="chart-footer"><span data-i18n="lastMinute">Recent samples / resets on
|
||||
reload</span><span id="chart-peak">peak 0 req/s</span></div>
|
||||
</article>
|
||||
<article class="card system-card">
|
||||
<article class="card system-card" data-dashboard-widget="system_chart">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="systemLoad">SYSTEM LOAD</span>
|
||||
<h2><span id="system-cpu">-</span> <small>CPU</small></h2>
|
||||
@@ -289,7 +753,7 @@
|
||||
<div class="chart-footer"><span data-i18n="systemUptime">System uptime</span><span
|
||||
id="system-uptime">-</span></div>
|
||||
</article>
|
||||
<article class="card connection-card">
|
||||
<article class="card connection-card" data-dashboard-widget="connection_chart">
|
||||
<div class="card-heading">
|
||||
<div><span class="small-label" data-i18n="connectionActivity">CONNECTION ACTIVITY</span>
|
||||
<h2><span id="connection-current">0</span> <small data-i18n="connections">Open
|
||||
@@ -312,9 +776,7 @@
|
||||
<div class="chart-footer"><span><span data-i18n="backends">Backends</span>: <b
|
||||
id="connection-backends">0</b></span></div>
|
||||
</article>
|
||||
</div>
|
||||
<div class="diagnostics-grid">
|
||||
<article class="card">
|
||||
<article class="card" data-dashboard-widget="cache_diagnostics">
|
||||
<div class="mini-heading">
|
||||
<h3 data-i18n="cacheOverview">RAM cache</h3><span class="pill" id="cache-state">-</span>
|
||||
</div>
|
||||
@@ -409,7 +871,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<article class="card" data-dashboard-widget="process_diagnostics">
|
||||
<div class="mini-heading">
|
||||
<h3 data-i18n="processDiagnostics">Process</h3>
|
||||
<div class="mini-heading-actions">
|
||||
@@ -521,7 +983,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<article class="card">
|
||||
<article class="card" data-dashboard-widget="network_diagnostics">
|
||||
<div class="mini-heading">
|
||||
<h3 data-i18n="networkDiagnostics">Network and responses</h3><span class="meta"
|
||||
data-i18n="hostScope">OS scope</span>
|
||||
@@ -581,106 +1043,117 @@
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<article class="card pool-card" id="backend-diagnostics-card">
|
||||
<div class="card-heading backend-card-heading">
|
||||
<h2 data-i18n="vhHealthTitle">Backends</h2>
|
||||
<div class="backend-table-tools">
|
||||
<label class="sr-only" data-i18n="backendSearch" for="pool-search">Search
|
||||
backends</label>
|
||||
<input autocomplete="off" data-i18n-aria="backendSearch"
|
||||
data-i18n-placeholder="backendSearchPlaceholder" id="pool-search" type="search" />
|
||||
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
|
||||
by</span><select data-i18n-aria="backendSortBy" id="pool-sort">
|
||||
<option data-i18n="vhVhost" value="vhost">Vhost</option>
|
||||
<option data-i18n="backend" value="backend">Backend</option>
|
||||
<option data-i18n="status" value="status">Status</option>
|
||||
<option data-i18n="vhActive" value="active">Active</option>
|
||||
<option data-i18n="vhRequests" value="requests">Requests</option>
|
||||
<option data-i18n="vhErrors" value="errors">Errors</option>
|
||||
<option data-i18n="vhLatency" value="latency">Latency</option>
|
||||
<option data-i18n="vhTraffic" value="traffic">Traffic/s</option>
|
||||
<option data-i18n="vhClientGeneration" value="generation">Client gen.</option>
|
||||
<option data-i18n="vhGenerationRequests" value="generationRequests">Gen.
|
||||
requests</option>
|
||||
<option data-i18n="vhGenerationAge" value="generationAge">Gen. age</option>
|
||||
<option data-i18n="vhClientRotations" value="rotations">Rotations</option>
|
||||
<option data-i18n="vhLastFailure" value="lastFailure">Last failure</option>
|
||||
<option data-i18n="vhLastRecovery" value="lastRecovery">Last recovery</option>
|
||||
</select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="pool-sort-direction" type="button">↑</button>
|
||||
<details class="backend-column-picker">
|
||||
<summary class="quiet" data-i18n="backendColumns">Columns</summary>
|
||||
<div class="backend-column-options">
|
||||
<label class="check"><input checked data-pool-column-toggle="vhost"
|
||||
type="checkbox" /><span data-i18n="vhVhost">Vhost</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="backend"
|
||||
type="checkbox" /><span data-i18n="backend">Backend</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="status"
|
||||
type="checkbox" /><span data-i18n="status">Status</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="active"
|
||||
type="checkbox" /><span data-i18n="vhActive">Active</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="requests"
|
||||
type="checkbox" /><span data-i18n="vhRequests">Requests</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="errors"
|
||||
type="checkbox" /><span data-i18n="vhErrors">Errors</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="latency"
|
||||
type="checkbox" /><span data-i18n="vhLatency">Latency</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="traffic"
|
||||
type="checkbox" /><span data-i18n="vhTraffic">Traffic/s</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="generation"
|
||||
type="checkbox" /><span data-i18n="vhClientGeneration">Client
|
||||
gen.</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="generationRequests"
|
||||
type="checkbox" /><span data-i18n="vhGenerationRequests">Gen.
|
||||
requests</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="generationAge"
|
||||
type="checkbox" /><span data-i18n="vhGenerationAge">Gen.
|
||||
age</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="rotations"
|
||||
type="checkbox" /><span
|
||||
data-i18n="vhClientRotations">Rotations</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="lastFailure"
|
||||
type="checkbox" /><span data-i18n="vhLastFailure">Last
|
||||
failure</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="lastRecovery"
|
||||
type="checkbox" /><span data-i18n="vhLastRecovery">Last
|
||||
recovery</span></label>
|
||||
</div>
|
||||
</details>
|
||||
<button class="quiet" data-i18n="vhManage" id="pool-open" type="button">Manage
|
||||
vhosts</button>
|
||||
<article class="card pool-card" id="backend-diagnostics-card"
|
||||
data-dashboard-widget="backend_table">
|
||||
<div class="card-heading backend-card-heading">
|
||||
<h2 data-i18n="vhHealthTitle">Backends</h2>
|
||||
<div class="backend-table-tools">
|
||||
<label class="sr-only" data-i18n="backendSearch" for="pool-search">Search
|
||||
backends</label>
|
||||
<input autocomplete="off" data-i18n-aria="backendSearch"
|
||||
data-i18n-placeholder="backendSearchPlaceholder" id="pool-search"
|
||||
type="search" />
|
||||
<label class="backend-sort-control"><span data-i18n="backendSortBy">Sort
|
||||
by</span><select data-i18n-aria="backendSortBy" id="pool-sort">
|
||||
<option data-i18n="vhVhost" value="vhost">Vhost</option>
|
||||
<option data-i18n="backend" value="backend">Backend</option>
|
||||
<option data-i18n="status" value="status">Status</option>
|
||||
<option data-i18n="vhActive" value="active">Active</option>
|
||||
<option data-i18n="vhRequests" value="requests">Requests</option>
|
||||
<option data-i18n="vhErrors" value="errors">Errors</option>
|
||||
<option data-i18n="vhLatency" value="latency">Latency</option>
|
||||
<option data-i18n="vhTraffic" value="traffic">Traffic/s</option>
|
||||
<option data-i18n="vhClientGeneration" value="generation">Client gen.
|
||||
</option>
|
||||
<option data-i18n="vhGenerationRequests" value="generationRequests">Gen.
|
||||
requests</option>
|
||||
<option data-i18n="vhGenerationAge" value="generationAge">Gen. age</option>
|
||||
<option data-i18n="vhClientRotations" value="rotations">Rotations</option>
|
||||
<option data-i18n="vhLastFailure" value="lastFailure">Last failure</option>
|
||||
<option data-i18n="vhLastRecovery" value="lastRecovery">Last recovery
|
||||
</option>
|
||||
</select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="pool-sort-direction" type="button">↑</button>
|
||||
<details class="backend-column-picker">
|
||||
<summary class="quiet" data-i18n="backendColumns">Columns</summary>
|
||||
<div class="backend-column-options">
|
||||
<label class="check"><input checked data-pool-column-toggle="vhost"
|
||||
type="checkbox" /><span data-i18n="vhVhost">Vhost</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="backend"
|
||||
type="checkbox" /><span data-i18n="backend">Backend</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="status"
|
||||
type="checkbox" /><span data-i18n="status">Status</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="active"
|
||||
type="checkbox" /><span data-i18n="vhActive">Active</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="requests"
|
||||
type="checkbox" /><span
|
||||
data-i18n="vhRequests">Requests</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="errors"
|
||||
type="checkbox" /><span data-i18n="vhErrors">Errors</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="latency"
|
||||
type="checkbox" /><span data-i18n="vhLatency">Latency</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="traffic"
|
||||
type="checkbox" /><span
|
||||
data-i18n="vhTraffic">Traffic/s</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="generation"
|
||||
type="checkbox" /><span data-i18n="vhClientGeneration">Client
|
||||
gen.</span></label>
|
||||
<label class="check"><input checked
|
||||
data-pool-column-toggle="generationRequests" type="checkbox" /><span
|
||||
data-i18n="vhGenerationRequests">Gen.
|
||||
requests</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="generationAge"
|
||||
type="checkbox" /><span data-i18n="vhGenerationAge">Gen.
|
||||
age</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="rotations"
|
||||
type="checkbox" /><span
|
||||
data-i18n="vhClientRotations">Rotations</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="lastFailure"
|
||||
type="checkbox" /><span data-i18n="vhLastFailure">Last
|
||||
failure</span></label>
|
||||
<label class="check"><input checked data-pool-column-toggle="lastRecovery"
|
||||
type="checkbox" /><span data-i18n="vhLastRecovery">Last
|
||||
recovery</span></label>
|
||||
</div>
|
||||
</details>
|
||||
<button class="quiet" data-i18n="vhManage" id="pool-open" type="button">Manage
|
||||
vhosts</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table class="pool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="vhVhost" data-pool-column="vhost">Vhost</th>
|
||||
<th data-i18n="backend" data-pool-column="backend">Backend</th>
|
||||
<th data-i18n="status" data-pool-column="status">Status</th>
|
||||
<th data-i18n="vhActive" data-pool-column="active">Active</th>
|
||||
<th data-i18n="vhRequests" data-pool-column="requests">Requests</th>
|
||||
<th data-i18n="vhErrors" data-pool-column="errors">Errors</th>
|
||||
<th data-i18n="vhLatency" data-pool-column="latency">Latency</th>
|
||||
<th data-i18n="vhTraffic" data-pool-column="traffic">Traffic/s</th>
|
||||
<th data-i18n="vhClientGeneration" data-pool-column="generation">Client gen.
|
||||
</th>
|
||||
<th data-i18n="vhGenerationRequests" data-pool-column="generationRequests">Gen.
|
||||
requests</th>
|
||||
<th data-i18n="vhGenerationAge" data-pool-column="generationAge">Gen. age</th>
|
||||
<th data-i18n="vhClientRotations" data-pool-column="rotations">Rotations</th>
|
||||
<th data-i18n="vhLastFailure" data-pool-column="lastFailure">Last failure</th>
|
||||
<th data-i18n="vhLastRecovery" data-pool-column="lastRecovery">Last recovery
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pool-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="fine" data-i18n="vhHealthEmpty" id="pool-empty">No backend diagnostics yet.</p>
|
||||
</article>
|
||||
<div class="table-scroll">
|
||||
<table class="pool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="vhVhost" data-pool-column="vhost">Vhost</th>
|
||||
<th data-i18n="backend" data-pool-column="backend">Backend</th>
|
||||
<th data-i18n="status" data-pool-column="status">Status</th>
|
||||
<th data-i18n="vhActive" data-pool-column="active">Active</th>
|
||||
<th data-i18n="vhRequests" data-pool-column="requests">Requests</th>
|
||||
<th data-i18n="vhErrors" data-pool-column="errors">Errors</th>
|
||||
<th data-i18n="vhLatency" data-pool-column="latency">Latency</th>
|
||||
<th data-i18n="vhTraffic" data-pool-column="traffic">Traffic/s</th>
|
||||
<th data-i18n="vhClientGeneration" data-pool-column="generation">Client gen.
|
||||
</th>
|
||||
<th data-i18n="vhGenerationRequests" data-pool-column="generationRequests">
|
||||
Gen.
|
||||
requests</th>
|
||||
<th data-i18n="vhGenerationAge" data-pool-column="generationAge">Gen. age
|
||||
</th>
|
||||
<th data-i18n="vhClientRotations" data-pool-column="rotations">Rotations
|
||||
</th>
|
||||
<th data-i18n="vhLastFailure" data-pool-column="lastFailure">Last failure
|
||||
</th>
|
||||
<th data-i18n="vhLastRecovery" data-pool-column="lastRecovery">Last recovery
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pool-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="fine" data-i18n="vhHealthEmpty" id="pool-empty">No backend diagnostics yet.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel" hidden="" id="panel-vhosts">
|
||||
<p class="notice fine" data-i18n="vhIntro">Each vhost owns its bound IPs, paths and backends.
|
||||
@@ -1899,7 +2372,10 @@
|
||||
<div class="appearance-settings-grid">
|
||||
<label><span data-i18n="theme">Theme</span><select id="ui-theme">
|
||||
<option data-i18n="themeDark" value="dark">Dark</option>
|
||||
<option data-i18n="themeUltraDark" value="ultra_dark">Ultra dark ·
|
||||
near-black</option>
|
||||
<option data-i18n="themeLight" value="light">Light</option>
|
||||
<option data-i18n="themeCream" value="cream">Warm cream</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="language">Language</span><select aria-label="Language"
|
||||
data-i18n-aria="language" id="language"></select></label>
|
||||
@@ -1922,6 +2398,18 @@
|
||||
</select><small class="fine" data-i18n="buttonStyleHelp">Normal uses the
|
||||
refreshed controls; Compact restores the smaller source-style buttons
|
||||
across all actions and dialog tabs.</small></label>
|
||||
<label><span data-i18n="cornerStyle">Corner rounding</span><select
|
||||
id="ui-corner-style">
|
||||
<option data-i18n="cornerStyleDefault" value="default">Original
|
||||
(theme-independent)</option>
|
||||
<option data-i18n="cornerStyleSquare" value="square">Square</option>
|
||||
<option data-i18n="cornerStyleSubtle" value="subtle">Subtle</option>
|
||||
<option data-i18n="cornerStyleRounded" value="rounded">Rounded</option>
|
||||
<option data-i18n="cornerStyleExtra" value="extra">Extra rounded
|
||||
</option>
|
||||
</select><small class="fine" data-i18n="cornerStyleHelp">Controls corners
|
||||
throughout the interface independently of the theme. Original keeps the
|
||||
previous geometry.</small></label>
|
||||
</div>
|
||||
</section>
|
||||
<section class="appearance-section">
|
||||
@@ -2002,6 +2490,10 @@
|
||||
data-i18n="dashboardWidgets">Overview cards</span><small class="fine"
|
||||
data-i18n="dashboardWidgetsHelp">Show, hide and reorder the telemetry
|
||||
cards at the top of Overview.</small>
|
||||
<button class="outline dashboard-layout-open-settings" type="button"
|
||||
id="overview-layout-open-settings"
|
||||
data-i18n="overviewLayoutEditorOpenSettings">Visual layout
|
||||
editor</button>
|
||||
<div class="dashboard-widget-list" id="dashboard-widget-list"></div>
|
||||
<small class="fine" data-i18n="streamWidgetSettingsHelp">Stream cards are
|
||||
opt-in and only change Dashboard presentation, not Stream routing or
|
||||
@@ -3328,6 +3820,67 @@
|
||||
type="button">Cancel</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="overview-layout-dialog-title" aria-modal="true" class="audit-dialog overview-layout-dialog"
|
||||
id="overview-layout-dialog">
|
||||
<div class="window-heading">
|
||||
<div><span class="small-label" data-i18n="dashboardWidgets">Overview cards</span>
|
||||
<h2 id="overview-layout-dialog-title" data-i18n="overviewLayoutEditorTitle">Arrange Overview cards</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-layout-body">
|
||||
<p class="fine" data-i18n="overviewLayoutEditorHelp">Drag visible cards to change their order. Changes are
|
||||
saved only when you select Save.</p>
|
||||
<section aria-labelledby="overview-layout-visible-title">
|
||||
<h3 id="overview-layout-visible-title" data-i18n="overviewLayoutEditorVisible">Visible cards</h3>
|
||||
<div class="overview-layout-grid" id="overview-layout-visible" role="list"></div>
|
||||
</section>
|
||||
<section aria-labelledby="overview-layout-hidden-title">
|
||||
<div class="overview-layout-section-heading">
|
||||
<h3 id="overview-layout-hidden-title" data-i18n="overviewLayoutEditorHidden">Hidden cards</h3>
|
||||
<div class="overview-layout-filters">
|
||||
<select id="overview-layout-type-filter" data-i18n-aria="overviewLayoutTypeFilter"
|
||||
aria-label="Filter blocks by type">
|
||||
<option value="all" data-i18n="overviewBlockTypeAll">All block types</option>
|
||||
<option value="value" data-i18n="overviewBlockTypeValue">Number</option>
|
||||
<option value="value_chart" data-i18n="overviewBlockTypeValueChart">Number + chart</option>
|
||||
<option value="chart" data-i18n="overviewBlockTypeChart">Chart</option>
|
||||
<option value="panel" data-i18n="overviewBlockTypePanel">Diagnostics panel</option>
|
||||
<option value="table" data-i18n="overviewBlockTypeTable">Table</option>
|
||||
</select>
|
||||
<input id="overview-layout-search" type="search"
|
||||
data-i18n-placeholder="overviewLayoutEditorSearch" aria-label="Find hidden card"
|
||||
data-i18n-aria="overviewLayoutEditorSearch" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-layout-hidden-list" id="overview-layout-hidden" role="list"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="overview-layout-footer">
|
||||
<p id="overview-layout-message" role="alert" aria-live="polite"></p>
|
||||
<button class="quiet" id="overview-layout-default" type="button"
|
||||
data-i18n="overviewLayoutDefaultPreview">Default view</button>
|
||||
<button class="outline" id="overview-layout-reset" type="button" data-i18n="overviewLayoutReset">Reset
|
||||
layout</button>
|
||||
<button class="quiet" id="overview-layout-cancel" type="button"
|
||||
data-i18n="overviewLayoutEditorCancel">Cancel</button>
|
||||
<button class="primary" id="overview-layout-save" type="button" data-i18n="overviewLayoutEditorSave">Save
|
||||
layout</button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="overview-reset-confirm-title" aria-modal="true" class="audit-dialog overview-reset-confirm"
|
||||
id="overview-reset-confirm">
|
||||
<div class="window-heading">
|
||||
<h2 id="overview-reset-confirm-title" data-i18n="overviewLayoutResetTitle">Reset dashboard layout?</h2>
|
||||
</div>
|
||||
<p data-i18n="overviewLayoutResetConfirm">This restores the default order and visibility of all Overview blocks.
|
||||
Other settings remain unchanged.</p>
|
||||
<div class="overview-layout-footer">
|
||||
<button class="quiet" id="overview-reset-cancel" type="button"
|
||||
data-i18n="overviewLayoutEditorCancel">Cancel</button>
|
||||
<button class="primary" id="overview-reset-apply" type="button" data-i18n="overviewLayoutResetApply">Reset
|
||||
and save</button>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog aria-labelledby="tunnels-dialog-title" class="audit-dialog" id="tunnels-dialog">
|
||||
<div class="window-heading">
|
||||
<div><span class="small-label">WEBSOCKET / CONNECT</span>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
const node = id => document.getElementById(id);
|
||||
const themes = new Set(["dark", "light"]);
|
||||
const themes = new Set(["dark", "ultra_dark", "light", "cream"]);
|
||||
const corners = new Set(["default", "square", "subtle", "rounded", "extra"]);
|
||||
try { const cached = localStorage.getItem("rp-theme"); document.documentElement.dataset.theme = themes.has(cached) ? cached : "dark"; } catch { document.documentElement.dataset.theme = "dark"; }
|
||||
try { const style = localStorage.getItem("rp-corner-style"); document.documentElement.dataset.cornerStyle = corners.has(style) ? style : "default"; } catch { document.documentElement.dataset.cornerStyle = "default"; }
|
||||
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/traffic", "/cache", "/transform", "/logging", "/logs", "/settings"]);
|
||||
const settingsTabs = new Set(["appearance", "listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "geoip", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
|
||||
let packs = new Map(), fallback = null, language = null, setupRequired = false, ready = false;
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
(() => {
|
||||
const buttons = [...document.querySelectorAll("[data-settings-tab]")];
|
||||
const search = $("settings-search"), searchStatus = $("settings-search-status");
|
||||
const defaultTab = (typeof location !== "undefined" && location.pathname === "/configuration") ? "manual-config" : "listeners";
|
||||
const legacyTab = (typeof location !== "undefined" && location.pathname === "/configuration") ? "manual-config" : "appearance";
|
||||
// Explicit URL tabs win. Otherwise reopen the last chosen Settings tab,
|
||||
// falling back to Appearance for new browsers and invalid stored values.
|
||||
let rememberedTab = null;
|
||||
try { rememberedTab = localStorage.getItem("rp-settings-last-tab"); } catch { }
|
||||
const defaultTab = legacyTab === "manual-config" ? legacyTab : (rememberedTab || "appearance");
|
||||
const requestedTab = typeof location !== "undefined" && location.pathname === "/logging"
|
||||
? "logging"
|
||||
: (typeof location !== "undefined" && location.pathname === "/settings" ? new URLSearchParams(location.search).get("tab") : null);
|
||||
@@ -19,7 +24,8 @@
|
||||
host && (!pinned || host.replace(/\.$/, "").toLowerCase() !== pinned));
|
||||
}
|
||||
const normalizedRequestedTab = tabAliases[requestedTab] || requestedTab;
|
||||
let active = buttons.some(button => button.dataset.settingsTab === normalizedRequestedTab) ? normalizedRequestedTab : defaultTab;
|
||||
const validTab = tab => buttons.some(button => button.dataset.settingsTab === tab);
|
||||
let active = validTab(normalizedRequestedTab) ? normalizedRequestedTab : (validTab(defaultTab) ? defaultTab : "appearance");
|
||||
function applySettingsSearch() {
|
||||
const query = String(search?.value || "").trim().toLocaleLowerCase();
|
||||
const matches = [];
|
||||
@@ -41,6 +47,7 @@
|
||||
const target = buttons.find(button => button.dataset.settingsTab === name); if (!target) return;
|
||||
if (target.hidden && search?.value) { search.value = ""; applySettingsSearch(); }
|
||||
active = name;
|
||||
if (persist) { rememberedTab = name; try { localStorage.setItem("rp-settings-last-tab", name); } catch { } }
|
||||
if (typeof window !== "undefined") window.rustProxyRefresh?.setSettingsTab(name);
|
||||
for (const b of buttons) { const chosen = b.dataset.settingsTab === name; b.setAttribute("aria-selected", String(chosen)); b.tabIndex = chosen ? 0 : -1; if (chosen && focus) b.focus(); }
|
||||
document.querySelectorAll("[data-settings-page]").forEach(p => p.hidden = p.dataset.settingsPage !== name);
|
||||
@@ -277,6 +284,6 @@
|
||||
const text = await response.text(); if (epoch !== authEpoch || !authenticated) return; $("prom-preview-output").textContent = text.slice(0, 262144); $("prom-preview-output").hidden = false;
|
||||
} catch (error) { if (epoch === authEpoch) toast(error.message, true); } finally { button.disabled = false; }
|
||||
});
|
||||
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, refreshSearch: applySettingsSearch, current: () => active, reset() { if (search) search.value = ""; applySettingsSearch(); select("listeners", false, false); selectPolicyTab("user-agent"); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); window.rustProxyCertificateApi?.reset(); $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
|
||||
window.rustProxySettings = { select, selectHttpTab, syncLocation, render, refreshSearch: applySettingsSearch, current: () => active, reset() { if (search) search.value = ""; applySettingsSearch(); select(validTab(rememberedTab) ? rememberedTab : "appearance", false, false); selectPolicyTab("user-agent"); selectCacheTab("general"); selectLoggingTab("outputs"); selectHttpTab("http2"); window.rustProxyCertificateApi?.reset(); $("prom-preview-output").textContent = ""; $("prom-preview-output").hidden = true; } };
|
||||
select(active); render();
|
||||
})();
|
||||
+521
-5
@@ -132,6 +132,135 @@ html[data-theme="light"] {
|
||||
--scrollbar-thumb-hover: #879684
|
||||
}
|
||||
|
||||
|
||||
/* Optional AMOLED palette: near-black surfaces, readable borders and restrained
|
||||
mint/coral emphasis. Does not change the established dark appearance. */
|
||||
html[data-theme="ultra_dark"] {
|
||||
color-scheme: dark;
|
||||
--bg: #030404;
|
||||
--panel: #0b0d0e;
|
||||
--panel2: #121516;
|
||||
--line: #292f30;
|
||||
--muted: #a4afb0;
|
||||
--text: #f1f4f2;
|
||||
--accent: #93dfc8;
|
||||
--logo-accent: #f69b74;
|
||||
--logo-secondary: #cfe0ac;
|
||||
--green: #b1dcb0;
|
||||
--red: #ff9899;
|
||||
--sidebar-bg: #000000;
|
||||
--brand-secondary: #a9cfb5;
|
||||
--nav-active-bg: #172422;
|
||||
--nav-hover-bg: #121819;
|
||||
--text-subtle: #879695;
|
||||
--separator: #526261;
|
||||
--border-hover: #7c9f96;
|
||||
--field-bg: #090b0c;
|
||||
--field-line: #445352;
|
||||
--placeholder: #879593;
|
||||
--row-line: #202627;
|
||||
--outline-line: #79968f;
|
||||
--chart-grid: #303839;
|
||||
--code-bg: #0e1313;
|
||||
--code-alt-bg: #101a1d;
|
||||
--code-alt-text: #d5eae9;
|
||||
--nav-selected-bg: #213730;
|
||||
--accent-ink: #071814;
|
||||
--accent-hover: #b6f2dd;
|
||||
--selected-ink: #071814;
|
||||
--notice-text: #e6c99c;
|
||||
--notice-line: #66533a;
|
||||
--notice-bg: #201a11;
|
||||
--pill-line: #625544;
|
||||
--pill-live-line: #506c59;
|
||||
--toast-bg: #bcf3da;
|
||||
--toast-text: #10221a;
|
||||
--toast-line: #d8ffe8;
|
||||
--toast-error-bg: #ffd5d0;
|
||||
--toast-error-line: #ff9c96;
|
||||
--toast-error-text: #472625;
|
||||
--danger-line: #754245;
|
||||
--danger-line-strong: #965257;
|
||||
--warn-line: #89754c;
|
||||
--blue: #8bbdfb;
|
||||
--transform-chart: #cfaeff;
|
||||
--backdrop: rgb(0 0 0 / .86);
|
||||
--backdrop-soft: rgb(0 0 0 / .77);
|
||||
--shadow-mid: rgb(0 0 0 / .64);
|
||||
--shadow-strong: rgb(0 0 0 / .84);
|
||||
--neutral-soft: rgb(132 171 158 / .045);
|
||||
--scrollbar-track: #060808;
|
||||
--scrollbar-thumb: #364243;
|
||||
--scrollbar-thumb-hover: #617373;
|
||||
}
|
||||
|
||||
/* Warm, muted ivory. No pure-white canvas; numeric values and charts retain
|
||||
clear contrast against their slightly lighter, neutral card surfaces. */
|
||||
html[data-theme="cream"] {
|
||||
color-scheme: light;
|
||||
--bg: #f2ede3;
|
||||
--panel: #fcf9f2;
|
||||
--panel2: #eae3d6;
|
||||
--line: #d5cabb;
|
||||
--muted: #6b6257;
|
||||
--text: #302c27;
|
||||
--accent: #8c5942;
|
||||
--green: #5e7655;
|
||||
--red: #a54645;
|
||||
--sidebar-bg: #eae2d5;
|
||||
--brand-secondary: #668367;
|
||||
--nav-active-bg: #e0d3c0;
|
||||
--nav-hover-bg: #e6dccc;
|
||||
--text-subtle: #786e61;
|
||||
--separator: #afa494;
|
||||
--border-hover: #968776;
|
||||
--field-bg: #fffcf7;
|
||||
--field-line: #baad9a;
|
||||
--placeholder: #807466;
|
||||
--row-line: #e2d9cb;
|
||||
--outline-line: #9c8d7c;
|
||||
--chart-grid: #ddd3c4;
|
||||
--code-bg: #f0e8da;
|
||||
--code-alt-bg: #e7ebe9;
|
||||
--code-alt-text: #364f4e;
|
||||
--nav-selected-bg: #d9cbb7;
|
||||
--accent-ink: #ffffff;
|
||||
--accent-hover: #774632;
|
||||
--selected-ink: #ffffff;
|
||||
--notice-text: #765e40;
|
||||
--notice-line: #d9c3a1;
|
||||
--notice-bg: #f8efdf;
|
||||
--pill-line: #c2a78d;
|
||||
--pill-live-line: #b0bd9d;
|
||||
--toast-bg: #d6ead2;
|
||||
--toast-text: #263d29;
|
||||
--toast-line: #b8d5b6;
|
||||
--toast-error-bg: #f8ded7;
|
||||
--toast-error-line: #dca99c;
|
||||
--toast-error-text: #60352f;
|
||||
--danger-line: #d2a29a;
|
||||
--danger-line-strong: #b97870;
|
||||
--warn-line: #b49c6b;
|
||||
--blue: #4d7292;
|
||||
--transform-chart: #83659a;
|
||||
--backdrop: rgb(37 27 16 / .44);
|
||||
--backdrop-soft: rgb(47 35 21 / .48);
|
||||
--shadow-mid: rgb(64 49 34 / .13);
|
||||
--shadow-strong: rgb(64 49 34 / .24);
|
||||
--neutral-soft: rgb(104 91 69 / .045);
|
||||
--scrollbar-track: #ece4d7;
|
||||
--scrollbar-thumb: #b5a997;
|
||||
--scrollbar-thumb-hover: #978a76;
|
||||
}
|
||||
|
||||
/* Theme palettes change color only; corners are controlled independently. */
|
||||
html[data-theme="ultra_dark"] :is(.card, .stat, dialog) {
|
||||
box-shadow: 0 6px 24px rgb(0 0 0 / .18);
|
||||
}
|
||||
html[data-theme="cream"] :is(.card, .stat, dialog) {
|
||||
box-shadow: 0 6px 20px rgb(78 58 38 / .055);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
@@ -198,7 +327,7 @@ html:not(.app-loading) .app-startup {
|
||||
}
|
||||
|
||||
.app-startup-brand span {
|
||||
color: var(--accent, #65cfa2)
|
||||
color: var(--logo-secondary, var(--accent, #65cfa2))
|
||||
}
|
||||
|
||||
.app-startup-spinner {
|
||||
@@ -361,8 +490,8 @@ h2 {
|
||||
|
||||
.brand-mark {
|
||||
font: 700 33px var(--mono);
|
||||
border: 1px solid var(--accent);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--logo-accent, var(--accent));
|
||||
color: var(--logo-accent, var(--accent));
|
||||
place-content: center;
|
||||
position: relative
|
||||
}
|
||||
@@ -371,7 +500,7 @@ h2 {
|
||||
font-size: 22px;
|
||||
left: 29px;
|
||||
top: 19px;
|
||||
color: var(--brand-secondary)
|
||||
color: var(--logo-secondary, var(--brand-secondary))
|
||||
}
|
||||
|
||||
.edition {
|
||||
@@ -10175,4 +10304,391 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
#panel-overview .stats-grid {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Optional HTTP backend mode distribution (configured modes, not negotiated ALPN). */
|
||||
#panel-overview .backend-mode-bar {
|
||||
display: flex;
|
||||
height: 7px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
gap: 2px;
|
||||
border-radius: 5px;
|
||||
background: var(--line);
|
||||
margin: 7px 0 5px;
|
||||
}
|
||||
#panel-overview .backend-mode-bar span { min-width: 0; }
|
||||
#panel-overview .backend-mode-h1 { background: var(--traffic-h1); }
|
||||
#panel-overview .backend-mode-auto { background: var(--traffic-h2); }
|
||||
#panel-overview .backend-mode-compat { background: var(--traffic-h2); opacity: .5; }
|
||||
#panel-overview .backend-mode-h2 { background: var(--traffic-h3); }
|
||||
|
||||
#panel-overview .backend-mode-bar[aria-label] { background-clip: padding-box; }
|
||||
.vh-origin-protocol-info { color: var(--muted); }
|
||||
.vh-origin-protocol-warning { color: var(--muted); }
|
||||
|
||||
/* Overview arrangement dialog: visual clones never participate in metric updates. */
|
||||
.overview-layout-dialog {
|
||||
width: min(1180px, calc(100vw - 24px));
|
||||
max-height: calc(100dvh - 24px);
|
||||
overflow: auto;
|
||||
}
|
||||
.overview-layout-dialog > .window-heading {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: var(--bg);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.overview-layout-body { display: grid; gap: 16px; padding: 14px; }
|
||||
.overview-layout-body h3 { font-size: 14px; margin: 0 0 10px; }
|
||||
.overview-layout-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--ui-gap, 10px); }
|
||||
.overview-layout-tile { min-width: 0; border: 1px solid var(--line); background: var(--panel); cursor: grab; }
|
||||
.overview-layout-tile:active { cursor: grabbing; }
|
||||
.overview-layout-tile.is-dragging { opacity: .4; }
|
||||
.overview-layout-tile.is-drop-target { outline: 2px solid var(--accent); outline-offset: -2px; }
|
||||
.overview-layout-tile-toolbar { display: flex; align-items: center; gap: 3px; padding: 4px; border-bottom: 1px solid var(--line); }
|
||||
.overview-layout-tile-toolbar button { min-height: 28px; min-width: 28px; padding: 3px 7px; }
|
||||
.overview-layout-grip { padding: 0 5px; color: var(--muted); user-select: none; }
|
||||
.overview-layout-hide { margin-inline-start: auto; font-size: 11px; }
|
||||
.overview-layout-grid .stat { border: 0; padding: 12px; min-height: 98px; pointer-events: none; }
|
||||
.overview-layout-grid .stat > span { font-size: 11px; }
|
||||
.overview-layout-grid .stat strong { font-size: 24px; margin-top: 7px; overflow-wrap: anywhere; }
|
||||
.overview-layout-grid .stat small { display: block; font-size: 10px; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.overview-layout-grid .stat svg { width: 100%; height: 53px; }
|
||||
.overview-layout-grid .stat-chart-legend { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.overview-layout-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.overview-layout-section-heading input { max-width: 230px; }
|
||||
.overview-layout-hidden-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
|
||||
.overview-layout-hidden-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; border: 1px solid var(--line); padding: 8px; background: var(--panel); min-width: 0; }
|
||||
.overview-layout-hidden-item span { font-size: 12px; min-width: 0; overflow-wrap: anywhere; }
|
||||
.overview-layout-hidden-item button { flex-shrink: 0; }
|
||||
.overview-layout-footer { position: sticky; bottom: 0; display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 12px; border-top: 1px solid var(--line); background: var(--bg); z-index: 2; }
|
||||
.overview-layout-footer p { margin: 0 auto 0 0; font-size: 11px; overflow-wrap: anywhere; }
|
||||
.dashboard-layout-open-settings { margin: 5px 0; justify-self: start; }
|
||||
@media (max-width: 850px) {
|
||||
.overview-layout-grid, .overview-layout-hidden-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 550px) {
|
||||
.overview-layout-grid, .overview-layout-hidden-list { grid-template-columns: minmax(0, 1fr); }
|
||||
.overview-layout-section-heading { align-items: stretch; flex-direction: column; }
|
||||
.overview-layout-section-heading input { max-width: none; width: 100%; }
|
||||
.overview-layout-footer { flex-wrap: wrap; }
|
||||
.overview-layout-footer p { width: 100%; }
|
||||
}
|
||||
|
||||
/* One rearrangeable Overview canvas. Existing large charts, diagnostics and the
|
||||
backend table keep their original nodes/IDs and their data update handlers. */
|
||||
#panel-overview .dashboard-overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: var(--ui-gap);
|
||||
align-items: stretch;
|
||||
}
|
||||
#panel-overview .dashboard-overview-grid > [data-dashboard-widget] { min-width: 0; }
|
||||
#panel-overview .dashboard-overview-grid > [hidden] { display: none !important; }
|
||||
#panel-overview .dashboard-overview-grid > .card:not(.pool-card) { grid-column: span 2; }
|
||||
#panel-overview .dashboard-overview-grid > .pool-card { grid-column: 1 / -1; margin-bottom: 0; }
|
||||
#panel-overview .dashboard-overview-grid > .activity-card svg { min-width: 0; }
|
||||
@media (max-width: 1200px) {
|
||||
#panel-overview .dashboard-overview-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
#panel-overview .dashboard-overview-grid > .stat { grid-column: span 2; }
|
||||
#panel-overview .dashboard-overview-grid > .card:not(.pool-card) { grid-column: span 2; }
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
#panel-overview .dashboard-overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
#panel-overview .dashboard-overview-grid > .stat { grid-column: span 1; }
|
||||
#panel-overview .dashboard-overview-grid > .card:not(.pool-card) { grid-column: 1 / -1; }
|
||||
}
|
||||
@media (max-width: 550px) {
|
||||
#panel-overview .dashboard-overview-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
#panel-overview .dashboard-overview-grid > .stat { grid-column: 1; }
|
||||
}
|
||||
.overview-layout-tile.is-panel { grid-column: span 2; }
|
||||
.overview-layout-grid .overview-layout-panel-preview {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
padding: 12px;
|
||||
min-height: 98px;
|
||||
background: var(--panel);
|
||||
pointer-events: none;
|
||||
}
|
||||
.overview-layout-panel-preview > strong { font-size: 13px; }
|
||||
.overview-layout-panel-preview svg { width: 100%; height: 80px; }
|
||||
.overview-layout-panel-preview small { color: var(--muted); }
|
||||
.overview-layout-footer #overview-layout-default { margin-right: auto; }
|
||||
.overview-reset-confirm { width: min(460px, calc(100vw - 24px)); padding: 16px; }
|
||||
.overview-reset-confirm > p { line-height: 1.5; }
|
||||
@media (max-width: 850px) { .overview-layout-tile.is-panel { grid-column: span 2; } }
|
||||
@media (max-width: 550px) { .overview-layout-tile.is-panel { grid-column: span 1; } }
|
||||
|
||||
/* Overview block kinds come from the same server-validated registry as the
|
||||
saved order. Labels distinguish numbers, plots, tables and diagnostic panels. */
|
||||
.overview-widget-type {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
background: var(--panel2);
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.25;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.dashboard-widget-setting .dashboard-widget-toggle { flex-wrap: wrap; }
|
||||
.dashboard-widget-setting .overview-widget-type { margin-left: 15px; }
|
||||
.overview-layout-tile-toolbar .overview-widget-type { margin-left: 3px; }
|
||||
.overview-layout-hidden-item .overview-widget-type { flex: 0 0 auto; }
|
||||
.overview-layout-filters { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.overview-layout-filters select, .overview-layout-filters input { width: min(220px, 100%); max-width: 100%; }
|
||||
|
||||
/* Inline editing of the actual Overview blocks; live charts and original DOM
|
||||
IDs remain in place. Dragging is enabled only on the explicit grip button. */
|
||||
.overview-inline-actions { display: inline-flex; gap: 7px; align-items: center; flex-wrap: wrap; }
|
||||
.overview-inline-actions[hidden], .overview-inline-hint[hidden] { display: none !important; }
|
||||
.overview-inline-hint { margin: 0 0 10px; }
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked > [data-dashboard-widget] {
|
||||
position: relative;
|
||||
outline: 1px dashed var(--accent);
|
||||
outline-offset: -2px;
|
||||
padding-top: 66px;
|
||||
}
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked > .is-overview-dragging { opacity: .45; }
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked > .is-overview-drop-before {
|
||||
box-shadow: inset 4px 0 var(--accent);
|
||||
}
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked > .is-overview-drop-after {
|
||||
box-shadow: inset -4px 0 var(--accent);
|
||||
}
|
||||
.overview-inline-toolbar {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
#panel-overview .overview-inline-toolbar button {
|
||||
min-width: 28px;
|
||||
min-height: 27px;
|
||||
padding: 2px 7px;
|
||||
font-size: 12px;
|
||||
}
|
||||
#panel-overview .overview-inline-toolbar .overview-inline-grip { cursor: grab; user-select: none; }
|
||||
#panel-overview .overview-inline-toolbar .overview-inline-grip:active { cursor: grabbing; }
|
||||
#panel-overview .overview-inline-toolbar .overview-widget-type { margin-left: auto; }
|
||||
@media (max-width: 600px) {
|
||||
.overview-layout-hidden-item { flex-wrap: wrap; }
|
||||
.overview-layout-filters select, .overview-layout-filters input { width: 100%; }
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked > [data-dashboard-widget] { padding-top: 68px; }
|
||||
}
|
||||
|
||||
|
||||
/* Direct Overview editing: hide is reversible until Save and remains
|
||||
recoverable later from the block selector or the compact hidden shelf. */
|
||||
#panel-overview .overview-inline-toolbar .overview-inline-hide {
|
||||
color: var(--red);
|
||||
border: 1px solid var(--danger-line);
|
||||
background: var(--panel);
|
||||
/* Use the same .quiet radius/corner preset as the arrow buttons. */
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
#panel-overview .overview-inline-toolbar .overview-inline-hide:hover,
|
||||
#panel-overview .overview-inline-toolbar .overview-inline-hide:focus-visible {
|
||||
border-color: var(--danger-line-strong);
|
||||
background: var(--notice-bg);
|
||||
}
|
||||
.overview-inline-hidden {
|
||||
margin: 0 0 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
}
|
||||
.overview-inline-hidden > summary {
|
||||
padding: 9px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.overview-inline-hidden > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
.overview-inline-hidden-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
max-height: min(30vh, 240px);
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.overview-inline-hidden-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
max-width: 100%;
|
||||
padding: 4px 7px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
color: var(--text);
|
||||
background: var(--panel2);
|
||||
font-size: 12px;
|
||||
}
|
||||
.overview-inline-hidden-item > span:not(.overview-widget-type) { overflow-wrap: anywhere; }
|
||||
.overview-inline-hidden-item > button {
|
||||
flex: 0 0 auto;
|
||||
min-width: 27px;
|
||||
min-height: 27px;
|
||||
padding: 1px 7px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--field-line);
|
||||
color: var(--green);
|
||||
background: var(--panel);
|
||||
}
|
||||
.overview-inline-hidden-item > button:hover { border-color: var(--accent); }
|
||||
@media (max-width: 600px) {
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked > [data-dashboard-widget] { padding-top: 82px; }
|
||||
.overview-inline-hidden-item { max-width: 100%; }
|
||||
}
|
||||
|
||||
/* The original fixed Overview grid, also used for personalized layouts.
|
||||
Metrics, wide charts, diagnostic panels and tables are separate rows;
|
||||
every tile in a row stretches to the SAME height. This intentionally
|
||||
favors symmetry over masonry or floating short cards beside tall panels.
|
||||
Existing density controls define the identical horizontal/vertical gap. */
|
||||
#panel-overview { container: overview-canvas / inline-size; }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: auto;
|
||||
align-items: stretch;
|
||||
align-content: start;
|
||||
gap: var(--ui-gap);
|
||||
}
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget] {
|
||||
grid-column: auto / span 2;
|
||||
grid-row: auto;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
margin-block: 0;
|
||||
position: relative;
|
||||
}
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .card:not(.pool-card) { grid-column: auto / span 4; }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .card.overview-wide-chart { grid-column: auto / span 6; }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .pool-card { grid-column: 1 / -1; }
|
||||
/* Each size category starts at the first track, even after drag/drop. */
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .overview-row-start,
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .card.overview-row-start { grid-column-start: 1; }
|
||||
@container overview-canvas (max-width: 1080px) {
|
||||
#panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: auto / span 2; }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .card:not(.pool-card) { grid-column: auto / span 3; }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .card.overview-wide-chart,
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .pool-card { grid-column: 1 / -1; }
|
||||
}
|
||||
@container overview-canvas (max-width: 700px) {
|
||||
#panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: auto / span 1; }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > .card:not(.pool-card) { grid-column: 1 / -1; }
|
||||
}
|
||||
@container overview-canvas (max-width: 440px) {
|
||||
#panel-overview .dashboard-overview-grid.is-aligned { grid-template-columns: minmax(0, 1fr); }
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].stat { grid-column: 1; }
|
||||
}
|
||||
/* Keep category boundaries intact at every responsive breakpoint. */
|
||||
#panel-overview .dashboard-overview-grid.is-aligned > [data-dashboard-widget].overview-row-start { grid-column-start: 1; }
|
||||
|
||||
/* Full-size optional charts: static SVG geometry and bounded historical data. */
|
||||
#panel-overview .overview-wide-chart { padding: 15px; overflow: hidden; }
|
||||
#panel-overview .overview-wide-chart .card-heading { min-width: 0; align-items: baseline; gap: 10px; }
|
||||
#panel-overview .overview-wide-chart h2 { min-width: 0; overflow-wrap: anywhere; }
|
||||
#panel-overview .overview-wide-summary { color: var(--accent); font: 500 12px var(--mono); white-space: nowrap; }
|
||||
#panel-overview .overview-wide-svg { display: block; width: 100%; height: clamp(125px, 13vw, 190px); margin-block: 13px 9px; }
|
||||
#panel-overview .overview-wide-svg .chart-grid { stroke: var(--line); fill: none; stroke-width: 1; opacity: .6; }
|
||||
#panel-overview .overview-wide-series { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
||||
#panel-overview .overview-wide-series-0 { stroke: var(--chart-accent); }
|
||||
#panel-overview .overview-wide-series-1 { stroke: var(--chart-secondary); }
|
||||
#panel-overview .overview-wide-series-2 { stroke: var(--chart-tertiary); }
|
||||
#panel-overview .overview-wide-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; color: var(--muted); font-size: 11px; }
|
||||
#panel-overview .overview-wide-legend > span { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
|
||||
#panel-overview .overview-wide-legend i { width: 8px; height: 8px; border-radius: 100%; background: var(--chart-accent); }
|
||||
#panel-overview .overview-wide-legend .overview-wide-key-1 { background: var(--chart-secondary); }
|
||||
#panel-overview .overview-wide-legend .overview-wide-key-2 { background: var(--chart-tertiary); }
|
||||
#panel-overview .overview-wide-legend b { font: 500 11px var(--mono); color: var(--text); }
|
||||
/* Block metadata stays to the left of every action group. */
|
||||
.dashboard-widget-setting .dashboard-widget-toggle { flex-wrap: nowrap; }
|
||||
.dashboard-widget-setting .dashboard-widget-toggle .overview-widget-type { margin: 0 0 0 auto; flex: 0 0 auto; }
|
||||
.dashboard-widget-setting .dashboard-widget-setting-actions { justify-self: end; flex-shrink: 0; }
|
||||
.overview-layout-tile-toolbar { flex-wrap: wrap; }
|
||||
.overview-layout-tile-toolbar .overview-widget-type { margin: 0; }
|
||||
.overview-layout-tile-toolbar .overview-layout-controls { display: inline-flex; align-items: center; gap: 3px; margin-left: auto; }
|
||||
.overview-layout-hidden-item > button { margin-left: auto; }
|
||||
.overview-layout-hidden-item > .overview-widget-type { margin: 0; }
|
||||
#panel-overview .overview-inline-toolbar .overview-widget-type { margin: 0 auto 0 0; }
|
||||
#panel-overview .overview-inline-toolbar .overview-inline-hide { margin-left: 0; }
|
||||
.overview-inline-hidden-item > .overview-widget-type { margin-right: 0; }
|
||||
.overview-inline-hidden-item > .overview-inline-restore { order: 3; margin-left: auto; }
|
||||
|
||||
/* Appearance selector: always show the block-kind label, independently from
|
||||
the visibility button, with actions pinned to the right edge. */
|
||||
.dashboard-widget-list {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
|
||||
}
|
||||
.dashboard-widget-setting {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 6px;
|
||||
}
|
||||
.dashboard-widget-setting .dashboard-widget-toggle {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.dashboard-widget-setting .dashboard-widget-setting-label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.dashboard-widget-setting > .overview-widget-type {
|
||||
flex: none;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
.dashboard-widget-setting > .dashboard-widget-setting-actions {
|
||||
justify-self: end;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Global shape preference. "default" applies NO overrides so existing dark,
|
||||
light, cream and Ultra Dark keep the same established corner geometry.
|
||||
Circular status dots, spinners, switches and chart markers retain their
|
||||
semantic shape; underline tabs and navigation accent strips stay intact. */
|
||||
html[data-corner-style="square"] { --rp-radius-surface: 0px; --rp-radius-control: 0px; }
|
||||
html[data-corner-style="subtle"] { --rp-radius-surface: 5px; --rp-radius-control: 4px; }
|
||||
html[data-corner-style="rounded"] { --rp-radius-surface: 12px; --rp-radius-control: 8px; }
|
||||
html[data-corner-style="extra"] { --rp-radius-surface: 18px; --rp-radius-control: 12px; }
|
||||
html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(
|
||||
.card, .stat, dialog, .login-card, .appearance-section, .settings-tabs,
|
||||
.dashboard-widget-setting, .overview-layout-tile, .overview-layout-hidden-item,
|
||||
.overview-inline-hidden-item, .notice, .toast,
|
||||
.chart-style-choice, .stream-dashboard-card, .pool-card,
|
||||
.file-log-advanced-parser, .table-container, .metric-info-popover
|
||||
) { border-radius: var(--rp-radius-surface) !important; }
|
||||
html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(
|
||||
button:not(.nav):not(.file-log-tab), input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
||||
select, textarea, .pill, .overview-widget-type, .badge, .quiet, .outline, .primary,
|
||||
.code, .command, .dashboard-widget-toggle, .settings-tabs button,
|
||||
.overview-inline-hidden-toggle, .overview-inline-restore
|
||||
) { border-radius: var(--rp-radius-control) !important; }
|
||||
|
||||
/* Appearance stays badge-free; badges remain useful in the visual editor. */
|
||||
#settings-page-appearance .dashboard-widget-setting .overview-widget-type { display: none; }
|
||||
@@ -198,7 +198,8 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
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"]] });
|
||||
const protocol = field(main, "member-protocol", "vhProtocol", value.protocol || "http1", "select", { values: [["http1", "vhProtocolHttp1"], ["auto", "vhProtocolAuto"], ["http2", "vhProtocolHttp2"], ["h2", "vhProtocolH2Strict"]] });
|
||||
attachOriginProtocolDetails(main, protocol, url);
|
||||
|
||||
const routing = el("div", undefined, "vh-wide vh-backend-switches"); main.append(routing);
|
||||
const controls = {};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
function attachOriginProtocolDetails(main, protocol, url) {
|
||||
const transportInfo = el("small", undefined, "fine vh-wide vh-origin-protocol-info");
|
||||
const compatibility = el("small", undefined, "fine vh-wide vh-origin-protocol-warning");
|
||||
compatibility.dataset.i18n = "vhProtocolCompatibilityWarning";
|
||||
compatibility.textContent = t("vhProtocolCompatibilityWarning");
|
||||
main.append(transportInfo, compatibility);
|
||||
const explainProtocol = () => {
|
||||
const https = /^https:\/\//i.test(url.value.trim());
|
||||
const name = protocol.value === "http1" ? "vhProtocolHttp1Details" :
|
||||
protocol.value === "auto" ? (https ? "vhProtocolAutoHttpsDetails" : "vhProtocolAutoHttpDetails") :
|
||||
protocol.value === "http2" ? (https ? "vhProtocolLegacyHttp2HttpsDetails" : "vhProtocolLegacyHttp2HttpDetails") :
|
||||
(https ? "vhProtocolH2HttpsDetails" : "vhProtocolH2HttpDetails");
|
||||
transportInfo.dataset.i18n = name;
|
||||
transportInfo.textContent = t(name);
|
||||
compatibility.hidden = protocol.value !== "http2" || !https;
|
||||
};
|
||||
protocol.addEventListener("change", explainProtocol);
|
||||
url.addEventListener("input", explainProtocol);
|
||||
explainProtocol();
|
||||
}
|
||||
Reference in new issue
Block a user