v0.36.10
This commit is contained in:
1 parent
2789991452
commit
e260612ef9
60 files changed
+69
-339
No files matched your search
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.36.9",
|
||||
"version": "0.36.10",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust. The API also permits explicitly scoped GeneralApiBearer tokens on a narrow route allowlist, with RO/RW and source CIDR checks; API-token management and Prometheus-token management are browser-session-only."
|
||||
},
|
||||
"servers": [
|
||||
|
||||
@@ -144,13 +144,10 @@ struct AdminPreferences {
|
||||
dashboard_hidden_widgets: Vec<String>,
|
||||
#[serde(default)]
|
||||
dashboard_widget_sizes: Option<std::collections::BTreeMap<String, u8>>,
|
||||
/// Re-flow the custom Overview layout for the current screen width (on by default).
|
||||
#[serde(default = "default_admin_dashboard_auto_fit")]
|
||||
dashboard_auto_fit: bool,
|
||||
/// Left menu collapsed to a narrow rail on wide screens (off by default).
|
||||
#[serde(default)]
|
||||
sidebar_collapsed: bool,
|
||||
/// Optional per-card heights in Overview row units (1..=32, a compact stat card is 2); missing cards size to content.
|
||||
#[serde(default)]
|
||||
dashboard_widget_heights: Option<std::collections::BTreeMap<String, u8>>,
|
||||
#[serde(default = "default_file_log_columns")]
|
||||
|
||||
@@ -1020,7 +1020,6 @@ mod tests {
|
||||
);
|
||||
assert!(json["config"].get("user_agent_blocking").is_none());
|
||||
|
||||
// A partial section update must not silently reset the existing UA policy.
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"expected_revision":1,
|
||||
"value": {"method_policy":{"mode":"off","methods":[]}}
|
||||
@@ -1043,7 +1042,6 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// GET returns the same section shape that PUT accepts.
|
||||
let read = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
|
||||
@@ -21,7 +21,6 @@ function syncConfigHistoryRestoreButton() {
|
||||
button.hidden = configHistorySelectedRevision === null;
|
||||
button.disabled = busy || !envelope || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision);
|
||||
}
|
||||
// Leaf-level diff keyed by object IDs, so reordering is not a change.
|
||||
function configDiff(before, after, path = "$", output = [], depth = 0) {
|
||||
if (output.length >= 200 || Object.is(before, after)) return output;
|
||||
if (depth >= 24) {
|
||||
@@ -58,7 +57,6 @@ function configDiff(before, after, path = "$", output = [], depth = 0) {
|
||||
}
|
||||
return output;
|
||||
}
|
||||
// New/deleted trees are traversed to show the affected fields only.
|
||||
const tree = oldObj ? before : newObj ? after : null;
|
||||
if (tree && !Array.isArray(tree)) {
|
||||
for (const key of Object.keys(tree).sort()) {
|
||||
|
||||
+28
-28
@@ -65,7 +65,7 @@ function translate() {
|
||||
}
|
||||
function toastNode() {
|
||||
let node = document.getElementById("toast");
|
||||
if (!node) { // the node lived inside a dialog that was rebuilt; recreate it instead of failing every later toast
|
||||
if (!node) {
|
||||
node = document.createElement("div"); node.id = "toast"; node.className = "toast"; node.hidden = true;
|
||||
node.setAttribute("role", "status"); node.setAttribute("aria-live", "polite"); node.setAttribute("popover", "manual"); document.body.append(node);
|
||||
}
|
||||
@@ -291,11 +291,15 @@ function redrawOverviewHistoryCharts() {
|
||||
}
|
||||
const httpValues = Object.values(protocolSamples), websocketValues = Object.values(websocketProtocolSamples);
|
||||
window.rustProxyChartScale.axes("http-protocol-chart", Math.max(1, ...httpValues.flat()), "rate",
|
||||
protocolSamples.h1.length, { seriesScales: Object.entries(protocolSamples).map(([name, values]) =>
|
||||
[name.toUpperCase(), Math.max(1, ...values)]) });
|
||||
protocolSamples.h1.length, {
|
||||
seriesScales: Object.entries(protocolSamples).map(([name, values]) =>
|
||||
[name.toUpperCase(), Math.max(1, ...values)])
|
||||
});
|
||||
window.rustProxyChartScale.axes("ws-protocol-chart", Math.max(1, ...websocketValues.flat()), "count",
|
||||
websocketProtocolSamples.h1.length, { seriesScales: Object.entries(websocketProtocolSamples).map(([name, values]) =>
|
||||
[name.toUpperCase(), Math.max(1, ...values)]) });
|
||||
websocketProtocolSamples.h1.length, {
|
||||
seriesScales: Object.entries(websocketProtocolSamples).map(([name, values]) =>
|
||||
[name.toUpperCase(), Math.max(1, ...values)])
|
||||
});
|
||||
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
|
||||
}
|
||||
window.rustProxyOverviewSamples.onChange(() => {
|
||||
@@ -305,8 +309,7 @@ window.rustProxyOverviewSamples.onChange(() => {
|
||||
}
|
||||
redrawOverviewHistoryCharts();
|
||||
});
|
||||
// Safari has no CSS scroll anchoring: keep the element under the viewport centre in place
|
||||
// when a live refresh changes the height of content above it.
|
||||
|
||||
const nativeScrollAnchoring = typeof CSS !== "undefined" && CSS.supports?.("overflow-anchor", "auto");
|
||||
function preserveScrollAnchor(render) {
|
||||
if (nativeScrollAnchoring || window.scrollY <= 0) return render();
|
||||
@@ -514,8 +517,6 @@ async function activate(info) {
|
||||
renderSession();
|
||||
document.documentElement.classList.remove("app-startup-failed");
|
||||
rustProxyRefresh.start();
|
||||
// Keep the startup screen until the account preferences (menu, layout, density)
|
||||
// are applied, so the panel never appears first with defaults and then changes.
|
||||
let preferences;
|
||||
try { preferences = await loadAdminPreferences(); }
|
||||
finally { document.documentElement.classList.remove("app-loading"); }
|
||||
@@ -606,8 +607,6 @@ document.addEventListener("click", event => { if (dashboardSidebar?.classList.co
|
||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && dashboardSidebar?.classList.contains("mobile-nav-open")) { setMobileMenu(false); mobileMenuToggle?.focus(); } });
|
||||
window.addEventListener("popstate", () => { setPanel(panelFromLocation(), false); setMobileMenu(false); });
|
||||
$("host-policy-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const hosts = [...new Set($("policy-hosts").value.split(",").map(h => h.trim().toLowerCase()).filter(Boolean))]; const acl = { default: $("policy-default").value, rules: parseRules($("policy-rules").value, $("policy-rules")) }; const next = structuredClone(envelope.config); next.host_policies = (next.host_policies || []).map(p => ({ ...p, hosts: p.hosts.filter(h => !hosts.includes(h)) })).filter(p => p.hosts.length); next.host_policies.push({ hosts, acl }); if (await saveSection("host-policies", next.host_policies)) { form.reset(); if ($("security-policy-editor")) $("security-policy-editor").open = false; } }); } catch (e) { toast(e.message, true); } });
|
||||
// Global HTTP controls are a single atomic API section. A header-only save preserves
|
||||
// the current block rules; a block-only save preserves the current security headers.
|
||||
const globalLines = id => [...new Set($(id).value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean))];
|
||||
const globalPatternLines = id => [...new Set($(id).value.split(/\r?\n/).map(v => v.trim()).filter(Boolean))];
|
||||
function globalSecurityPayload(security, ua) {
|
||||
@@ -635,10 +634,12 @@ $("global-security-form")?.addEventListener("submit", async event => {
|
||||
acl,
|
||||
method_policy: { mode: methodMode, methods },
|
||||
content_type_policy: { enabled: $("global-content-type-enabled").checked, blocked: contentTypes },
|
||||
rate_limit: { ...rate, enabled: $("global-rate-enabled").checked,
|
||||
rate_limit: {
|
||||
...rate, enabled: $("global-rate-enabled").checked,
|
||||
requests: Number($("global-rate-requests").value),
|
||||
window_seconds: Number($("global-rate-window").value),
|
||||
burst: Number($("global-rate-burst").value) },
|
||||
burst: Number($("global-rate-burst").value)
|
||||
},
|
||||
};
|
||||
if (await saveSection("global-security", globalSecurityPayload(security, ua))) {
|
||||
setSettingsDirty("global-security-form", false); renderSettings();
|
||||
@@ -652,16 +653,20 @@ $("global-security-headers-form")?.addEventListener("submit", async event => {
|
||||
await withFormSubmitState(form, event.submitter, async () => {
|
||||
const previous = envelope.config.global_security || {};
|
||||
const optional = id => $(id).value.trim() || null;
|
||||
const security = { ...previous, security_headers: {
|
||||
hsts: { enabled: $("global-hsts-enabled").checked,
|
||||
max_age_seconds: Number($("global-hsts-age").value),
|
||||
include_subdomains: $("global-hsts-enabled").checked && $("global-hsts-sub").checked,
|
||||
preload: $("global-hsts-enabled").checked && $("global-hsts-preload").checked },
|
||||
nosniff: $("global-nosniff").checked, frame_options: $("global-frame").value,
|
||||
referrer_policy: optional("global-referrer"),
|
||||
content_security_policy: optional("global-csp"),
|
||||
permissions_policy: optional("global-permissions")
|
||||
}};
|
||||
const security = {
|
||||
...previous, security_headers: {
|
||||
hsts: {
|
||||
enabled: $("global-hsts-enabled").checked,
|
||||
max_age_seconds: Number($("global-hsts-age").value),
|
||||
include_subdomains: $("global-hsts-enabled").checked && $("global-hsts-sub").checked,
|
||||
preload: $("global-hsts-enabled").checked && $("global-hsts-preload").checked
|
||||
},
|
||||
nosniff: $("global-nosniff").checked, frame_options: $("global-frame").value,
|
||||
referrer_policy: optional("global-referrer"),
|
||||
content_security_policy: optional("global-csp"),
|
||||
permissions_policy: optional("global-permissions")
|
||||
}
|
||||
};
|
||||
if (await saveSection("global-security", globalSecurityPayload(security, previous.user_agent_blocking || { enabled: false, patterns: [] }))) {
|
||||
setSettingsDirty("global-security-headers-form", false); renderSettings();
|
||||
}
|
||||
@@ -682,7 +687,6 @@ $("config-editor").addEventListener("input", () => setSettingsDirty("config-edit
|
||||
$("save-json").addEventListener("click", async event => { const editor = $("config-editor"), page = editor.closest("[data-settings-page]"); if (!envelope || busy) return; try { await withFormSubmitState(page, event.currentTarget, async () => { let config; try { config = JSON.parse(editor.value); } catch { throw fieldError(editor, t("vhJsonInvalid")); } if (await save(config)) setSettingsDirty("config-editor", false); }); } catch (e) { toast(e.message, true); } });
|
||||
$("export-config").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify(envelope.config, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = `RustProxy-config-r${envelope.revision}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
|
||||
|
||||
// Auto-generated identifiers: a new entry's ID follows its name (or first host) as a unique slug until the user edits the ID by hand.
|
||||
const rustProxyAutoId = (() => {
|
||||
const FOLD = { "ł": "l", "đ": "d", "ø": "o", "ß": "ss", "æ": "ae", "œ": "oe", "þ": "th", "ð": "d" };
|
||||
const states = new WeakMap();
|
||||
@@ -700,7 +704,6 @@ const rustProxyAutoId = (() => {
|
||||
}
|
||||
const locked = input => input.readOnly || input.disabled;
|
||||
function suggestion(state) { return unique(slugify(state.text()), state.taken()); }
|
||||
// The ID is shown as a plain value with a "Change" link; the input only appears after the user asks for it (or when validation needs it).
|
||||
function paint(input, state) {
|
||||
const isLocked = locked(input), wanted = suggestion(state), value = input.value.trim(), compact = !state.open || isLocked;
|
||||
state.holder.classList.toggle("is-compact", compact);
|
||||
@@ -713,7 +716,6 @@ const rustProxyAutoId = (() => {
|
||||
state.reset.hidden = compact || state.auto || state.resettable === false || !wanted || wanted === value; state.reset.textContent = t("autoIdReset");
|
||||
}
|
||||
function regenerate(input, state) { if (!state.auto || locked(input)) return; input.value = suggestion(state); input.setCustomValidity?.(""); paint(input, state); }
|
||||
// options: { elements: inputs whose changes drive the ID, text: () => source string, taken: () => existing IDs }
|
||||
function attach(input, options) {
|
||||
let state = states.get(input);
|
||||
if (!state) {
|
||||
@@ -741,7 +743,6 @@ const rustProxyAutoId = (() => {
|
||||
})();
|
||||
window.rustProxyAutoId = rustProxyAutoId;
|
||||
|
||||
// Automatic certificate choice: for a new entry, pick the stored certificate that covers every host name (exact names beat wildcards); the user can still choose another one.
|
||||
const rustProxyAutoCertificate = (() => {
|
||||
const states = new WeakMap();
|
||||
const covers = (pattern, host) => {
|
||||
@@ -769,7 +770,6 @@ const rustProxyAutoCertificate = (() => {
|
||||
}
|
||||
return best;
|
||||
}
|
||||
// options: { hosts: () => host names, certificates: () => certificate objects, auto: boolean, anchor: element after which the hint is placed }
|
||||
function attach(select, options) {
|
||||
let state = states.get(select);
|
||||
if (!state) {
|
||||
|
||||
@@ -71,7 +71,6 @@ window.rustProxyInlineLayout = (() => {
|
||||
const value = node.querySelector(".overview-inline-width-value");
|
||||
if (value) {
|
||||
value.textContent = `${span}/12`;
|
||||
// Auto-fit may show the card wider on this screen than its saved width.
|
||||
const fitted = window.rustProxyOverviewRows?.fitFor?.(node.dataset.dashboardWidget);
|
||||
value.classList.toggle("is-fitted", !!fitted && draft.autoFit !== false);
|
||||
value.title = fitted && draft.autoFit !== false ? t("overviewLayoutAutoFitWidened").replace("{span}", String(fitted)) : t("overviewLayoutWidth");
|
||||
@@ -128,7 +127,6 @@ window.rustProxyInlineLayout = (() => {
|
||||
enterFreeLayout();
|
||||
draft.heights = { ...(draft.heights || {}) };
|
||||
const next = rows.span + delta;
|
||||
// At or below the content height the card returns to automatic sizing.
|
||||
if (next <= rows.min) delete draft.heights[id];
|
||||
else draft.heights[id] = Math.min(DASHBOARD_MAX_ROWS, next);
|
||||
paint();
|
||||
@@ -183,7 +181,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
unlock.setAttribute("aria-pressed", "true");
|
||||
unlock.dataset.i18n = "overviewLayoutLock";
|
||||
unlock.textContent = t("overviewLayoutLock");
|
||||
unlock.hidden = true; // Save and lock is the only save action while unlocked.
|
||||
unlock.hidden = true;
|
||||
actions.hidden = false;
|
||||
hint.hidden = false;
|
||||
hint.textContent = t("overviewLayoutInlineHint");
|
||||
@@ -213,7 +211,6 @@ window.rustProxyInlineLayout = (() => {
|
||||
applyDashboardLayout(original.order, [...original.hidden], original.sizes, original.heights);
|
||||
}
|
||||
draft = original = null;
|
||||
// Recompute from scratch so the saved view is exactly what was arranged.
|
||||
window.rustProxyOverviewRows?.flush?.();
|
||||
}
|
||||
async function begin() {
|
||||
@@ -253,9 +250,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
finally { saving = false; saveButton.disabled = cancelButton.disabled = unlock.disabled = false; }
|
||||
}
|
||||
function cancel() { if (draft && !saving) finish(false); }
|
||||
// Pointer-driven sorting with a live placeholder. The grid never back-fills
|
||||
// holes while a block is lifted, so the placeholder shows exactly where the
|
||||
// block lands; neighbours glide (FLIP) instead of jumping.
|
||||
|
||||
const DRAG_THRESHOLD = 5, EDGE = 64, MOVE_COOLDOWN_MS = 90;
|
||||
const reducedMotion = () => window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
|
||||
let pointerDrag = null;
|
||||
@@ -279,7 +274,6 @@ window.rustProxyInlineLayout = (() => {
|
||||
if (widget && grid.contains(widget) && widget !== drag.node && !widget.hidden) return { node: widget };
|
||||
if (hit === drag.placeholder || drag.placeholder.contains(hit)) return null;
|
||||
if (hit !== grid) return null;
|
||||
// Empty grid area (gap or hole at a row end): attach to the nearest block in that row.
|
||||
const candidates = nodes().filter(node => node !== drag.node && !node.hidden);
|
||||
let best = null, bestDistance = Infinity;
|
||||
for (const node of candidates) {
|
||||
@@ -300,7 +294,6 @@ window.rustProxyInlineLayout = (() => {
|
||||
if (!target) return;
|
||||
const children = [...grid.children];
|
||||
const placeholderIndex = children.indexOf(drag.placeholder), targetIndex = children.indexOf(target.node);
|
||||
// Entering a block swaps the placeholder past it; this is stable and never oscillates.
|
||||
const after = target.after ?? placeholderIndex < targetIndex;
|
||||
const reference = after ? target.node.nextSibling : target.node;
|
||||
if (reference === drag.placeholder || (after ? target.node.nextSibling === drag.placeholder : target.node.previousSibling === drag.placeholder)) return;
|
||||
@@ -417,8 +410,7 @@ window.rustProxyInlineLayout = (() => {
|
||||
nodeFor(handle.dataset.overviewDragHandle)?.querySelector(".overview-inline-grip")?.focus();
|
||||
});
|
||||
function compact() {
|
||||
// Explicit, one-off gap filling: reads the dense packing the browser would
|
||||
// produce and adopts it as the new order, so nothing moves unexpectedly later.
|
||||
|
||||
if (!draft) return;
|
||||
enterFreeLayout();
|
||||
paint();
|
||||
|
||||
@@ -20,8 +20,10 @@ window.rustProxyDashboardLayout = (() => {
|
||||
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)),
|
||||
sizes: normalizeDashboardWidgetSizes(prefs.dashboard_widget_sizes) };
|
||||
return {
|
||||
order, hidden: new Set(normalizeHiddenDashboardWidgets(prefs.dashboard_hidden_widgets, prefs.dashboard_widgets)),
|
||||
sizes: normalizeDashboardWidgetSizes(prefs.dashboard_widget_sizes)
|
||||
};
|
||||
}
|
||||
function defaultDraft() {
|
||||
return {
|
||||
@@ -144,7 +146,6 @@ window.rustProxyDashboardLayout = (() => {
|
||||
widen.addEventListener("click", () => resize(id, 1));
|
||||
const widthValue = el("span", `${width}/12`, "overview-layout-width-value");
|
||||
widthValue.title = t("overviewLayoutWidth");
|
||||
// Size and order controls are grouped separately so they wrap in narrow dialogs.
|
||||
const sizeActions = el("span", undefined, "overview-layout-size-actions");
|
||||
sizeActions.append(narrow, widthValue, widen);
|
||||
const orderActions = el("span", undefined, "overview-layout-order-actions");
|
||||
|
||||
@@ -131,16 +131,12 @@ function uiIconMatchesTranslation(target, keys) {
|
||||
const strings = uiIconStrings(target); if (!strings.length) return false;
|
||||
for (const key of keys) {
|
||||
const raw = String(t(key) || "").trim(), translated = raw.toLocaleLowerCase();
|
||||
// t() returns the key itself when a translation is missing; compare case-sensitively
|
||||
// so an English label such as "Edit" for the key "edit" still counts as translated.
|
||||
if (!translated || raw === key) continue;
|
||||
if (strings.some(value => value === translated || value.startsWith(`${translated} `) || value.startsWith(`${translated}:`))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Explicit choices for controls whose ids/i18n keys would otherwise collide with
|
||||
// broader keyword rules (for example "unlock" vs "lock", "presets" vs "reset",
|
||||
// "docker" vs "dock", or a domain noun such as "cache" before a verb such as "next").
|
||||
|
||||
const UI_ID_ICONS = Object.freeze({
|
||||
"about-open": "info", "login-submit": "login",
|
||||
"overview-layout-unlock": "unlock", "overview-inline-save": "lock", "overview-inline-compact": "grid",
|
||||
@@ -159,8 +155,7 @@ const UI_I18N_ICONS = Object.freeze({
|
||||
previous: "chevronLeft", next: "chevronRight", cachePrevious: "chevronLeft", cacheNext: "chevronRight",
|
||||
renewalEnable: "toggleOn", renewalDisable: "toggleOff", tunnelKill: "ban", certAssignVhosts: "network",
|
||||
});
|
||||
// Buttons whose visible label is already a single symbol (×, −, +, ←, →) keep the
|
||||
// native glyph instead of receiving a second, competing SVG.
|
||||
|
||||
const UI_GLYPH_LABEL = /^[×✕−+←→↑↓⠿]$/;
|
||||
function uiIconForTarget(target) {
|
||||
const manual = target.dataset.uiIconName;
|
||||
@@ -203,8 +198,7 @@ function uiIconForTarget(target) {
|
||||
if (/sort-direction|sort-button/.test(signature)) return /descending|desc/.test(aria) ? "sortDown" : "sortUp";
|
||||
if (/scroll-prev|move-earlier|layout-up|-prev\b/.test(signature)) return "chevronLeft";
|
||||
if (/scroll-next|move-later|layout-down|-next\b/.test(signature)) return "chevronRight";
|
||||
// Verbs win over domain nouns: a "Close" button inside the health or history
|
||||
// dialog is still a close action.
|
||||
|
||||
if (/close|cancel/.test(signature)) return "x";
|
||||
if (/sign-in|login/.test(signature)) return "login";
|
||||
if (/logout|sign-out/.test(signature)) return "logout";
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// Optional, account-scoped subtle animation of the sidebar r/p logo
|
||||
// (Settings → Appearance → Interface → Animate logo). Off by default.
|
||||
// Effects are short (≤1.2 s), run at random intervals, pause while the tab is
|
||||
// hidden and are skipped entirely when the system asks for reduced motion.
|
||||
|
||||
const LOGO_ANIMATION_EFFECTS = Object.freeze(["spin-slash", "flip", "wave", "glow", "sheen", "glitch", "wobble"]);
|
||||
const LOGO_ANIMATION_DURATION_MS = 1400;
|
||||
let logoAnimationTimer = null, logoAnimationReset = null, logoAnimationLast = "", logoAnimationHoverBound = false;
|
||||
@@ -9,7 +6,6 @@ function logoAnimationMark() { return document.querySelector(".sidebar a.brand >
|
||||
function logoAnimationReducedMotion() { return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true; }
|
||||
function prepareLogoAnimationMark(mark) {
|
||||
if (mark.dataset.logoPrepared === "true") return;
|
||||
// Wrap the bare "r" and "p" text nodes so each letter can be transformed on its own.
|
||||
for (const node of [...mark.childNodes]) {
|
||||
if (node.nodeType !== Node.TEXT_NODE || !node.textContent.trim()) continue;
|
||||
const letter = document.createElement("b"); letter.className = "brand-letter"; letter.textContent = node.textContent.trim();
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// Memory breakdown card (memory_breakdown): splits process RSS into cache data,
|
||||
// cache bookkeeping, cache buffers, allocator-held free memory, file-backed pages
|
||||
// and the rest of the process, and tracks the "outside cache" share over time so
|
||||
// a steady climb (a leak) is distinguishable from flat overhead.
|
||||
|
||||
window.rustProxyMemoryBreakdown = (() => {
|
||||
const $ = id => document.getElementById(id);
|
||||
const segments = ["cache-data", "cache-meta", "transient", "alloc-free", "files", "rest"];
|
||||
@@ -22,8 +19,7 @@ window.rustProxyMemoryBreakdown = (() => {
|
||||
let free = allocator ? (num(allocator.free_bytes) ?? 0) : 0;
|
||||
const values = { "cache-data": logical, "cache-meta": allocated - logical, transient, "alloc-free": free, files: fileBacked };
|
||||
let rest = rss - Object.values(values).reduce((sum, value) => sum + value, 0);
|
||||
// Allocator free memory may already be released to the kernel (malloc_trim),
|
||||
// so it is the first share to shrink when the parts exceed the measured RSS.
|
||||
|
||||
if (rest < 0) { const cut = Math.min(free, -rest); free -= cut; values["alloc-free"] = free; rest += cut; }
|
||||
values.rest = Math.max(0, rest);
|
||||
return {
|
||||
@@ -100,7 +96,6 @@ window.rustProxyMemoryBreakdown = (() => {
|
||||
const slope = span >= MIN_TREND_MS ? slopePerHour(samples) : null;
|
||||
let state = "collecting";
|
||||
if (slope != null) {
|
||||
// Rising faster than 2% of the current outside-cache size per hour (and at least 4 MiB/h).
|
||||
const threshold = Math.max(4 * 1048576, data.outside * 0.02);
|
||||
state = slope > threshold ? "rising" : slope < -threshold ? "falling" : "stable";
|
||||
}
|
||||
@@ -113,8 +108,10 @@ window.rustProxyMemoryBreakdown = (() => {
|
||||
}
|
||||
|
||||
function rowKey(key) {
|
||||
return { "cache-data": "memoryCacheData", "cache-meta": "memoryCacheMeta", transient: "memoryTransient",
|
||||
"alloc-free": "memoryAllocatorFree", files: "memoryFiles", rest: "memoryRest" }[key];
|
||||
return {
|
||||
"cache-data": "memoryCacheData", "cache-meta": "memoryCacheMeta", transient: "memoryTransient",
|
||||
"alloc-free": "memoryAllocatorFree", files: "memoryFiles", rest: "memoryRest"
|
||||
}[key];
|
||||
}
|
||||
|
||||
window.rustProxyOverviewSamples?.onChange?.(() => { samples = samples.slice(-limit()); });
|
||||
|
||||
@@ -9,8 +9,7 @@ window.rustProxyMasonry = (() => {
|
||||
|
||||
function refresh() {
|
||||
const previous = widgets();
|
||||
// Existing layouts (without a width map) retain their historical rows.
|
||||
// Once a user moves/resizes a block, DOM order is the actual order.
|
||||
|
||||
const legacy = grid.dataset.overviewLayoutMode !== "custom";
|
||||
const ordered = legacy ? previous.map((node, index) => ({ node, index }))
|
||||
.sort((a, b) => group(a.node) - group(b.node) || a.index - b.index)
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
// Row units for the custom Overview layout. Widths stay on the 12-column grid;
|
||||
// heights snap to a shared row unit (the tallest compact stat card), so a tall
|
||||
// panel can sit next to several rows of small cards and every edge lines up.
|
||||
// Each card gets at least the rows its content needs; an explicit height
|
||||
// (dashboard_widget_heights) can only make it taller, and charts stretch into
|
||||
// the extra space. Below the narrow breakpoint the grid falls back to content
|
||||
// height (CSS sets --overview-row-units: off).
|
||||
// With auto-fit on (the default) the layout is packed for the current screen:
|
||||
// every card keeps its saved width (but at least a readable minimum for its type),
|
||||
// cards are placed in saved order into the first free spot from the top, stat
|
||||
// tiles of one row get one height, remaining holes are filled by widening a
|
||||
// neighbouring card sideways, and small steps at the bottom are closed. The
|
||||
// saved widths, heights and order are never changed.
|
||||
window.rustProxyOverviewRows = (() => {
|
||||
const grid = document.getElementById("dashboard-overview-grid");
|
||||
// A compact stat card spans ROW_DIVISIONS rows; finer rows keep taller cards
|
||||
// close to their natural height instead of rounding up by a whole stat card.
|
||||
const MAX_ROWS = 32, ROW_DIVISIONS = 2, FALLBACK_STAT = 84, MIN_INTERVAL_MS = 1500, SHRINK_CONFIRMATIONS = 3;
|
||||
// Auto-fit (dashboard_auto_fit): the smallest readable width of a card by its
|
||||
// registry display type, in CSS pixels. Cards narrower than this on the current
|
||||
// screen get more columns; the saved widths are not changed.
|
||||
const MIN_CARD_WIDTH = { value: 150, value_chart: 210, chart: 300, panel: 340, table: 640 };
|
||||
const noop = { schedule() { }, flush() { }, setHeights() { }, setAutoFit() { }, reveal() { }, dataReady() { }, rowsFor() { return null; }, fitFor() { return null; } };
|
||||
if (!grid) return noop;
|
||||
@@ -57,7 +39,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
function widgetType(node) {
|
||||
return typeof dashboardWidgetType === "function" ? dashboardWidgetType(node.dataset.dashboardWidget) : "value";
|
||||
}
|
||||
// Widen cards that would be narrower than their readable minimum on this screen.
|
||||
function widenToMinimum(items, geometry) {
|
||||
const columns = geometry.starts.length;
|
||||
const column = geometry.starts.length > 1 ? geometry.starts[1] - geometry.starts[0] : 0;
|
||||
@@ -79,13 +60,9 @@ window.rustProxyOverviewRows = (() => {
|
||||
const items = cards().filter(node => !node.hidden && node.getClientRects().length);
|
||||
if (!items.length) return;
|
||||
clearFit();
|
||||
// Measure as the saved (locked) dashboard: editing styles move a few
|
||||
// controls inside cards, which must not change the layout being edited.
|
||||
const editing = grid.classList.contains("is-unlocked");
|
||||
if (editing) grid.classList.remove("is-unlocked");
|
||||
grid.classList.add("has-row-units", "is-measuring-rows");
|
||||
// Bounding boxes include CSS zoom (Settings → Interface scale); grid tracks,
|
||||
// gaps and the row unit are in CSS pixels, so divide measurements by it.
|
||||
const zoom = zoomOf();
|
||||
if (autoFit) {
|
||||
const preliminary = gridGeometry(1, zoom);
|
||||
@@ -96,8 +73,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
if (editing) grid.classList.add("is-unlocked");
|
||||
const gap = parseFloat(getComputedStyle(grid).rowGap) || 0;
|
||||
const isStat = node => node.classList.contains("stat");
|
||||
// The unit is half of the smallest stat card, so every card ends within a
|
||||
// few pixels of its natural height instead of being rounded up by a whole tile.
|
||||
const stats = natural.filter((_, index) => isStat(items[index]) && natural[index] > 0);
|
||||
const statHeight = stats.length ? Math.min(...stats) : FALLBACK_STAT;
|
||||
const unit = Math.max(16, (statHeight - gap * (ROW_DIVISIONS - 1)) / ROW_DIVISIONS);
|
||||
@@ -106,11 +81,8 @@ window.rustProxyOverviewRows = (() => {
|
||||
items.forEach((node, index) => {
|
||||
const id = node.dataset.dashboardWidget;
|
||||
seen.add(id);
|
||||
// Small tolerance so sub-pixel rounding never adds a whole row.
|
||||
let min = Math.max(1, Math.ceil((natural[index] + gap) / (unit + gap) - 0.04));
|
||||
const previous = state.get(id);
|
||||
// Grow at once; shrink only after the smaller size is seen repeatedly,
|
||||
// so live data (table rows, wrapping labels) cannot make the grid jump.
|
||||
if (previous && min < previous.min) {
|
||||
const votes = (shrinkVotes.get(id) || 0) + 1;
|
||||
if (votes < SHRINK_CONFIRMATIONS) { shrinkVotes.set(id, votes); min = previous.min; }
|
||||
@@ -128,8 +100,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
equalizeStatRows(items.filter(isStat), geometry);
|
||||
alignBottom(items, geometry);
|
||||
}
|
||||
// Auto-fit packing on a model of the grid (columns × rows), then explicit
|
||||
// placement through CSS variables. Pure arithmetic: no layout passes per card.
|
||||
function packCards(items, geometry, isStat) {
|
||||
const columns = geometry.starts.length;
|
||||
const cardsModel = items.map(node => {
|
||||
@@ -166,9 +136,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
if (free(r, c, card.w, card.h)) { card.r = r; card.c = c; mark(r, c, card.w, card.h); placed = true; break; }
|
||||
}
|
||||
}
|
||||
// A free strip beside the new card (over its whole height) that is
|
||||
// narrower than every card still to be placed could never be filled:
|
||||
// the card takes it now instead of leaving a hole.
|
||||
let narrowest = Infinity;
|
||||
for (let next = index + 1; next < cardsModel.length; next++) narrowest = Math.min(narrowest, cardsModel[next].base);
|
||||
let right = 0;
|
||||
@@ -179,8 +146,7 @@ window.rustProxyOverviewRows = (() => {
|
||||
if (left > 0 && left < narrowest) { card.c -= left; card.w += left; mark(card.r, card.c, left, card.h); }
|
||||
});
|
||||
};
|
||||
// Stat tiles starting on the same row share the tallest height; re-pack until
|
||||
// stable (raising a row can move later cards).
|
||||
|
||||
const settle = () => {
|
||||
for (let pass = 0; pass < 6; pass++) {
|
||||
pack();
|
||||
@@ -196,10 +162,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
return Math.max(...cardsModel.map(card => card.r + card.h));
|
||||
};
|
||||
let total = settle();
|
||||
// A card whose side neighbour ends slightly lower (up to one stat tile) is
|
||||
// extended to that edge, so the cards below start on one line instead of a
|
||||
// staggered notch. Each extension is kept only while the whole dashboard
|
||||
// grows by at most two stat tiles in total, so it can never cascade.
|
||||
const snap = ROW_DIVISIONS, budget = total + ROW_DIVISIONS * 2;
|
||||
for (let attempt = 0; attempt < cardsModel.length; attempt++) {
|
||||
const candidates = [];
|
||||
@@ -222,8 +184,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
if (next > budget) { cardsModel.forEach((item, index) => { item.h = saved[index]; }); card.h = previous; total = settle(); }
|
||||
else total = next;
|
||||
}
|
||||
// Holes beside a card (over its whole height) are taken by widening it,
|
||||
// first to the right, then to the left.
|
||||
const order = [...cardsModel].sort((a, b) => a.r - b.r || a.c - b.c);
|
||||
const columnFree = (x, r, h) => {
|
||||
for (let y = r; y < r + h; y++) if (occupancy[y]?.[x]) return false;
|
||||
@@ -233,8 +193,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
while (card.c + card.w < columns && columnFree(card.c + card.w, card.r, card.h)) { mark(card.r, card.c + card.w, 1, card.h); card.w++; }
|
||||
while (card.c > 0 && columnFree(card.c - 1, card.r, card.h)) { card.c--; card.w++; mark(card.r, card.c, 1, card.h); }
|
||||
}
|
||||
// Free cells directly above a card (left by a taller neighbour that started
|
||||
// earlier) are taken by growing it upwards, within the same limit.
|
||||
const rowFree = (y, c, w) => {
|
||||
if (y < 0) return false;
|
||||
for (let x = c; x < c + w; x++) if (occupancy[y]?.[x]) return false;
|
||||
@@ -245,9 +203,7 @@ window.rustProxyOverviewRows = (() => {
|
||||
while (up < Math.max(ROW_DIVISIONS * 2, card.h) && rowFree(card.r - up - 1, card.c, card.w)) up++;
|
||||
if (up) { mark(card.r - up, card.c, card.w, up); card.r -= up; card.h += up; }
|
||||
}
|
||||
// Steps below a card (up to two stat tiles or its own height) are closed by
|
||||
// stretching it down to the next card or the end of the grid; charts and
|
||||
// stacks inside grow into the extra height.
|
||||
|
||||
const bottom = Math.max(...cardsModel.map(card => card.r + card.h));
|
||||
for (const card of order) {
|
||||
const end = card.r + card.h;
|
||||
@@ -256,8 +212,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
if (other !== card && other.c < card.c + card.w && other.c + other.w > card.c && other.r >= end) next = Math.min(next, other.r);
|
||||
}
|
||||
const extra = next - end;
|
||||
// At the end of the grid a chart or panel always reaches the last row (its
|
||||
// chart grows) instead of leaving an empty area; stat tiles stay limited.
|
||||
const limit = next === bottom && !card.stat ? Infinity : Math.max(ROW_DIVISIONS * 2, card.h);
|
||||
if (extra > 0 && extra <= limit) { mark(end, card.c, card.w, extra); card.h += extra; }
|
||||
}
|
||||
@@ -281,7 +235,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
const visual = grid.getBoundingClientRect().width;
|
||||
return css > 0 && visual > 0 ? visual / css : 1;
|
||||
}
|
||||
// Geometry in the grid's own CSS pixels (content box), independent of zoom.
|
||||
function gridGeometry(step, zoom) {
|
||||
const style = getComputedStyle(grid);
|
||||
const tracks = style.gridTemplateColumns.split(" ").map(parseFloat).filter(Number.isFinite);
|
||||
@@ -307,9 +260,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
const row = Math.max(0, Math.round(top / geometry.step));
|
||||
return { node, first, last, row, end: row + rowsOf(node) };
|
||||
}
|
||||
// Stat cards that start on the same grid row get the height of the tallest of
|
||||
// them, like a classic row of tiles. Raising a row can move later cards, so the
|
||||
// pass repeats until nothing changes. Large cards never stretch the tiles.
|
||||
function equalizeStatRows(stats, geometry) {
|
||||
for (let pass = 0; pass < 6; pass++) {
|
||||
const groups = new Map();
|
||||
@@ -326,12 +276,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
if (!changed) return;
|
||||
}
|
||||
}
|
||||
// A card is stretched down over the empty cells beneath it, up to the next card
|
||||
// below (or the grid's last row), so rows end on straight edges instead of
|
||||
// jagged ones. Only empty cells are taken, which never changes where any other
|
||||
// card is placed. The stored / edited height (state.span) is unaffected.
|
||||
// Only small steps are closed (up to two stat tiles or half the card's own
|
||||
// height); a large hole is left visible rather than hidden by a stretched card.
|
||||
function alignBottom(items, geometry) {
|
||||
const cards = items.map(node => place(node, geometry));
|
||||
const bottom = Math.max(...cards.map(card => card.end));
|
||||
@@ -358,9 +302,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
heights = value && typeof value === "object" ? { ...value } : null;
|
||||
schedule(true);
|
||||
}
|
||||
// The grid is shown (see is-layout-pending) once the saved layout has been
|
||||
// applied (reveal) and the first live data has been rendered (dataReady), and
|
||||
// the layout is computed from scratch at that moment, so nothing moves after.
|
||||
let layoutReady = false, contentReady = false;
|
||||
function show() {
|
||||
if (!layoutReady || !contentReady || !grid.classList.contains("is-layout-pending")) return;
|
||||
@@ -369,7 +310,6 @@ window.rustProxyOverviewRows = (() => {
|
||||
}
|
||||
function reveal() { layoutReady = true; show(); }
|
||||
function dataReady() { contentReady = true; show(); }
|
||||
// Never leave the dashboard hidden if preferences cannot be loaded.
|
||||
setTimeout(() => grid.classList.contains("is-layout-pending") && grid.classList.remove("is-layout-pending"), 4000);
|
||||
function setAutoFit(value) {
|
||||
const next = value !== false;
|
||||
|
||||
@@ -6,7 +6,7 @@ window.rustProxyOverviewSamples = (() => {
|
||||
try {
|
||||
const stored = Number(localStorage.getItem(storageKey));
|
||||
if (allowed.has(stored)) limit = stored;
|
||||
} catch { /* Private browsing or storage policy: selection still works for this visit. */ }
|
||||
} catch { }
|
||||
const callbacks = new Set();
|
||||
const getLimit = () => limit;
|
||||
const trim = array => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
// These are reviewable shell instructions only. The panel never executes them.
|
||||
const views = new WeakMap();
|
||||
const quote = value => `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
||||
function absolutePath(value, directory) {
|
||||
|
||||
@@ -323,8 +323,6 @@ 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); }
|
||||
// No Overview layout is kept in the browser: the grid stays invisible until the
|
||||
// account's layout from the server is applied (rustProxyOverviewRows.reveal).
|
||||
try { localStorage.removeItem("rp-dashboard-layout"); } catch { }
|
||||
|
||||
async function loadAdminPreferences(force = false) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Shared rate sampling: rebase after background-tab pauses instead of plotting a false spike.
|
||||
const rustProxyRateSampling = (() => {
|
||||
let generation = 0;
|
||||
const pause = () => { generation += 1; };
|
||||
|
||||
@@ -344,8 +344,6 @@ function svgNode(tag, attrs = {}) {
|
||||
|
||||
function setChartText(node, value) {
|
||||
const text = String(value);
|
||||
// Controls decorated by the optional SVG icons hold [svg.ui-icon, text]; keep the icon
|
||||
// and update only the text node so periodic refreshes do not rebuild the button.
|
||||
const icon = node.querySelector(":scope > svg.ui-icon");
|
||||
if (icon) {
|
||||
const last = node.lastChild;
|
||||
@@ -417,8 +415,6 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
setChartText(row.querySelector(".network-interface-hide"), t("hideMetric"));
|
||||
row.querySelector(".network-rx-line").setAttribute("points", sparklinePoints(history.rx, 300, 60, 5, peak) || "0,55 300,55");
|
||||
row.querySelector(".network-tx-line").setAttribute("points", sparklinePoints(history.tx, 300, 60, 5, peak) || "0,55 300,55");
|
||||
// The Hide button precedes the chart and may carry an injected svg.ui-icon:
|
||||
// address the chart explicitly so axes/labels never attach to the icon.
|
||||
const chart = row.querySelector("svg:not(.ui-icon)");
|
||||
window.rustProxyChartScale.axes(chart, peak, "bytes/s", history.rx.length,
|
||||
{ top: 5 / 60, bottom: 55 / 60 });
|
||||
@@ -469,8 +465,6 @@ function cacheOutcomeMix(totals) {
|
||||
const counts = [hits, misses, bypasses, requests - cacheTotal];
|
||||
return { counts, percentages: counts.map(value => value / requests * 100), requests };
|
||||
}
|
||||
// Percent labels live inside the bar segments; a label that does not fit is hidden
|
||||
// (the legend carries the request counts; tooltips give both).
|
||||
function fitCacheMixLabels() {
|
||||
const bar = $("cache-mix-bar");
|
||||
if (!bar) return;
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
// Collapsible left menu. On wide screens (above 1300 px) the account preference
|
||||
// sidebar_collapsed shows the menu as a narrow rail (logo, section numbers or icons,
|
||||
// status dot). Between 851 and 1300 px the menu is always a rail and the toggle
|
||||
// opens it temporarily as an overlay (closed by choosing a section, clicking
|
||||
// outside or Esc). At 850 px and below the existing mobile menu is used.
|
||||
window.rustProxySidebar = (() => {
|
||||
const root = document.documentElement;
|
||||
const sidebar = document.querySelector(".sidebar");
|
||||
|
||||
@@ -68,8 +68,6 @@
|
||||
};
|
||||
const reason = value => { const key = `cacheReason_${value}`, text = t(key); return text === key ? value : text; };
|
||||
|
||||
// Decision counters: label + count + share bar; top three visible, the rest folded into a details toggle.
|
||||
// Folded counters keep their open/closed state across auto-refresh re-renders.
|
||||
const openReasonRows = new Set();
|
||||
function reasonsCell(reasons, rowKey = "") {
|
||||
const td = el("td", undefined, "cache-reasons"), entries = Object.entries(reasons || {}).sort((a, b) => b[1] - a[1]);
|
||||
@@ -179,7 +177,6 @@
|
||||
$("cp-rejected").textContent = `${t("cacheRejected")}: ${number(m.rejected_fills)} · ${t("cacheMemoryPressureShort")}: ${number(m.memory_pressure_rejections)} · ${t("cacheNukeLimitShort")}: ${number(m.hot_eviction_limited)}`;
|
||||
$("cache-domain-rows").replaceChildren(...shownGroups.map(row => {
|
||||
const tr = el("tr"); for (const value of [row.vhost || t("cacheLegacy"), row.domain, number(row.hot_entries), number(row.cold_entries), fmtBytes(row.resident_bytes), fmtBytes(row.cold_bytes)]) tr.append(el("td", value));
|
||||
// HIT / MISS / BYPASS: coloured counts that match the dashboard cache bar, plus a hit-ratio meter.
|
||||
const outcomes = el("td", undefined, "cache-outcomes-cell");
|
||||
[[row.hits, "cache-outcome-hit"], [row.misses, "cache-outcome-miss"], [row.bypasses, "cache-outcome-bypass"]].forEach(([count, cls], index) => { if (index) outcomes.append(el("span", "/", "cache-outcome-sep")); outcomes.append(el("span", number(count), cls)); });
|
||||
const ratioPercent = Math.max(0, Math.min(100, (row.hit_ratio || 0) * 100)), ratio = el("td", undefined, "cache-ratio-cell"), meter = el("span", undefined, "cache-ratio-meter"), fill = el("i"); fill.style.width = `${ratioPercent}%`; meter.append(fill); ratio.append(el("span", `${fmt(ratioPercent, 1)}%`, "cache-ratio-value"), meter);
|
||||
@@ -194,7 +191,6 @@
|
||||
tr.append(el("td", row.vhost || t("cacheLegacy")), el("td", row.domain));
|
||||
const targetValue = row.target || row.path || "-", target = makeCopyCell(targetValue);
|
||||
if (row.transformed) {
|
||||
// Keep the badge outside the ellipsised text so a long URL never clips it.
|
||||
const wrap = el("div", undefined, "cache-target-wrap");
|
||||
wrap.append(el("span", targetValue, "cache-target-text"), el("span", t("transformBadge"), "cache-transform-badge"));
|
||||
target.replaceChildren(wrap);
|
||||
|
||||
@@ -148,7 +148,6 @@ curl --fail --show-error --request POST \
|
||||
try {
|
||||
const data = await api(`${path}/regenerate`, { method: "POST" });
|
||||
setCertificateApiCredential(data.credential);
|
||||
// Never put the bearer token into the persistent credential list/state.
|
||||
showCertificateApiSecret(data.token || "");
|
||||
renderCertificateApi();
|
||||
toast(t("certificateApiRegenerated"));
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Theme palettes and appearance tokens */
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Shared reset, typography, controls, forms, tables, cards and overlay primitives */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -1226,8 +1225,6 @@ html[data-dashboard-density=compact] #panel-overview .dashboard-overview-grid>.c
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Small controls: a smaller icon must not change the button height when the
|
||||
optional SVG icons are toggled or re-applied during a refresh (Safari jumped). */
|
||||
.network-interface-hide>.ui-icon,
|
||||
.network-interface-hidden .quiet>.ui-icon {
|
||||
width: 11px;
|
||||
@@ -1595,8 +1592,6 @@ html[data-table-density=ultra] :is(table th, table td) {
|
||||
.path-permissions-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
/* Safety net: whatever chart/card rule is added later, injected UI icons stay inline,
|
||||
fixed-size and out of layout flow (they used to stretch inside chart cards on mobile Safari). */
|
||||
html[data-interface-icons="true"] :is(button, a, summary)>svg.ui-icon {
|
||||
display: inline-block;
|
||||
position: static;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Application startup, authentication, navigation, page chrome and draft dock */
|
||||
|
||||
html.app-loading .shell {
|
||||
visibility: hidden;
|
||||
@@ -839,7 +838,6 @@ main {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
/* Optional subtle logo animation (Settings → Appearance → Animate logo). */
|
||||
.brand-mark .brand-letter {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
@@ -996,9 +994,6 @@ a.brand>.brand-mark[data-logo-effect="wobble"]>span {
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapsible left menu (sidebar.js). Above 850 px the menu can be a narrow rail:
|
||||
html.sidebar-collapsed (account preference on wide screens, always between 851
|
||||
and 1300 px) and html.sidebar-peek (rail temporarily opened as an overlay). */
|
||||
.sidebar-collapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Overview metrics, charts, dashboard density, inline layout controls and presets */
|
||||
|
||||
html[data-theme=ultra_dark] :is(.card, .stat, dialog) {
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
|
||||
@@ -326,7 +325,6 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
|
||||
.pool-table td:nth-child(n
|
||||
|
||||
/**/
|
||||
+4) {
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
@@ -335,7 +333,6 @@ html[data-theme=cream] :is(.card, .stat, dialog) {
|
||||
[dir=rtl] table,
|
||||
.pool-table th:nth-child(n
|
||||
|
||||
/**/
|
||||
+4),
|
||||
.logging-builder-action .small-label {
|
||||
text-align: right;
|
||||
@@ -649,9 +646,6 @@ html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-m
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Sparkline tiles with a one-line detail (e.g. Backend retry attempts: recent rate,
|
||||
successful retries): the detail sits on the right opposite the value, like a series
|
||||
legend, and the chart spans the full width underneath. */
|
||||
#panel-overview .stats-grid>.stat.stat-chart:has(>small:not([hidden], .stat-chart-legend, .backend-health-origins)):not(:has(>.stat-chart-legend)) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -708,9 +702,6 @@ html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-m
|
||||
min-height: 26px;
|
||||
}
|
||||
|
||||
/* Sparkline tiles with a series legend (HTTP / WebSocket protocol mix, backend
|
||||
traffic): the legend sits on the right, opposite the main value, one series per
|
||||
line, and the chart spans the full width below. */
|
||||
#panel-overview .stats-grid>.stat.stat-chart:has(>.stat-chart-legend) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -749,7 +740,6 @@ html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-m
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
/* Row-unit layout: the chart takes the remaining height of a taller tile. */
|
||||
#panel-overview #dashboard-overview-grid.has-row-units>.stat.stat-chart:has(>.stat-chart-legend)>:is(svg:not(.ui-icon), .rp-chart-axis-host) {
|
||||
align-self: stretch;
|
||||
}
|
||||
@@ -1366,15 +1356,10 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Legacy grouped layout: reserve room for the editing toolbar. With row units the
|
||||
toolbar overlays the card header instead, so the unlocked grid has exactly the
|
||||
geometry of the saved one (what you arrange is what you get). */
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked:not(.has-row-units)>[data-dashboard-widget] {
|
||||
padding-top: 40px !important;
|
||||
}
|
||||
|
||||
/* Row-unit editing toolbar: a slim opaque header (handle, card name, hide) that
|
||||
expands to a second line with width / height / move controls on hover or focus. */
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-toolbar {
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
@@ -1442,7 +1427,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* "Fit to screen" switch in the unlocked layout toolbar. */
|
||||
.overview-inline-autofit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1461,15 +1445,12 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Width shown wider on this screen by Fit to screen. */
|
||||
#panel-overview .overview-inline-width-value.is-fitted {
|
||||
color: var(--accent);
|
||||
text-decoration: underline dotted;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Height value: readable between its − / + buttons. Automatic height (fits the
|
||||
content) is slightly dimmed, a fixed height is shown in full colour. */
|
||||
#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-width-value.overview-inline-height-value {
|
||||
flex: 0 0 auto;
|
||||
min-width: 34px;
|
||||
@@ -1486,9 +1467,6 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
/* Stat tiles taller than their content (a taller neighbour in the same row): keep
|
||||
the label on top, centre the value in the free space, keep meters and legends at
|
||||
the bottom and let sparklines grow, instead of leaving an empty band below. */
|
||||
#panel-overview #dashboard-overview-grid.has-row-units>.stat:not([hidden], .stat-donut, .stat-chart) {
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
@@ -2256,9 +2234,6 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
grid-column-start: auto;
|
||||
}
|
||||
|
||||
/* Row units (custom layout): heights snap to the compact stat card height so tall
|
||||
panels sit beside several rows of small cards with aligned edges.
|
||||
overview-rows.js sets --overview-row-unit and each card's --overview-rows. */
|
||||
#panel-overview .dashboard-overview-grid.is-aligned.has-row-units {
|
||||
grid-auto-rows: var(--overview-row-unit, 84px);
|
||||
}
|
||||
@@ -2272,26 +2247,17 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* Auto-fit: overview-rows.js packs the cards and places them explicitly. While a
|
||||
block is being dragged the normal flow is used, so the drop placeholder works. */
|
||||
#panel-overview #dashboard-overview-grid.has-row-units.has-auto-placement:not(.is-overview-sorting)>[data-dashboard-widget]:not([hidden]) {
|
||||
grid-column: var(--overview-col, auto)/span var(--overview-cols, 1);
|
||||
grid-row: var(--overview-row, auto)/span var(--overview-rows, 1);
|
||||
}
|
||||
|
||||
/* Until the account's layout is applied the cards stay invisible, so the page
|
||||
never shows the factory arrangement first and then jumps (no browser storage).
|
||||
Meanwhile the panel's startup screen is shown in their place (see below). */
|
||||
#panel-overview .dashboard-overview-grid.is-layout-pending {
|
||||
position: relative;
|
||||
min-height: 280px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* The panel's startup screen (logo, spinner, "Loading RustProxy…") is shown in
|
||||
place of the cards while the layout is pending: a zero-height block right
|
||||
before the grid whose content overlays the still invisible grid, so the cards'
|
||||
order and first-child styles are unaffected. */
|
||||
#panel-overview .overview-layout-loading {
|
||||
height: 0;
|
||||
margin-inline: auto;
|
||||
@@ -2311,8 +2277,6 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
align-self: start !important;
|
||||
}
|
||||
|
||||
/* A card taller than its content gives the extra height to its charts instead of
|
||||
leaving an empty band at the bottom. */
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget]:not([hidden]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -2333,7 +2297,6 @@ html:is([data-corner-style=square], [data-corner-style=subtle], [data-corner-sty
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
/* With chart scales enabled the SVG sits in an axis host: grow the host and its SVG. */
|
||||
#panel-overview .dashboard-overview-grid.has-row-units>.card[data-dashboard-widget] :is(.rp-chart-axis-host, .mini-live-chart>.rp-chart-axis-host) {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
@@ -2672,7 +2635,6 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* WebSocket tunnels: "Established" sits to the right of the main value instead of a third text line. */
|
||||
#panel-overview .stat .stat-value-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
@@ -2703,8 +2665,6 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
|
||||
font-weight: 450;
|
||||
}
|
||||
|
||||
/* Compact stat cards: a card made only of title, value and one secondary line shows that line
|
||||
beside the value (right-aligned) instead of on a line of its own. */
|
||||
#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small, .overview-inline-toolbar))):has(>small)) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
@@ -2730,7 +2690,6 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Backends overview card (0.36.7): follows table density (global table rules), corner style tokens and interface colours */
|
||||
#panel-overview .backend-overview-card.is-no-proxy,
|
||||
#panel-overview #backend-diagnostics-card.is-no-proxy { display: none !important; }
|
||||
.bo-heading { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px; min-width: 0; }
|
||||
@@ -2811,7 +2770,6 @@ html[data-table-density=compact] .bo-spark-bars { width: 70px; }
|
||||
html[data-table-density=compact] .bo-chip { padding: 5px 9px; }
|
||||
html[data-table-density=compact] .bo-meter { margin-top: 3px; }
|
||||
|
||||
/* Process overview card (process_diagnostics) */
|
||||
[data-dashboard-widget="process_diagnostics"] { display: flex; flex-direction: column; min-height: 560px; }
|
||||
.pr-chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 0; }
|
||||
.pr-chip { display: inline-flex; align-items: baseline; gap: 7px; margin: 0; padding: 3px 9px; border: 1px solid var(--line); border-radius: var(--rp-radius-control, 0); font: 11px var(--mono); }
|
||||
@@ -2842,7 +2800,6 @@ html[data-table-density=ultra] .pr-chip { padding: 1px 7px; }
|
||||
.pr-cols, .pr-charts { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Memory breakdown card (memory_breakdown) */
|
||||
.mb-card { display: flex; flex-direction: column; }
|
||||
.mb-card .mb-chips { margin-bottom: 12px; }
|
||||
.mb-bar { display: flex; height: 22px; overflow: hidden; background: var(--line); border-radius: var(--rp-progress-radius, 0); gap: 1px; }
|
||||
@@ -2874,10 +2831,8 @@ html[data-table-density=compact] .mb-item { padding-top: 5px; padding-bottom: 5p
|
||||
html[data-table-density=ultra] .mb-item { padding-top: 3px; padding-bottom: 3px; }
|
||||
html[data-table-density=ultra] .mb-bar { height: 16px; }
|
||||
|
||||
/* initial width of the cache outcome segments (set from JS via CSSOM; inline style attributes are blocked by the CSP) */
|
||||
.cache-mix-hit, .cache-mix-miss, .cache-mix-bypass, .cache-mix-other { width: 0; }
|
||||
|
||||
/* Backends overview: trend charts in the requests, latency and traffic cells */
|
||||
.bo-chartcell { display: flex; align-items: center; justify-content: flex-end; gap: 10px; font: 12px var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.bo-requests .bo-chartcell>span { min-width: 44px; text-align: right; }
|
||||
.bo-spark-bars rect { fill: var(--accent); opacity: .75; }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Shared service editors: Vhosts, backends, Web Server, Streams, routing and ACL pickers */
|
||||
|
||||
.host-chip-editor.field-invalid,
|
||||
.listener-choices.field-invalid {
|
||||
@@ -1483,8 +1482,6 @@ html[data-dashboard-density=ultra] #panel-overview .dashboard-overview-grid>.car
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
/* Same geometry and typography as .pill so protocol indicators sit evenly in the
|
||||
Vhost policy row; only the colour (muted = not observed, green = observed) differs. */
|
||||
.vhost-backend-protocol-badge {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
@@ -1948,8 +1945,6 @@ html.app-loading,
|
||||
margin: 0 !important;
|
||||
min-width: 0;
|
||||
}
|
||||
/* Spacing review (0.36.2): descriptive text and blocks that used to touch the next
|
||||
element. Rules only add space where the layout had none; grid/flex gaps are untouched. */
|
||||
.fine+:is(.switch-grid, .settings-tabs, .table-scroll, .soft-card, .appearance-settings-grid, .runtime-information-grid, details),
|
||||
.form-actions+.fine,
|
||||
.table-scroll+:is(.empty, .fine),
|
||||
@@ -2021,7 +2016,6 @@ h2+.fine {
|
||||
}
|
||||
}
|
||||
|
||||
/* Redirect cards (0.36.5): host chips → target flow plus a quiet meta line, no nested boxes */
|
||||
@media (min-width: 761px) { .redirect-card .vhost-card-head { align-items: center; } }
|
||||
.redirect-flow { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.4fr); gap: 12px; align-items: center; margin-top: 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--rp-radius-surface, 0); background: var(--bg); }
|
||||
.redirect-flow-from, .redirect-flow-to { display: flex; flex-wrap: wrap; gap: 6px; min-width: 0; align-items: center; }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Settings, certificates, integrations and Docker agent/service management */
|
||||
|
||||
.settings-tabs button.has-unsaved:after {
|
||||
content: "";
|
||||
@@ -119,8 +118,6 @@
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.renewal-entry {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr auto;
|
||||
@@ -1173,7 +1170,6 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Global security policy forms */
|
||||
.global-policy-form .global-policy-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -1260,7 +1256,6 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
/* Settings rows (0.36.3): label and help on the left, control on the right, one setting per row. */
|
||||
#settings-page-appearance .appearance-section-heading .small-label {
|
||||
display: none;
|
||||
}
|
||||
@@ -1335,8 +1330,6 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Checkbox settings: text on the left, the checkbox on the right edge of the row
|
||||
(.check is display:flex !important, so the row is reversed instead of using grid areas). */
|
||||
#settings-page-appearance .appearance-settings-grid>label.check {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
@@ -1398,9 +1391,7 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings navigation (0.36.3) */
|
||||
@media (min-width: 761px) {
|
||||
/* Sub-tabs are a horizontal tab bar; the vertical rail layout is for the main settings menu only. */
|
||||
#panel-settings .settings-tabs.sub-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -1419,7 +1410,6 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Visual groups in the main menu: appearance / traffic / rules / observability / system / security. */
|
||||
#panel-settings #settings-tabs button:is([data-settings-tab=listeners], [data-settings-tab=policies], [data-settings-tab=logging], [data-settings-tab=administration], [data-settings-tab=certificate-api]) {
|
||||
margin-top: 7px;
|
||||
position: relative;
|
||||
@@ -1435,7 +1425,6 @@ html[data-dashboard-density=normal] #panel-overview .stat>span,
|
||||
}
|
||||
}
|
||||
|
||||
/* Cache and static transformation pages (0.36.3): grouped sections, help attached to its switch. */
|
||||
#cache-form .cache-section-title {
|
||||
margin-top: 20px;
|
||||
padding-top: 14px;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Traffic, cache, transformations, live/stored logs and configuration history */
|
||||
|
||||
.certificate-card .pill,
|
||||
.cache-miss-key,
|
||||
@@ -1073,7 +1072,6 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
|
||||
font: 11px/1.5 var(--mono);
|
||||
}
|
||||
|
||||
/* Cache → Domains and effectiveness (0.36.3): numeric columns aligned, outcomes coloured like the dashboard bar. */
|
||||
#cache-domain-scroll .cache-table :is(th, td):nth-child(n+3):nth-child(-n+8) {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
@@ -1148,7 +1146,6 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Cache → decision counters, stored objects and pager (0.36.4) */
|
||||
#panel-cache .cache-table .cache-reasons { min-width: 200px; max-width: 320px; }
|
||||
.cache-reason-line { display: inline-flex; align-items: baseline; gap: 4px; margin: 0 10px 0 0; font-size: 11px; line-height: 1.5; white-space: nowrap; }
|
||||
.cache-reason-name { color: var(--muted); }
|
||||
@@ -1172,7 +1169,6 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
|
||||
.cache-pager #cache-page { padding: 0 14px; min-width: 120px; text-align: center; font-variant-numeric: tabular-nums; border-inline: 1px solid var(--line); align-self: stretch; display: flex; align-items: center; justify-content: center; }
|
||||
.cache-pager #cache-next { flex-direction: row-reverse; }
|
||||
|
||||
/* Static transformation tables (0.36.4): same presentation as Cache → Domains and effectiveness */
|
||||
.transform-vhost-table :is(th, td):nth-child(n+2), .transform-type-table :is(th, td):nth-child(2) { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.transform-vhost-table td, .transform-type-table td { vertical-align: middle; }
|
||||
.transform-vhost-table td:first-child { color: var(--text); font-weight: 600; }
|
||||
@@ -1185,7 +1181,6 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
|
||||
|
||||
.cache-ratio-meter, .cache-ratio-meter>i { border-radius: var(--rp-progress-radius, 0); }
|
||||
|
||||
/* Decision counters (0.36.7): name + count on one line, a thin share bar below (relative to the largest counter) */
|
||||
.cache-reasons .cache-reason-line { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; align-items: baseline; margin: 0 0 6px; white-space: nowrap; }
|
||||
.cache-reasons .cache-reason-name { overflow: hidden; text-overflow: ellipsis; }
|
||||
.cache-reasons .cache-reason-track { grid-column: 1 / -1; display: block; height: 2px; margin-top: 3px; background: var(--line); border-radius: var(--rp-progress-radius, 0); overflow: hidden; }
|
||||
@@ -1197,26 +1192,21 @@ html[data-table-density=ultra] .file-log-inline-detail pre {
|
||||
.cache-reasons .cache-reason-more[open]>summary::before { transform: rotate(90deg); }
|
||||
.cache-reasons .cache-reason-more>summary:hover { color: var(--text); border-color: var(--muted); }
|
||||
|
||||
/* Transformed badge (0.36.7): the URL ellipsises, the badge always stays visible */
|
||||
.cache-target-wrap { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
||||
.cache-target-text { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.cache-target-wrap .cache-transform-badge { flex: 0 0 auto; margin-left: 0; }
|
||||
|
||||
|
||||
/* Cache pager (0.36.7): never wraps inside its frame; the heading wraps instead */
|
||||
#panel-cache .card-heading:has(.cache-pager) { flex-wrap: wrap; row-gap: 8px; }
|
||||
.cache-pager { flex-wrap: nowrap; flex: 0 0 auto; max-width: 100%; margin-left: auto; }
|
||||
.cache-pager button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap; flex: 0 0 auto; }
|
||||
.cache-pager #cache-page { flex: 1 1 auto; white-space: nowrap; }
|
||||
|
||||
/* Cache pager follows the interface rounding setting: frame and the two outer buttons share the control radius */
|
||||
.cache-pager { overflow: visible; border-radius: var(--rp-radius-control, 0px); }
|
||||
.cache-pager button { border-radius: 0; }
|
||||
.cache-pager>button:first-child { border-radius: max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)); }
|
||||
.cache-pager>button:last-child { border-radius: 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)) max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0; }
|
||||
.cache-pager button:focus-visible { outline-offset: -2px; }
|
||||
|
||||
/* The global corner-style rule forces the full control radius on every button with !important; the pager is a segmented control, so only its outer corners are rounded */
|
||||
#panel-cache .cache-pager>button { border-radius: 0 !important; }
|
||||
#panel-cache .cache-pager>button:first-child { border-radius: max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)) !important; }
|
||||
#panel-cache .cache-pager>button:last-child { border-radius: 0 max(0px, calc(var(--rp-radius-control, 0px) - 1px)) max(0px, calc(var(--rp-radius-control, 0px) - 1px)) 0 !important; }
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Responsive rules */
|
||||
|
||||
@media (min-width:1201px) {
|
||||
#panel-overview .stats-grid {
|
||||
@@ -1597,8 +1596,6 @@
|
||||
--overview-widget-placement: auto/span 3;
|
||||
}
|
||||
|
||||
/* A third of the wide grid becomes half of the 6-column grid, so two panels or
|
||||
charts sit side by side instead of leaving a two-column hole. */
|
||||
#panel-overview .dashboard-overview-grid.is-aligned>[data-dashboard-widget][data-dashboard-span="4"] {
|
||||
--overview-widget-placement: auto/span 3;
|
||||
}
|
||||
@@ -1703,7 +1700,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Docker vhosts (0.36.3): compact service rows - switch | name and details | health and actions. */
|
||||
@media (min-width:900px) {
|
||||
.docker-service-head {
|
||||
grid-template-columns: auto minmax(220px, 1fr) auto;
|
||||
@@ -1739,7 +1735,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Editors lift (0.36.4): Docker per-app forms and shared vhost/web/stream/redirect/ACL form chrome */
|
||||
.docker-service-editor:not(.docker-service-metrics)>summary,
|
||||
.docker-health-editor>summary, .docker-acl-editor>summary, .docker-protocol-editor>summary, .docker-timeout-editor>summary, .docker-passive-health-editor>summary {
|
||||
list-style: none;
|
||||
@@ -1787,7 +1782,6 @@
|
||||
.auto-id-row[hidden], .auto-id-change[hidden], .auto-id-badge[hidden] { display: none; }
|
||||
.auto-id-field input { font-family: var(--mono, monospace); font-size: 12px; }
|
||||
|
||||
/* Docker vhosts (post-0.36.6): section headers aligned left, hint follows the title */
|
||||
.docker-service-editor>summary, .docker-health-editor>summary, .docker-acl-editor>summary,
|
||||
.docker-protocol-editor>summary, .docker-timeout-editor>summary, .docker-passive-health-editor>summary {
|
||||
justify-content: flex-start; text-align: left;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* Documentation Reader layout. */
|
||||
* {
|
||||
box-sizing: border-box
|
||||
}
|
||||
@@ -376,7 +375,6 @@ main {
|
||||
padding: 18px
|
||||
}
|
||||
}
|
||||
/* Corner style follows the panel preference. */
|
||||
html[data-corner-style=square] {
|
||||
--rp-docs-radius-surface: 0px;
|
||||
--rp-docs-radius-control: 0px;
|
||||
|
||||
@@ -13,13 +13,12 @@
|
||||
let visibleColumns = new Set(defaultColumnIds);
|
||||
let sortBy = "time", sortDescending = true, pendingRefresh = false;
|
||||
|
||||
|
||||
const CONSOLIDATION_STORAGE_KEY = "rp-file-logs-consolidate-vhosts";
|
||||
const consolidationToggle = $("file-log-consolidate-vhosts");
|
||||
try {
|
||||
const saved = localStorage.getItem(CONSOLIDATION_STORAGE_KEY);
|
||||
if (consolidationToggle && (saved === "1" || saved === "0")) consolidationToggle.checked = saved === "1";
|
||||
} catch { /* html default */ }
|
||||
} catch { }
|
||||
|
||||
const sourceSelect = () => $("file-log-source");
|
||||
function setView(view) {
|
||||
|
||||
@@ -215,7 +215,6 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
const connection = el("div", undefined, "vh-wide vh-backend-switches vh-backend-connection"); main.append(connection);
|
||||
controls["member-private"] = field(connection, "member-private", "vhPrivate", value.allow_private, "checkbox");
|
||||
const preserve = field(connection, "member-host", "vhPreserveHost", value.preserve_host === true, "checkbox");
|
||||
// Preserve Host also applies to HTTP/2 :authority.
|
||||
|
||||
const advanced = el("details", undefined, "vh-wide vh-backend-advanced"), extra = el("div", undefined, "vh-grid"); advanced.append(text("summary", "advanced"), extra);
|
||||
field(extra, "member-allowlist", "vhAddressAllowlist", (value.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
|
||||
@@ -229,7 +228,6 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
|
||||
field(extra, "member-max-concurrent", "vhMaxConcurrentRequests", value.max_concurrent_requests ?? 0, "number", { required: true, min: 0, max: 1000000 });
|
||||
field(extra, "member-queue-timeout", "vhBackendQueueTimeout", value.queue_timeout_ms ?? 0, "number", { required: true, min: 0, max: 60000 });
|
||||
note(extra, "vhBackendCapacityHelp");
|
||||
// One persistent form per backend keeps unsaved edits when switching.
|
||||
const healthPanel = el("section", undefined, "vh-grid vh-backend-health-panel");
|
||||
const customHealth = field(healthPanel, "member-health-custom", "backendHealthOverride", value.health_check != null, "checkbox");
|
||||
const health = value.health_check || {};
|
||||
@@ -305,7 +303,6 @@ function debugOriginIssues(members) {
|
||||
let parsed; try { parsed = new URL(member.url); } catch { return false; }
|
||||
const host = parsed.hostname.replace(/^\[|\]$/g, ""), ipHost = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
|
||||
if (parsed.protocol !== "https:" || !ipHost) return false;
|
||||
// Warn only when TLS verification of an IP host needs an explicit server name.
|
||||
return member.tls_verify === true && !String(member.tls_server_name || "").trim();
|
||||
});
|
||||
}
|
||||
@@ -496,11 +493,9 @@ function locationsEditor(parent, values) {
|
||||
row.addEventListener("input", () => { syncUi(); sync(); }); row.addEventListener("change", () => { syncUi(); sync(); });
|
||||
rows.append(row); syncUi();
|
||||
}
|
||||
// The main backend is an internal fallback, never an editable location.
|
||||
for (const value of values) add(value);
|
||||
const bottom = el("div", undefined, "vh-wide location-toolbar location-toolbar-bottom"); bottom.append(button("locationAdd", () => { add(); sync(); }, "primary")); parent.append(bottom);
|
||||
form.querySelector(".vh-members").addEventListener("input", () => { for (const select of rows.querySelectorAll('[name="location-backend"]')) memberOptions(select); });
|
||||
// Advanced JSON remains an explicit escape hatch; edits rebuild visual rows.
|
||||
read("paths").addEventListener("change", () => { try { const data = JSON.parse(str("paths")); if (!Array.isArray(data) || data.length > 64) return; reloading = true; rows.replaceChildren(); for (const value of data) add(value); } catch { } finally { reloading = false; } });
|
||||
sync(); return readRows;
|
||||
}
|
||||
@@ -275,7 +275,6 @@ function load(value = defaults(), migrationIds = [], show = true) {
|
||||
field(g, "cookie-ttl", "vhCookieTtl", sticky.ttl_seconds ?? 3600, "number", { required: true, min: 60, max: 604800 }); note(g, "vhStickyNote");
|
||||
const rate = value.rate_limit || {}, access = rate.country_access || {}, methodPolicy = value.method_policy || {};
|
||||
g = group("vhTrafficPolicy");
|
||||
// Policies share their original form/data model; the boundaries are presentational only.
|
||||
const aclFields = subsection(g, "vhAclProfile");
|
||||
const selectedProfiles = new Set([value.acl_profile_id, ...(value.acl_profile_ids || [])].filter(Boolean));
|
||||
const profileBox = el("div", undefined, "vh-wide vh-acl-profile-picker");
|
||||
|
||||
@@ -92,7 +92,7 @@ function refreshVhostBackendPopup(status = latestStatus) {
|
||||
scheme = url.protocol.replace(":", "").toUpperCase();
|
||||
const port = url.port || (scheme === "HTTPS" ? "443" : scheme === "HTTP" ? "80" : "");
|
||||
address = `${url.hostname}${port ? `:${port}` : ""}`;
|
||||
} catch { /* Configuration may have changed while status was refreshing. */ }
|
||||
} catch { }
|
||||
const upstream = upstreamProtocolBadgeText(backend, member);
|
||||
const protocol = `${scheme} · ${upstream.mode}${upstream.observed ? ` → ${upstream.observed}` : ""}`;
|
||||
const statusCode = Number(member?.last_status);
|
||||
@@ -256,8 +256,6 @@ function initPoolTableTools() {
|
||||
}));
|
||||
syncPoolColumns(); syncPoolSortDirection();
|
||||
}
|
||||
// Backends overview (dashboard card "backend_table"): a richer view of the same backend data. It shares the sort,
|
||||
// direction and column preferences with the compact "Tiny backends" table and follows table density and corner style.
|
||||
const BO_SORT_LABELS = { vhost: "vhVhost", backend: "backend", status: "status", active: "vhActive", requests: "vhRequests", errors: "vhErrors", latency: "vhLatency", traffic: "vhTraffic", generation: "vhClientGeneration", generationRequests: "vhGenerationRequests", generationAge: "vhGenerationAge", rotations: "vhClientRotations", lastFailure: "vhLastFailure", lastRecovery: "vhLastRecovery" };
|
||||
const BO_DETAIL_COLUMNS = ["generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
|
||||
const BO_SAMPLES = 30;
|
||||
@@ -282,7 +280,7 @@ const BO_SVG = "http://www.w3.org/2000/svg";
|
||||
function boSvg(width, kind) {
|
||||
const svg = document.createElementNS(BO_SVG, "svg");
|
||||
svg.setAttribute("viewBox", `0 0 ${width} 20`); svg.setAttribute("class", `bo-spark bo-spark-${kind}`); svg.setAttribute("aria-hidden", "true");
|
||||
boBaseline(svg, width, kind === "latency" ? 10 : 19); // idle or not-yet-sampled rows still show where the chart is
|
||||
boBaseline(svg, width, kind === "latency" ? 10 : 19);
|
||||
return svg;
|
||||
}
|
||||
function boBaseline(svg, width, y) {
|
||||
@@ -426,7 +424,6 @@ function applyBackendOverviewView() {
|
||||
const scroller = body.closest(".table-scroll"), scrollTop = scroller?.scrollTop ?? 0, scrollLeft = scroller?.scrollLeft ?? 0;
|
||||
body.replaceChildren(...rows);
|
||||
if (scroller) { scroller.scrollTop = scrollTop; scroller.scrollLeft = scrollLeft; }
|
||||
// restore keyboard focus without scrolling: the table is rebuilt on every refresh and a plain focus() jumped back to the focused row
|
||||
if (hadFocus) [...body.querySelectorAll(".bo-expand")].find(button => button.dataset.boKey === focusKey)?.focus({ preventScroll: true });
|
||||
const empty = $("bo-empty");
|
||||
if (empty) { empty.textContent = boItems.length ? t("backendSearchEmpty") : t("vhHealthEmpty"); empty.hidden = list.length > 0 || !boHasProxy; }
|
||||
@@ -600,8 +597,6 @@ function render() {
|
||||
policy.append(el("small", `WebSocket · ${transports.join("+")}`, "pill"));
|
||||
if (v.websocket_legacy_support) policy.append(el("small", t("vhWebsocketLegacyBadge"), "pill"));
|
||||
}
|
||||
// Keep the upstream protocol indicators with the other Vhost policy pills.
|
||||
// Retain their own badge class so observed H1/H2 keeps its green styling.
|
||||
if (hasProxySource) {
|
||||
for (const backend of v.backends || []) {
|
||||
const badge = el("button", undefined, "vhost-backend-protocol-badge");
|
||||
@@ -632,7 +627,6 @@ function backendTrafficRate(poolId, member, snapshot, now, seen) {
|
||||
let inRate = sampled && bytesIn >= previous.bytesIn ? (bytesIn - previous.bytesIn) / seconds : 0;
|
||||
let outRate = sampled && bytesOut >= previous.bytesOut ? (bytesOut - previous.bytesOut) / seconds : 0;
|
||||
const rate = reported > 0 ? reported : observed;
|
||||
// keep the in/out split consistent with the total shown elsewhere (the server-reported rate wins when present)
|
||||
if (rate > 0 && inRate + outRate === 0) { inRate = rate * (total > 0 ? bytesIn / total : 0.5); outRate = rate - inRate; }
|
||||
else if (reported > 0 && inRate + outRate > 0) { const scale = reported / (inRate + outRate); inRate *= scale; outRate *= scale; }
|
||||
backendTrafficHistory.set(key, { ...rustProxyRateSampling.stamp(now), total, bytesIn, bytesOut, snapshot, rate, inRate, outRate });
|
||||
|
||||
Vendored
-3
@@ -1558,8 +1558,6 @@ mod tests {
|
||||
let second = cache
|
||||
.ticket(1, "http", "example.test", "app", "/second", &headers)
|
||||
.unwrap();
|
||||
// Stored bodies are trimmed to their real size, so the first object must
|
||||
// itself be large for the next fill reservation to need its space.
|
||||
let large: &'static str = Box::leak("f".repeat(60 * 1024).into_boxed_str());
|
||||
fill(&cache, first.clone(), large, &s).await;
|
||||
assert!(cache.get(&first).is_some());
|
||||
@@ -1828,7 +1826,6 @@ mod tests {
|
||||
let cache = Cache::new(1);
|
||||
let s = settings();
|
||||
let t = ticket(&cache, &HeaderMap::new());
|
||||
// Header values parsed by hyper are slices of a larger read buffer.
|
||||
let read_buffer = Bytes::from(vec![b'x'; 64 * 1024]);
|
||||
let mut h = headers("max-age=60");
|
||||
h.insert(
|
||||
|
||||
Vendored
-4
@@ -119,7 +119,6 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Marks a key as uncacheable so requests skip coalescing.
|
||||
pub fn remember_uncacheable(&self, ticket: &Ticket) {
|
||||
let now = std::time::Instant::now();
|
||||
let mut memo = self
|
||||
@@ -129,9 +128,6 @@ impl Cache {
|
||||
if memo.len() >= UNCACHEABLE_MEMO_CAPACITY && !memo.contains_key(&ticket.key) {
|
||||
memo.retain(|_, until| *until > now);
|
||||
if memo.len() >= UNCACHEABLE_MEMO_CAPACITY {
|
||||
// Keep hit-for-miss working for new keys under sustained churn:
|
||||
// drop the marker closest to expiry instead of silently
|
||||
// re-enabling request collapsing for this (uncacheable) key.
|
||||
if let Some(oldest) = memo
|
||||
.iter()
|
||||
.min_by_key(|(_, until)| **until)
|
||||
|
||||
Vendored
-4
@@ -43,10 +43,6 @@ impl Cache {
|
||||
ticket.freshness_headers(),
|
||||
);
|
||||
if state.servable() {
|
||||
// Approximate LRU: an entry already among the most recently used
|
||||
// eighth of the store is not re-linked on every hit. This keeps the
|
||||
// global HOT lock short for popular objects without changing which
|
||||
// cold entries are evicted first.
|
||||
let promote = store
|
||||
.entries
|
||||
.get(&storage_key)
|
||||
|
||||
Vendored
-13
@@ -28,10 +28,6 @@ const COLD_QUEUE: usize = 16;
|
||||
const COLD_FORMAT_VERSION: u8 = 3;
|
||||
type Key = [u8; 32];
|
||||
|
||||
/// Cache keys are SHA-256 digests, so their bytes are already uniformly
|
||||
/// distributed and not attacker-steerable. Hashing them again with SipHash
|
||||
/// costs measurable time on every HOT lookup; this hasher folds the digest
|
||||
/// cheaply instead (any other input is still mixed, Fx-style).
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct DigestKeyHasher(u64);
|
||||
impl std::hash::Hasher for DigestKeyHasher {
|
||||
@@ -53,7 +49,6 @@ impl std::hash::Hasher for DigestKeyHasher {
|
||||
}
|
||||
}
|
||||
fn write_usize(&mut self, _length: usize) {
|
||||
// The only usize written for `[u8; 32]` is its constant length prefix.
|
||||
}
|
||||
}
|
||||
type KeyMap<V> = HashMap<Key, V, std::hash::BuildHasherDefault<DigestKeyHasher>>;
|
||||
@@ -71,11 +66,6 @@ struct CleanupCandidate {
|
||||
key: Key,
|
||||
}
|
||||
|
||||
/// Minimum spacing between two `malloc_trim` calls. Trimming walks every glibc
|
||||
/// arena under its lock and can take milliseconds with a large heap; doing it
|
||||
/// after each eviction made a full HOT cache with constant turnover several
|
||||
/// times slower. Skipped requests are remembered and flushed by the cache
|
||||
/// maintenance tick, so freed memory still returns to the OS within seconds.
|
||||
const ALLOCATOR_RELEASE_INTERVAL_MS: u64 = 250;
|
||||
static ALLOCATOR_RELEASE_LAST_MS: AtomicU64 = AtomicU64::new(0);
|
||||
static ALLOCATOR_RELEASE_PENDING: std::sync::atomic::AtomicBool =
|
||||
@@ -83,7 +73,6 @@ static ALLOCATOR_RELEASE_PENDING: std::sync::atomic::AtomicBool =
|
||||
|
||||
fn allocator_clock_ms() -> u64 {
|
||||
static EPOCH: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
|
||||
// +1 so the very first request is never mistaken for "just trimmed".
|
||||
(EPOCH.get_or_init(Instant::now).elapsed().as_millis() as u64)
|
||||
.saturating_add(ALLOCATOR_RELEASE_INTERVAL_MS + 1)
|
||||
}
|
||||
@@ -103,7 +92,6 @@ fn release_allocator_memory() {
|
||||
cache_trim_allocator();
|
||||
}
|
||||
|
||||
/// Called from the periodic cache maintenance task.
|
||||
fn flush_pending_allocator_release() {
|
||||
if ALLOCATOR_RELEASE_PENDING.load(Ordering::Relaxed) {
|
||||
release_allocator_memory();
|
||||
@@ -133,7 +121,6 @@ pub enum StaleErrorClass {
|
||||
Tls,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MemoryPreset {
|
||||
|
||||
Vendored
-3
@@ -92,11 +92,9 @@ pub fn plan(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Surrogate-Control: no-store always vetoes shared caching.
|
||||
if settings.varnish_compatible && surrogate_no_store(headers) {
|
||||
return None;
|
||||
}
|
||||
// Fallback TTL overrides origin no-cache only for public responses.
|
||||
let override_public_no_cache = settings.varnish_compatible
|
||||
&& control.contains_key("no-cache")
|
||||
&& control.contains_key("public")
|
||||
@@ -158,7 +156,6 @@ pub fn plan(
|
||||
None if requires_revalidation => 0,
|
||||
None => return None,
|
||||
};
|
||||
// Expired entries are kept for revalidation only; implicit grace needs no explicit SWR/no-cache.
|
||||
if settings.varnish_compatible
|
||||
&& !control.contains_key("stale-while-revalidate")
|
||||
&& !control.contains_key("no-cache")
|
||||
|
||||
Vendored
-1
@@ -395,7 +395,6 @@ impl Cache {
|
||||
}
|
||||
|
||||
impl Ticket {
|
||||
/// Ignores forced-reload directives only; other client constraints apply.
|
||||
pub fn with_varnish_compatible(mut self, compatible: bool) -> Self {
|
||||
if let std::borrow::Cow::Owned(freshness) =
|
||||
compatible_freshness_headers(&self.request_headers, compatible)
|
||||
|
||||
@@ -108,7 +108,6 @@ pub(crate) fn compile_backend_with_timeout(
|
||||
})
|
||||
}
|
||||
|
||||
/// Backend copy used only by active health checks that probe a different port.
|
||||
pub(crate) fn health_probe_backend(backend: &Backend, port: Option<u16>) -> Result<Option<Backend>> {
|
||||
let Some(port) = port else { return Ok(None) };
|
||||
let mut url = Url::parse(&backend.url).context("invalid backend URL")?;
|
||||
|
||||
@@ -139,8 +139,6 @@ impl Snapshot {
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Specificity of the vhost frontend matching `host` (an exact name beats any wildcard,
|
||||
/// a longer wildcard suffix beats a shorter one).
|
||||
pub fn frontend_rank(&self, host: &str, local: SocketAddr, tls: bool) -> Option<usize> {
|
||||
let mut rank = None;
|
||||
Self::find_host_pattern(host, |pattern| {
|
||||
@@ -158,7 +156,6 @@ impl Snapshot {
|
||||
});
|
||||
rank
|
||||
}
|
||||
/// Specificity of the redirect host matching `host` and `path` (see `frontend_rank`).
|
||||
pub fn redirect_rank(
|
||||
&self,
|
||||
host: &str,
|
||||
@@ -186,7 +183,6 @@ impl Snapshot {
|
||||
});
|
||||
rank
|
||||
}
|
||||
/// Specificity of the 404 host matching `host` (see `frontend_rank`).
|
||||
pub fn not_found_rank(&self, host: &str, local: SocketAddr, tls: bool) -> Option<usize> {
|
||||
let mut rank = None;
|
||||
Self::find_host_pattern(host, |pattern| {
|
||||
|
||||
@@ -148,11 +148,6 @@ impl Diagnostics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Heap statistics reported by the glibc allocator (`mallinfo2`).
|
||||
///
|
||||
/// `free_bytes` is memory the allocator keeps for reuse (fragmentation and
|
||||
/// not-yet-returned pages). Part of it may already be released to the kernel
|
||||
/// by `malloc_trim`, so it is an upper bound of the resident share.
|
||||
#[derive(Debug, Clone, Copy, serde::Serialize)]
|
||||
pub struct AllocatorStats {
|
||||
pub allocator: &'static str,
|
||||
@@ -164,9 +159,6 @@ pub struct AllocatorStats {
|
||||
pub mmap_blocks: u64,
|
||||
}
|
||||
|
||||
/// Returns glibc heap statistics, sampled at most every two seconds because
|
||||
/// `mallinfo2` walks every arena under its lock. `None` on other allocators
|
||||
/// (musl, non-Linux or the optional mimalloc build).
|
||||
pub fn allocator_stats() -> Option<AllocatorStats> {
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu", not(feature = "mimalloc")))]
|
||||
{
|
||||
|
||||
@@ -252,7 +252,6 @@ pub(crate) fn auth_context(
|
||||
method: &Method,
|
||||
context: &forwarding::Context,
|
||||
) -> Result<()> {
|
||||
// Never trust inbound original metadata, including markers from another auth module.
|
||||
for name in headers
|
||||
.keys()
|
||||
.filter(|name| name.as_str().starts_with("x-original-"))
|
||||
|
||||
@@ -35,7 +35,6 @@ impl PeerFormatting {
|
||||
untrusted_headers: std::sync::Mutex::new(None),
|
||||
}
|
||||
}
|
||||
/// Text form of the peer address when it needed no canonicalization.
|
||||
pub(crate) fn raw_peer_text(&self) -> Option<&str> {
|
||||
(self.peer.ip() == self.peer_ip)
|
||||
.then(|| self.peer_header.to_str().ok())
|
||||
|
||||
@@ -97,7 +97,6 @@ mod tests {
|
||||
assert!(gateway_502.contains("RustProxy"));
|
||||
assert!(gateway_502.contains("#131615"));
|
||||
assert!(!gateway_502.contains("Tinyauth"));
|
||||
// Stock error pages must stay unchanged.
|
||||
assert_eq!(
|
||||
ERROR_CODES,
|
||||
[400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/// Certificate expiry read from the certificate files, refreshed at most every five minutes.
|
||||
fn certificate_expiry(
|
||||
app: &App,
|
||||
certificates: &[crate::tls::Certificate],
|
||||
|
||||
@@ -139,8 +139,6 @@ async fn forward_request(
|
||||
}
|
||||
let websocket_legacy_support =
|
||||
handshake.is_some() && frontend.is_some_and(|frontend| frontend.websocket_legacy_support);
|
||||
// The most specific host name wins: an exact vhost such as autodiscover.example.com must not be
|
||||
// shadowed by a wildcard redirect or 404 host (*.example.com) that happens to be evaluated first.
|
||||
let frontend_rank = frontend.and_then(|_| snapshot.frontend_rank(&host, local.0, tls));
|
||||
let redirect = snapshot
|
||||
.redirect_on(&host, &path, local.0, tls)
|
||||
@@ -205,7 +203,6 @@ async fn forward_request(
|
||||
{
|
||||
return response;
|
||||
}
|
||||
// Redirects are evaluated after global and per-vhost country/IP/rate checks below.
|
||||
}
|
||||
let mut context = match snapshot.forwarding.context_with_peer(
|
||||
request.headers(),
|
||||
@@ -264,7 +261,6 @@ async fn forward_request(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Global rules are cumulative: an explicit IP allow cannot bypass a country deny/allowlist.
|
||||
if !snapshot
|
||||
.global_security
|
||||
.country_access
|
||||
@@ -369,7 +365,6 @@ async fn forward_request(
|
||||
"RustProxy: denied by access policy\n",
|
||||
);
|
||||
}
|
||||
// Canonical / HTTPS redirects must not bypass global or per-vhost access policy.
|
||||
if !cache_http_invalidation_method(request.method()) {
|
||||
if let Some(mut response) = frontend.and_then(|frontend| {
|
||||
canonical_host_redirect(frontend, &host, tls, local.0.port(), request.uri())
|
||||
|
||||
@@ -17,7 +17,6 @@ mod http2_tests {
|
||||
request
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn rfc8441_extended_connect_is_recognized() {
|
||||
let request = extended_connect("websocket");
|
||||
|
||||
@@ -3,7 +3,6 @@ mod http3_tests {
|
||||
use http::{Method, Request, Version};
|
||||
use super::{h3_protocol, validate_h3};
|
||||
|
||||
|
||||
#[test]
|
||||
fn rfc9220_extended_connect_is_recognized() {
|
||||
let mut request = Request::builder()
|
||||
|
||||
@@ -104,7 +104,6 @@ fn read_pem_with_policy(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
// O_NONBLOCK avoids hanging on a FIFO; special files are rejected below.
|
||||
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
|
||||
}
|
||||
let file = options
|
||||
|
||||
+1
-1
@@ -303,7 +303,7 @@ mod tests {
|
||||
assert!(pqc.validate().is_err());
|
||||
pqc.tls13 = true;
|
||||
pqc.cipher_suites = vec!["X25519MLKEM768".into()];
|
||||
assert!(pqc.validate().is_err()); // KEX name is not a cipher suite
|
||||
assert!(pqc.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -229,7 +229,6 @@ impl Analytics {
|
||||
let (tx, mut rx) = mpsc::channel::<Sample>(QUEUE_CAPACITY);
|
||||
let worker_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
// Drain on a short interval instead of waking once per request.
|
||||
let mut tick = tokio::time::interval(DRAIN_INTERVAL);
|
||||
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
let mut batch = Vec::with_capacity(256);
|
||||
|
||||
@@ -81,7 +81,6 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Reads a regular file in bounded chunks on the calling task.
|
||||
#[cfg(unix)]
|
||||
pub(super) struct InlineFileStream {
|
||||
file: std::fs::File,
|
||||
|
||||
Reference in new issue
Block a user