v0.31.0
This commit is contained in:
1 parent
f213d853e2
commit
76b8bb7ee2
78 files changed
+7750
-5204
No files matched your search
+107
-29
@@ -23,8 +23,8 @@ function clearInvalidOriginRecovery() {
|
||||
const PANEL_PATHS = { overview: "/overview", vhosts: "/vhosts", "web-server": "/web-server", "docker-vhosts": "/docker-vhosts", redirects: "/redirects", notfound: "/404-hosts", streams: "/streams", "acl-profiles": "/acl-profiles", tls: "/tls", traffic: "/traffic", cache: "/cache", transform: "/transform", filelogs: "/logs", settings: "/settings" };
|
||||
function panelFromLocation() { if (typeof location === "undefined") return "overview"; if (location.pathname === "/configuration" || location.pathname === "/logging") return "settings"; const found = Object.entries(PANEL_PATHS).find(([, path]) => path === location.pathname); return found?.[0] || "overview"; }
|
||||
let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null;
|
||||
let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], polling = false, toastTimer, editingBackend = null, editingRoute = null;
|
||||
let liveEvents = null, liveConnected = false, authEpoch = 0, pendingAuthDestination = null;
|
||||
let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], toastTimer, editingBackend = null, editingRoute = null;
|
||||
let liveEvents = null, liveConnected = false, liveFallback = false, liveInterval = null, authEpoch = 0, pendingAuthDestination = null;
|
||||
let lastApiLatencyMs = null;
|
||||
let adminAuthChannel = null; try { adminAuthChannel = new BroadcastChannel("rustproxy-admin-session"); adminAuthChannel.addEventListener("message", event => { if (event.data === "locked" && authenticated) lock(false); }); } catch { }
|
||||
// Language content is discovered by build.rs from langs/*.json, never compiled
|
||||
@@ -152,14 +152,16 @@ function setPanel(name, navigate = true) {
|
||||
if (name === "security") { setPanel("settings", navigate); window.rustProxySettings?.select("security"); return; }
|
||||
if (!Object.hasOwn(PANEL_PATHS, name)) name = "overview";
|
||||
currentPanel = name;
|
||||
$("overview-refresh").hidden = name !== "overview";
|
||||
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
|
||||
document.querySelectorAll(".panel").forEach(e => e.hidden = e.id !== `panel-${name}`);
|
||||
$("page-label").textContent = t(name); $("page-title").textContent = t(`${name}Title`); $("page-description").textContent = t(`${name}Description`);
|
||||
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
|
||||
if (name === "settings") window.rustProxySettings?.syncLocation?.();
|
||||
window.rustProxyRefresh?.setPanel(name);
|
||||
if (name === "tls") window.rustProxyAcme?.syncLocation?.();
|
||||
if (name === "filelogs") void window.rustProxyFileLogs?.refresh();
|
||||
if (name === "traffic") void window.rustProxyTrafficPanel?.refresh(); if (name === "cache") void window.rustProxyCachePanel?.refresh(); if (name === "transform") void window.rustProxyTransformPanel?.refresh(); if (name === "docker-vhosts") void window.rustProxyDocker?.refresh(false); if (name === "tls") { void window.rustProxyAcme?.refresh(); void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); }); }
|
||||
if (name === "tls") void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); });
|
||||
}
|
||||
function bytes(value) { if (value < 1024) return `${value} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
|
||||
function uptime(value) {
|
||||
@@ -192,8 +194,14 @@ function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
|
||||
}
|
||||
window.rustProxyEnableControl = enhanceEnabledControl;
|
||||
function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; }
|
||||
function renderStatus(s) {
|
||||
const firstStatus = latestStatus === null; latestStatus = s; window.rustProxySettings?.render(); if (firstStatus) renderSettings();
|
||||
function renderStatus(s, systemSample = false) {
|
||||
const firstStatus = latestStatus === null;
|
||||
latestStatus = { ...(latestStatus || {}), ...s,
|
||||
streams: s.streams ? { ...(latestStatus?.streams || {}), ...s.streams } : latestStatus?.streams,
|
||||
listeners: s.listeners ? { ...(latestStatus?.listeners || {}), ...s.listeners } : latestStatus?.listeners,
|
||||
logging: s.logging ? { ...(latestStatus?.logging || {}), ...s.logging } : latestStatus?.logging
|
||||
};
|
||||
s = latestStatus; window.rustProxySettings?.render(); if (firstStatus) renderSettings();
|
||||
connected = true; $("connection-label").textContent = t("connected"); $("signal").classList.remove("status-error"); const m = s.metrics;
|
||||
$("metric-ws").textContent = m.websocket_active || 0;
|
||||
$("metric-ws-h1").textContent = m.websocket_h1_active || 0;
|
||||
@@ -250,7 +258,7 @@ function renderStatus(s) {
|
||||
$("ws-protocol-h2").setAttribute("points", sparklinePoints(websocketProtocolSamples.h2, 300, 60, 5));
|
||||
$("ws-protocol-h3").setAttribute("points", sparklinePoints(websocketProtocolSamples.h3, 300, 60, 5));
|
||||
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
|
||||
renderFooter(s); renderDiagnostics(s); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s);
|
||||
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s);
|
||||
}
|
||||
function renderFooter(s) {
|
||||
const apiLatency = lastApiLatencyMs == null ? "API -" : `API ${lastApiLatencyMs.toFixed(lastApiLatencyMs < 10 ? 1 : 0)} ms`;
|
||||
@@ -273,9 +281,40 @@ function renderFooter(s) {
|
||||
}
|
||||
let lastLogs = null;
|
||||
function renderLogs(data) { lastLogs = data; window.rustProxyLogs?.render(data); }
|
||||
async function refresh(force = false) { if (!authenticated || polling || (!force && liveConnected)) return; polling = true; const epoch = authEpoch; try { const [status, logs] = await Promise.all([api("/api/status"), api("/api/logs")]); if (!authenticated || epoch !== authEpoch) return; renderStatus(status); renderLogs(logs); } catch (e) { connected = false; $("connection-label").textContent = t("disconnected"); if (e.status === 401) lock(); } finally { polling = false; } }
|
||||
function stopLive() { if (liveEvents) liveEvents.close(); liveEvents = null; liveConnected = false; }
|
||||
function startLive() { stopLive(); if (!authenticated || document.hidden || typeof EventSource === "undefined") return; const epoch = authEpoch; const stream = new EventSource("/api/events"); liveEvents = stream; stream.addEventListener("snapshot", event => { if (!authenticated || epoch !== authEpoch) return; try { const payload = JSON.parse(event.data); liveConnected = true; renderStatus(payload.status); renderLogs(payload.logs); } catch { liveConnected = false; } }); stream.onerror = () => { if (epoch !== authEpoch) return; liveConnected = false; void refresh(); }; }
|
||||
async function refresh(force = false) {
|
||||
if (!authenticated) return;
|
||||
if (force) return Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("logs-fallback")]);
|
||||
return rustProxyRefresh.manual("status-fallback");
|
||||
}
|
||||
function renderLiveStatus(value) { renderStatus(value); }
|
||||
function stopLive() {
|
||||
if (liveEvents) { liveEvents.close(); liveEvents = null; }
|
||||
liveConnected = false; liveFallback = false; liveInterval = null;
|
||||
}
|
||||
function startLive() {
|
||||
if (!authenticated || document.hidden || typeof EventSource === "undefined") return;
|
||||
const seconds = window.rustProxyRefresh?.current().overview_seconds || 2;
|
||||
if (liveEvents && liveInterval === seconds) return;
|
||||
stopLive();
|
||||
liveInterval = seconds;
|
||||
const epoch = authEpoch, stream = new EventSource(`/api/events?interval=${seconds}`);
|
||||
liveEvents = stream;
|
||||
stream.addEventListener("snapshot", event => {
|
||||
if (!authenticated || epoch !== authEpoch || stream !== liveEvents) return;
|
||||
try {
|
||||
const payload = JSON.parse(event.data);
|
||||
if (!payload.status || !payload.logs) throw new Error("Invalid SSE payload");
|
||||
liveConnected = true; liveFallback = false;
|
||||
renderLiveStatus(payload.status); renderLogs(payload.logs);
|
||||
window.rustProxyRefresh?.sync();
|
||||
} catch (error) { liveConnected = false; liveFallback = true; window.rustProxyRefresh?.sync(); }
|
||||
});
|
||||
stream.onerror = () => {
|
||||
if (epoch !== authEpoch || stream !== liveEvents) return;
|
||||
liveConnected = false; liveFallback = true;
|
||||
window.rustProxyRefresh?.sync();
|
||||
};
|
||||
}
|
||||
async function refreshCertificateInventory() {
|
||||
if (!authenticated) return;
|
||||
const epoch = authEpoch;
|
||||
@@ -322,6 +361,7 @@ function authReturnDestination() {
|
||||
return tab ? `/settings?tab=${encodeURIComponent(tab)}` : "/settings";
|
||||
}
|
||||
function lock(notify = true) {
|
||||
window.rustProxyRefresh?.stop();
|
||||
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
||||
@@ -334,16 +374,52 @@ function lock(notify = true) {
|
||||
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
|
||||
}
|
||||
|
||||
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-menu").hidden = false; $("logs-open").hidden = false; $("file-logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyFileLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); window.rustProxyDocker?.applyPreferences(preferences); window.rustProxyRedirects?.applyPreferences(preferences); window.rustProxyNotFound?.applyPreferences(preferences); window.rustProxyStreams?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
|
||||
async function activate(info) {
|
||||
pendingAuthDestination = null; authMessage("");
|
||||
for (const id of ["api-docs", "logs-menu", "logs-open", "file-logs-open"]) $(id).hidden = false;
|
||||
const epoch = ++authEpoch;
|
||||
sessionInfo = info; csrf = info.csrf_token; authenticated = true;
|
||||
for (const id of ["password", "password-confirm", "setup-token"]) $(id).value = "";
|
||||
$("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false;
|
||||
renderSession();
|
||||
// Paint a usable shell immediately. Preferences, configuration and initial
|
||||
// telemetry are network requests and must not block the first content.
|
||||
document.documentElement.classList.remove("app-loading");
|
||||
document.documentElement.classList.remove("app-startup-failed");
|
||||
rustProxyRefresh.start();
|
||||
const preferences = await loadAdminPreferences();
|
||||
if (!authenticated || epoch !== authEpoch) return;
|
||||
window.rustProxyLogs?.applyPreferences(preferences);
|
||||
window.rustProxyFileLogs?.applyPreferences(preferences);
|
||||
window.rustProxyVhosts?.applyPreferences(preferences);
|
||||
window.rustProxyDocker?.applyPreferences(preferences);
|
||||
window.rustProxyRedirects?.applyPreferences(preferences);
|
||||
window.rustProxyNotFound?.applyPreferences(preferences);
|
||||
window.rustProxyStreams?.applyPreferences(preferences);
|
||||
// The live SSE stream is already running. Configuration can load behind the
|
||||
// visible shell; it does not belong in the live refresh cycle.
|
||||
try { await loadConfig(); }
|
||||
catch (error) {
|
||||
if (authenticated && epoch === authEpoch) toast(error.message, true);
|
||||
return;
|
||||
}
|
||||
if (!authenticated || epoch !== authEpoch) return;
|
||||
// A single full snapshot seeds non-live diagnostics; subsequent updates
|
||||
// continue over SSE rather than polling full status on a timer.
|
||||
void refresh(true).catch(error => {
|
||||
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
||||
});
|
||||
}
|
||||
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [formatDateTime(item.time * 1000), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
|
||||
let tunnelRefreshTimer = null;
|
||||
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
|
||||
async function loadTunnels() {
|
||||
if (!authenticated) return;
|
||||
let tunnelsLoading = false;
|
||||
async function loadTunnels(context = null) {
|
||||
if (!authenticated || tunnelsLoading) return;
|
||||
tunnelsLoading = true;
|
||||
const epoch = authEpoch;
|
||||
try {
|
||||
const data = await api("/api/tunnels");
|
||||
if (!authenticated || epoch !== authEpoch) return;
|
||||
const data = await api("/api/tunnels", context?.signal ? { signal: context.signal } : {});
|
||||
if (!authenticated || epoch !== authEpoch || !$("tunnels-dialog")?.open || (context?.isCurrent && !context.isCurrent())) return;
|
||||
const rows = (data.tunnels || []).map(item => {
|
||||
const tr = el("tr");
|
||||
for (const value of [item.id, item.client, item.host, item.upstream, item.protocol, `${fmt((Number(item.duration_ms) || 0) / 1000, 1)} s`, fmtBytes(item.rx_bytes), fmtBytes(item.tx_bytes)]) {
|
||||
@@ -365,10 +441,11 @@ async function loadTunnels() {
|
||||
actions.append(kill); tr.append(actions); return tr;
|
||||
});
|
||||
$("tunnels-rows").replaceChildren(...rows); $("tunnels-empty").hidden = rows.length > 0;
|
||||
} catch (error) { if (authenticated && epoch === authEpoch) tunnelMessage(error.message, true); }
|
||||
} catch (error) { if (error?.name !== "AbortError" && authenticated && epoch === authEpoch && $("tunnels-dialog")?.open) tunnelMessage(error.message, true); }
|
||||
finally { tunnelsLoading = false; }
|
||||
}
|
||||
function stopTunnelRefresh() { if (tunnelRefreshTimer) clearInterval(tunnelRefreshTimer); tunnelRefreshTimer = null; }
|
||||
function startTunnelRefresh() { stopTunnelRefresh(); tunnelRefreshTimer = setInterval(() => { if ($("tunnels-dialog")?.open) void loadTunnels(); }, 2000); }
|
||||
function stopTunnelRefresh() { window.rustProxyRefresh?.sync(); }
|
||||
function startTunnelRefresh() { window.rustProxyRefresh?.sync(); }
|
||||
$("login-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; authMessage(""); try { await withFormSubmitState(form, event.submitter, async () => { const username = $("username").value.trim(); const password = $("password").value; if (setupRequired) { if (password !== $("password-confirm").value) throw fieldError("password-confirm", t("passwordMismatch")); await api("/api/auth/setup", { method: "POST", body: JSON.stringify({ token: $("setup-token").value.trim(), username, password }) }); setupRequired = false; syncAuthForm(); } const info = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); await activate(info); clearTimeout(toastTimer); hideToast(); }); } catch (e) { if (e.code === "auth.already_initialized") { setupRequired = false; syncAuthForm(); } $("password").value = ""; $("password-confirm").value = ""; authMessage(e.message); } });
|
||||
$("logout").addEventListener("click", async () => { try { await api("/api/auth/logout", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { if (e.status === 401) lock(); toast(e.message, true); } });
|
||||
$("about-open").addEventListener("click", () => { const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
|
||||
@@ -390,9 +467,9 @@ $("revoke-sessions").addEventListener("click", async () => { if (!(await appConf
|
||||
$("security-audit-open").addEventListener("click", () => { const dialog = $("audit-dialog"); if (!dialog.open) dialog.showModal(); void loadAudit(); });
|
||||
$("audit-dialog-close").addEventListener("click", () => $("audit-dialog").close());
|
||||
$("reload-audit").addEventListener("click", () => void loadAudit());
|
||||
$("tunnels-open").addEventListener("click", () => { const dialog = $("tunnels-dialog"); tunnelMessage(); if (!dialog.open) dialog.showModal(); void loadTunnels(); startTunnelRefresh(); });
|
||||
$("tunnels-close").addEventListener("click", () => { stopTunnelRefresh(); $("tunnels-dialog").close(); });
|
||||
$("tunnels-reload").addEventListener("click", () => void loadTunnels());
|
||||
$("tunnels-open").addEventListener("click", () => { const dialog = $("tunnels-dialog"); tunnelMessage(); if (!dialog.open) dialog.showModal(); void rustProxyRefresh.manual("tunnels"); startTunnelRefresh(); });
|
||||
$("tunnels-close").addEventListener("click", () => { $("tunnels-dialog").close(); stopTunnelRefresh(); });
|
||||
$("tunnels-reload").addEventListener("click", () => void rustProxyRefresh.manual("tunnels"));
|
||||
$("tunnels-dialog").addEventListener("close", stopTunnelRefresh);
|
||||
$("config-editor").addEventListener("input", () => setSettingsDirty("config-editor"));
|
||||
$("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); } });
|
||||
@@ -404,18 +481,19 @@ async function revalidateSession() {
|
||||
clearInvalidOriginRecovery();
|
||||
if (epoch !== authEpoch) return;
|
||||
if (!authenticated) { await activate(info); return; }
|
||||
sessionInfo = info; csrf = info.csrf_token; renderSession(); await refresh(true); startLive();
|
||||
sessionInfo = info; csrf = info.csrf_token; renderSession(); window.rustProxyRefresh?.sync();
|
||||
} catch (error) { if (recoverInvalidOrigin(error)) return; if (error.status !== 401) toast(error.message, true); }
|
||||
}
|
||||
window.addEventListener("pageshow", event => { if (event.persisted) void revalidateSession(); });
|
||||
document.addEventListener("visibilitychange", () => { if (document.hidden) stopLive(); else if (authenticated) { void revalidateSession(); } });
|
||||
setInterval(() => { if (!document.hidden) void refresh(); }, 5000);
|
||||
function showStartupFailure(error) {
|
||||
// index.html is served only after the server has accepted the browser session.
|
||||
// A module/configuration error must therefore never masquerade as a logout.
|
||||
// Never expose a blank page or an incomplete workspace after startup fails.
|
||||
// The authenticated shell keeps its own retry link; 401 still redirects to login.
|
||||
$("login").hidden = true;
|
||||
$("workspace").hidden = false;
|
||||
$("workspace").hidden = true;
|
||||
$("logout").hidden = !authenticated;
|
||||
document.documentElement.classList.remove("app-loading");
|
||||
toast(error?.message || String(error), true);
|
||||
document.documentElement.classList.add("app-loading", "app-startup-failed");
|
||||
$("app-startup-title").textContent = languagePacks.size ? t("startupFailed") : "Could not load RustProxy";
|
||||
const detail = $("app-startup-detail");
|
||||
detail.textContent = error?.message || String(error);
|
||||
detail.hidden = false;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
button_style: "normal", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [],
|
||||
});
|
||||
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool" });
|
||||
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve();
|
||||
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
|
||||
|
||||
function normalizeAdminTheme(value) { return ADMIN_THEMES.has(value) ? value : "dark"; }
|
||||
function normalizeAdminLanguage(value) { return value === "auto" || /^[A-Za-z0-9_-]{1,64}$/.test(value || "") ? value : "auto"; }
|
||||
@@ -122,7 +122,7 @@ function refreshChartStylePalette(active = adminPreferences?.chart_style || "def
|
||||
});
|
||||
target.replaceChildren(...buttons);
|
||||
}
|
||||
function refreshAppearanceUi() { refreshChartStylePalette(); renderDashboardWidgetSettings(); window.rustProxySettings?.refreshSearch?.(); }
|
||||
function refreshAppearanceUi() { refreshChartStylePalette(); renderDashboardWidgetSettings(); renderRefreshPreferences(adminPreferences?.refresh); window.rustProxySettings?.refreshSearch?.(); }
|
||||
function formatDateTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
|
||||
function formatTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleTimeString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
|
||||
function applyAdminPreferences(value, cache = true) {
|
||||
@@ -130,11 +130,14 @@ function applyAdminPreferences(value, cache = true) {
|
||||
const dashboard = applyDashboardLayout(value.dashboard_widgets, value.dashboard_hidden_widgets);
|
||||
const normalized = {
|
||||
...value,
|
||||
refresh: normalizeRefreshPreferences(value.refresh),
|
||||
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
|
||||
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), button_style: applyAdminButtonStyle(value.button_style), table_density: applyAdminTableDensity(value.table_density),
|
||||
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: dashboard.order, dashboard_hidden_widgets: dashboard.hidden,
|
||||
};
|
||||
adminPreferences = normalized;
|
||||
window.rustProxyRefresh?.update(normalized.refresh);
|
||||
renderRefreshPreferences(normalized.refresh);
|
||||
window.rustProxyVhosts?.applyPreferences(normalized);
|
||||
window.rustProxyDocker?.applyPreferences(normalized);
|
||||
window.rustProxyRedirects?.applyPreferences(normalized);
|
||||
@@ -152,9 +155,18 @@ async function loadAdminPreferences(force = false) {
|
||||
if (adminPreferencesLoading) return adminPreferencesLoading;
|
||||
const epoch = authEpoch;
|
||||
adminPreferencesLoading = (async () => {
|
||||
const value = await api("/api/preferences");
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
return applyAdminPreferences(value);
|
||||
try {
|
||||
const value = await api("/api/preferences");
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
adminPreferencesUnavailable = false;
|
||||
return applyAdminPreferences(value);
|
||||
} catch (error) {
|
||||
if (error.status === 401 || !authenticated || epoch !== authEpoch) throw error;
|
||||
// Preferences are optional for startup. Keep UI and live telemetry usable.
|
||||
console.warn("Preferences unavailable; using defaults", error);
|
||||
adminPreferencesUnavailable = true;
|
||||
return applyAdminPreferences({ ...ADMIN_APPEARANCE_DEFAULTS, refresh: REFRESH_DEFAULTS });
|
||||
}
|
||||
})();
|
||||
try { return await adminPreferencesLoading; } finally { adminPreferencesLoading = null; }
|
||||
}
|
||||
@@ -162,9 +174,11 @@ function updateAdminPreferences(patch) {
|
||||
const epoch = authEpoch;
|
||||
const task = adminPreferencesWrite.then(async () => {
|
||||
if (!authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
|
||||
const current = await loadAdminPreferences();
|
||||
const current = await loadAdminPreferences(adminPreferencesUnavailable);
|
||||
if (adminPreferencesUnavailable) throw new Error("Cannot persist preferences while the preference service is unavailable");
|
||||
if (!current || !authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
|
||||
const next = { ...current, ...patch };
|
||||
next.refresh = normalizeRefreshPreferences(next.refresh);
|
||||
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
|
||||
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.button_style = normalizeAdminButtonStyle(next.button_style); next.table_density = normalizeAdminDensity(next.table_density);
|
||||
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets);
|
||||
@@ -174,7 +188,44 @@ function updateAdminPreferences(patch) {
|
||||
});
|
||||
adminPreferencesWrite = task.catch(() => undefined); return task;
|
||||
}
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); window.rustProxyRefresh?.update(REFRESH_DEFAULTS); }
|
||||
function renderRefreshPreferences(value = REFRESH_DEFAULTS) {
|
||||
const refresh = normalizeRefreshPreferences(value);
|
||||
const switcher = $("refresh-live-enabled"); if (switcher) switcher.checked = refresh.live_enabled;
|
||||
for (const field of Object.keys(REFRESH_CHOICES)) {
|
||||
const input = $(`refresh-${field.replaceAll("_", "-")}`);
|
||||
if (input) input.value = String(refresh[field]);
|
||||
}
|
||||
const manual = refresh.system_info_seconds === 0 || !refresh.live_enabled;
|
||||
const hint = $("system-info-refresh-note");
|
||||
if (hint) hint.textContent = t(manual ? "refreshManualHint" : "refreshSystemHint");
|
||||
}
|
||||
for (const control of document.querySelectorAll("[data-refresh-preference]")) {
|
||||
control.addEventListener("change", () => {
|
||||
const key = control.dataset.refreshPreference;
|
||||
const previous = normalizeRefreshPreferences(adminPreferences?.refresh);
|
||||
const value = key === "live_enabled" ? control.checked : Number(control.value);
|
||||
if (key !== "live_enabled" && !REFRESH_CHOICES[key]?.includes(value)) {
|
||||
renderRefreshPreferences(previous); return;
|
||||
}
|
||||
const next = { ...previous, [key]: value };
|
||||
if (adminPreferences) adminPreferences.refresh = next;
|
||||
window.rustProxyRefresh?.update(next); renderRefreshPreferences(next);
|
||||
void saveAppearancePreference({ refresh: next }, control, () => {
|
||||
if (adminPreferences) adminPreferences.refresh = previous;
|
||||
window.rustProxyRefresh?.update(previous); renderRefreshPreferences(previous);
|
||||
});
|
||||
});
|
||||
}
|
||||
$("refresh-reset")?.addEventListener("click", () => {
|
||||
const previous = normalizeRefreshPreferences(adminPreferences?.refresh), next = { ...REFRESH_DEFAULTS };
|
||||
if (adminPreferences) adminPreferences.refresh = next;
|
||||
window.rustProxyRefresh?.update(next); renderRefreshPreferences(next);
|
||||
void saveAppearancePreference({ refresh: next }, $("refresh-reset"), () => {
|
||||
if (adminPreferences) adminPreferences.refresh = previous;
|
||||
window.rustProxyRefresh?.update(previous); renderRefreshPreferences(previous);
|
||||
});
|
||||
});
|
||||
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme, refreshAppearanceUi };
|
||||
|
||||
async function saveAppearancePreference(patch, control, rollback) {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// Single scheduler for all authenticated dashboard polling. SSE is controlled here
|
||||
// but remains one native EventSource with its own browser reconnect behavior.
|
||||
const REFRESH_DEFAULTS = Object.freeze({
|
||||
live_enabled: true, overview_seconds: 2, traffic_seconds: 5,
|
||||
cache_seconds: 5, system_info_seconds: 60
|
||||
});
|
||||
const REFRESH_CHOICES = Object.freeze({
|
||||
overview_seconds: [1, 2, 5, 10], traffic_seconds: [0, 3, 5, 10, 30],
|
||||
cache_seconds: [0, 3, 5, 10, 30], system_info_seconds: [0, 15, 30, 60, 300]
|
||||
});
|
||||
function normalizeRefreshPreferences(input = {}) {
|
||||
const value = { ...REFRESH_DEFAULTS };
|
||||
if (typeof input.live_enabled === "boolean") value.live_enabled = input.live_enabled;
|
||||
for (const [key, allowed] of Object.entries(REFRESH_CHOICES)) {
|
||||
if (allowed.includes(input[key])) value[key] = input[key];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const rustProxyRefresh = (() => {
|
||||
const tasks = new Map();
|
||||
let ready = false, options = { ...REFRESH_DEFAULTS }, panel = currentPanel;
|
||||
let settingsTab = "listeners", watchingVisibility = false;
|
||||
const visible = () => !document.hidden;
|
||||
function allowed(task) {
|
||||
return ready && authenticated && visible() && options.live_enabled &&
|
||||
task.interval() > 0 && (!task.active || task.active());
|
||||
}
|
||||
function clear(task, abort = false) {
|
||||
if (task.timer !== null) clearTimeout(task.timer);
|
||||
task.timer = null;
|
||||
if (abort) {
|
||||
++task.generation;
|
||||
task.controller?.abort();
|
||||
}
|
||||
}
|
||||
function schedule(task, delay) {
|
||||
if (task.timer !== null || task.inFlight || !allowed(task)) return;
|
||||
task.timer = setTimeout(() => { task.timer = null; void execute(task, false); }, delay);
|
||||
}
|
||||
function execute(task, manual) {
|
||||
if (!authenticated || (!manual && !allowed(task))) return Promise.resolve();
|
||||
if (task.inFlight) {
|
||||
if (manual) task.queued = true;
|
||||
return task.inFlight;
|
||||
}
|
||||
clear(task);
|
||||
const controller = new AbortController(), generation = task.generation, epoch = authEpoch;
|
||||
const panelAtStart = panel, tabAtStart = settingsTab;
|
||||
task.controller = controller;
|
||||
// A manual request remains valid with Live switched off, but not in a stale view.
|
||||
const isCurrent = () => authenticated && epoch === authEpoch && !controller.signal.aborted &&
|
||||
generation === task.generation && (manual ? panel === panelAtStart && settingsTab === tabAtStart : allowed(task));
|
||||
const promise = Promise.resolve().then(() => task.run({ signal: controller.signal, isCurrent }));
|
||||
task.inFlight = promise.catch(error => {
|
||||
if (error?.name !== "AbortError" && isCurrent()) {
|
||||
// A failure is isolated to this source; other timers continue.
|
||||
if (task.onError) task.onError(error);
|
||||
else console.warn(`Refresh ${task.id} failed:`, error);
|
||||
}
|
||||
}).finally(() => {
|
||||
task.inFlight = null;
|
||||
if (task.controller === controller) task.controller = null;
|
||||
if (task.queued && authenticated) {
|
||||
task.queued = false;
|
||||
void execute(task, true);
|
||||
} else {
|
||||
task.queued = false;
|
||||
schedule(task, task.interval() * 1000);
|
||||
}
|
||||
});
|
||||
return task.inFlight;
|
||||
}
|
||||
function register(id, spec) {
|
||||
if (tasks.has(id)) throw new Error(`Duplicate refresh task: ${id}`);
|
||||
if (typeof spec.run !== "function" || typeof spec.interval !== "function") throw new Error(`Invalid refresh task: ${id}`);
|
||||
const task = { ...spec, id, timer: null, inFlight: null, controller: null, generation: 0, queued: false, enabled: false, seconds: null };
|
||||
tasks.set(id, task);
|
||||
sync();
|
||||
return () => { clear(task, true); tasks.delete(id); };
|
||||
}
|
||||
function sync() {
|
||||
for (const task of tasks.values()) {
|
||||
const shouldRun = allowed(task), seconds = task.interval();
|
||||
if (!shouldRun) {
|
||||
if (task.enabled) clear(task, true);
|
||||
task.enabled = false;
|
||||
} else if (!task.enabled || task.seconds !== seconds) {
|
||||
clear(task);
|
||||
task.enabled = true;
|
||||
schedule(task, 0);
|
||||
}
|
||||
task.seconds = seconds;
|
||||
}
|
||||
if (ready && authenticated && visible() && options.live_enabled) startLive();
|
||||
else stopLive();
|
||||
}
|
||||
function onVisibility() { sync(); if (visible() && ready && authenticated) void revalidateSession(); }
|
||||
function start() {
|
||||
ready = true; panel = currentPanel;
|
||||
settingsTab = window.rustProxySettings?.current?.() || "listeners";
|
||||
if (!watchingVisibility) { document.addEventListener("visibilitychange", onVisibility); watchingVisibility = true; }
|
||||
sync();
|
||||
}
|
||||
function stop() {
|
||||
ready = false;
|
||||
for (const task of tasks.values()) {
|
||||
clear(task, true); task.queued = false; task.enabled = false;
|
||||
}
|
||||
if (watchingVisibility) { document.removeEventListener("visibilitychange", onVisibility); watchingVisibility = false; }
|
||||
stopLive();
|
||||
}
|
||||
function update(value) { options = normalizeRefreshPreferences(value); sync(); }
|
||||
function setPanel(value) {
|
||||
panel = value; settingsTab = window.rustProxySettings?.current?.() || settingsTab;
|
||||
for (const task of tasks.values()) if (task.active && !task.active()) task.queued = false;
|
||||
sync();
|
||||
}
|
||||
function setSettingsTab(value) {
|
||||
settingsTab = value;
|
||||
for (const task of tasks.values()) if (task.active && !task.active()) task.queued = false;
|
||||
sync();
|
||||
}
|
||||
function manual(id) {
|
||||
const task = tasks.get(id);
|
||||
if (!task) return Promise.reject(new Error(`Unknown refresh task: ${id}`));
|
||||
return execute(task, true);
|
||||
}
|
||||
register("status-full", { interval: () => 0, run: async ({ signal, isCurrent }) => {
|
||||
const value = await api("/api/status", { signal });
|
||||
if (isCurrent()) renderStatus(value, true);
|
||||
}});
|
||||
register("status-fallback", { interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
|
||||
const value = await api("/api/status?view=live", { signal });
|
||||
if (isCurrent()) renderLiveStatus(value);
|
||||
}, onError: error => { if (error?.status !== 401) $("connection-label").textContent = t("disconnected"); }});
|
||||
register("logs-fallback", { interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
|
||||
const value = await api("/api/logs", { signal });
|
||||
if (isCurrent()) renderLogs(value);
|
||||
}});
|
||||
register("system-info", { interval: () => options.system_info_seconds,
|
||||
active: () => panel === "overview" || (panel === "settings" && ["system-info", "cache"].includes(settingsTab)),
|
||||
run: async ({ signal, isCurrent }) => {
|
||||
const value = await api("/api/system-info", { signal });
|
||||
if (!isCurrent()) return;
|
||||
latestStatus = { ...(latestStatus || {}), ...value };
|
||||
if (latestStatus.metrics) renderDiagnostics(latestStatus, false, true);
|
||||
window.rustProxySettings?.render();
|
||||
}
|
||||
});
|
||||
register("tunnels", { interval: () => 2, active: () => !!$("tunnels-dialog")?.open,
|
||||
run: async context => { await loadTunnels(context); } });
|
||||
return { register, manual, start, stop, sync, update, setPanel, setSettingsTab,
|
||||
current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null })) };
|
||||
})();
|
||||
window.rustProxyRefresh = rustProxyRefresh;
|
||||
|
||||
$("overview-refresh")?.addEventListener("click", () => {
|
||||
void Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("logs-fallback"), rustProxyRefresh.manual("system-info")]);
|
||||
});
|
||||
$("system-info-refresh")?.addEventListener("click", () => void rustProxyRefresh.manual("system-info"));
|
||||
@@ -242,7 +242,7 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
target.append(el("p", t("networkNoVisibleInterfaces"), "empty network-interface-empty"));
|
||||
}
|
||||
}
|
||||
function renderDiagnostics(status, appendSample = true) {
|
||||
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
|
||||
const d = status.system || {}, c = status.cache || {}, m = status.metrics || {};
|
||||
$("metric-h3").textContent = fmt(m.http3_requests);
|
||||
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
|
||||
@@ -257,7 +257,7 @@ function renderDiagnostics(status, appendSample = true) {
|
||||
const sourceSupported = !!d.source && d.source !== "unsupported";
|
||||
$("system-source").hidden = !sourceSupported;
|
||||
if (sourceSupported) $("system-source").textContent = t("realSample");
|
||||
if (appendSample && d.cpu_percent != null) { systemSamples.push(d.cpu_percent); systemSamples = systemSamples.slice(-30); }
|
||||
if (systemSample && d.cpu_percent != null) { systemSamples.push(d.cpu_percent); systemSamples = systemSamples.slice(-30); }
|
||||
$("system-sparkline").setAttribute("points", systemSamples.map((n, i) => `${i * 300 / Math.max(1, systemSamples.length - 1)},${95 - Math.max(0, Math.min(100, n)) * .9}`).join(" "));
|
||||
const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("system-memory", systemMemory != null, systemMemory);
|
||||
@@ -294,6 +294,8 @@ function renderDiagnostics(status, appendSample = true) {
|
||||
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
|
||||
lastCacheCounters = currentCounters;
|
||||
}
|
||||
}
|
||||
if (systemSample) {
|
||||
if (d.process_cpu_percent != null) { processCpuSamples.push(Number(d.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-60); }
|
||||
if (d.process_rss_bytes != null) { processRssSamples.push(Number(d.process_rss_bytes)); processRssSamples = processRssSamples.slice(-60); }
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ function renderSettings() {
|
||||
$("cache-sie-timeout").checked = c.stale_if_error_timeout !== false; $("cache-sie-connection").checked = c.stale_if_error_connection !== false; $("cache-sie-dns").checked = c.stale_if_error_dns !== false; $("cache-sie-tls").checked = c.stale_if_error_tls !== false; $("cache-sie-statuses").value = (c.stale_if_error_statuses || [500, 502, 503, 504]).join(", ");
|
||||
$("cache-proactive-enabled").checked = c.proactive_refresh_enabled === true; $("cache-proactive-before").value = c.proactive_refresh_before_expiry_seconds ?? 15; $("cache-proactive-percent").value = c.proactive_refresh_percent ?? 10; $("cache-proactive-inflight").value = c.proactive_refresh_max_inflight ?? 4;
|
||||
$("cache-query").checked = !!c.bypass_query; $("cache-prefixes").value = (c.bypass_prefixes || []).join("\n");
|
||||
$("cache-http-invalidation-cidrs").value = (c.http_invalidation_allowed_cidrs || []).join("\n");
|
||||
$("cache-static-cookie").checked = c.static_ignore_cookies !== false; $("cache-static-extensions").value = (c.static_extensions || []).join("\n");
|
||||
$("cache-status-header").checked = !!c.expose_status; $("cache-hits-header").checked = !!c.expose_hits; $("cache-rfc-header").checked = !!c.expose_cache_status; $("cache-resident-age-header").checked = !!c.expose_resident_age; $("cache-only-hits").checked = !!c.expose_only_hits;
|
||||
const viaHeader = c.via_header === undefined ? "RustProxy" : c.via_header;
|
||||
@@ -285,7 +286,7 @@ bindSettingsForm("response-form", next => {
|
||||
next.compression = { min_bytes: Number($("compress-min").value) }; for (const name of ["gzip", "br", "zstd", "deflate"]) next.compression[name] = $(`compress-${name}`).checked;
|
||||
}, "response", next => ({ server_header: next.server_header, diagnostic_headers: next.diagnostic_headers, compression: next.compression }));
|
||||
bindSettingsForm("cache-form", next => {
|
||||
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, expose_only_hits: $("cache-only-hits").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null };
|
||||
next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), http_invalidation_allowed_cidrs: $("cache-http-invalidation-cidrs").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, expose_only_hits: $("cache-only-hits").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null };
|
||||
}, "cache");
|
||||
bindSettingsForm("transform-form", next => {
|
||||
next.transform = {
|
||||
|
||||
Reference in new issue
Block a user