494 lines
47 KiB
JavaScript
494 lines
47 KiB
JavaScript
"use strict";
|
|
const $ = (id) => document.getElementById(id);
|
|
let language = null, languagePreference = null;
|
|
try { languagePreference = localStorage.getItem("rp-language"); language = languagePreference; } catch { }
|
|
let csrf = "", authenticated = false, setupRequired = false, sessionInfo = null;
|
|
const ORIGIN_RECOVERY_KEY = "rp-origin-recovery-v1";
|
|
function recoverInvalidOrigin(error) {
|
|
if (error?.code !== "auth.invalid_origin" || typeof location === "undefined") return false;
|
|
const marker = `${location.origin}${location.pathname}${location.search}`;
|
|
try {
|
|
if (sessionStorage.getItem(ORIGIN_RECOVERY_KEY) === marker) return false;
|
|
sessionStorage.setItem(ORIGIN_RECOVERY_KEY, marker);
|
|
} catch {
|
|
if (recoverInvalidOrigin.attempted) return false;
|
|
recoverInvalidOrigin.attempted = true;
|
|
}
|
|
location.reload();
|
|
return true;
|
|
}
|
|
function clearInvalidOriginRecovery() {
|
|
try { sessionStorage.removeItem(ORIGIN_RECOVERY_KEY); } catch { recoverInvalidOrigin.attempted = false; }
|
|
}
|
|
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 = [], toastTimer, editingBackend = null, editingRoute = null;
|
|
let liveEvents = null, liveConnected = false, liveFallback = false, liveInterval = null, authEpoch = 0, pendingAuthDestination = null;
|
|
let lastApiLatencyMs = null, sessionCheckPromise = null;
|
|
let adminAuthChannel = null; try { adminAuthChannel = new BroadcastChannel("rustproxy-admin-session"); adminAuthChannel.addEventListener("message", event => { if (event.data === "locked" && authenticated) lock(false); }); } catch { }
|
|
let languagePacks = new Map(), defaultLanguage = null;
|
|
function activePack() { return languagePacks.get(language) || languagePacks.get(defaultLanguage); }
|
|
function locale() { try { return Intl.getCanonicalLocales(activePack()?.meta.locale || "und")[0]; } catch { return "und"; } }
|
|
function t(key) { const pack = activePack()?.translations; const fallback = languagePacks.get(defaultLanguage)?.translations; return (pack && Object.hasOwn(pack, key) ? pack[key] : fallback?.[key]) ?? key; }
|
|
async function loadLanguages() {
|
|
const bundle = await api("/assets/langs.json");
|
|
if (!Array.isArray(bundle.languages) || !bundle.languages.length) throw new Error("i18n.invalid_bundle");
|
|
languagePacks = new Map(bundle.languages.map(pack => [pack.id, pack])); defaultLanguage = bundle.default;
|
|
if (!languagePacks.has(defaultLanguage)) throw new Error("i18n.missing_fallback");
|
|
const candidates = [language, ...(navigator.languages || [navigator.language])].filter(Boolean).map(v => v.replaceAll("_", "-").toLowerCase());
|
|
const packs = [...languagePacks.values()];
|
|
const selected = candidates.map(value => packs.find(p => p.id.replaceAll("_", "-").toLowerCase() === value || p.meta.locale.toLowerCase() === value) || packs.find(p => p.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean);
|
|
language = selected?.id || defaultLanguage;
|
|
const automatic = el("option", t("languageAutomatic")); automatic.value = "auto"; automatic.dataset.i18n = "languageAutomatic";
|
|
$("language").replaceChildren(automatic, ...packs.map(pack => { const option = el("option", pack.meta.native_name); option.value = pack.id; return option; }));
|
|
$("language").value = languagePreference === "auto" ? "auto" : language;
|
|
}
|
|
let latestStatus = null, certificateInventory = new Map();
|
|
let selectedCacheMemoryPreset = null, selectedCacheColdPreset = null, selectedTransformPreset = null;
|
|
function translate() {
|
|
clearTimeout(toastTimer); hideToast();
|
|
document.documentElement.lang = locale(); document.documentElement.dir = activePack()?.meta.direction || "ltr";
|
|
document.querySelectorAll("[data-i18n]").forEach(e => e.textContent = t(e.dataset.i18n));
|
|
document.querySelectorAll("[data-i18n-placeholder]").forEach(e => e.placeholder = t(e.dataset.i18nPlaceholder));
|
|
document.querySelectorAll("[data-i18n-aria]").forEach(e => e.setAttribute("aria-label", t(e.dataset.i18nAria)));
|
|
window.rustProxyAclProfiles?.refreshTranslations?.();
|
|
$("language").value = languagePreference === "auto" ? "auto" : language; setPanel(currentPanel, false); syncAuthForm(); renderSession();
|
|
window.rustProxyDrafts?.refresh();
|
|
window.rustProxyPreferences?.refreshAppearanceUi?.();
|
|
$("connection-label").textContent = t(connected ? "connected" : "disconnected");
|
|
if (envelope) renderConfig(); if (latestStatus) { renderFooter(latestStatus); renderDiagnostics(latestStatus, false); window.rustProxyVhosts?.renderStatus(latestStatus); window.rustProxyStreams?.renderStatus(latestStatus); window.rustProxyStreamDashboard?.render(latestStatus); window.rustProxyBackendDashboard?.render(latestStatus, null, false); window.rustProxyExtraOverview?.render(latestStatus, false); window.rustProxyWideOverview?.render(latestStatus, false); window.rustProxyWebOverview?.render(latestStatus, false); } if (lastLogs) renderLogs(lastLogs);
|
|
syncSettingsDirtyIndicators();
|
|
}
|
|
function hideToast() { const node = $("toast"); if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover(); node.hidden = true; }
|
|
function raiseToast() {
|
|
const node = $("toast"); if (node.hidden) return;
|
|
const modal = [...document.querySelectorAll("dialog[open]")].at(-1), parent = modal || document.body;
|
|
if (typeof node.hidePopover === "function" && node.matches(":popover-open")) node.hidePopover();
|
|
if (node.parentElement !== parent) parent.append(node);
|
|
if (typeof node.showPopover === "function") node.showPopover();
|
|
}
|
|
function toast(message, error = false) { clearTimeout(toastTimer); const node = $("toast"); node.textContent = message; node.classList.toggle("error", error); node.hidden = false; raiseToast(); toastTimer = setTimeout(hideToast, error ? 10000 : 4000); }
|
|
function authMessage(message = "") { const node = $("auth-message"); if (!node) return; node.textContent = message; node.classList.toggle("error", Boolean(message)); }
|
|
function fieldError(target, message) { const error = new Error(message); error.fieldTarget = target; return error; }
|
|
function clearFieldError(target) {
|
|
if (!target) return; target.classList?.remove("field-invalid"); target.removeAttribute?.("aria-invalid");
|
|
}
|
|
function clearFormFieldErrors(form) { form?.querySelectorAll(".field-invalid,[aria-invalid=true]").forEach(clearFieldError); }
|
|
function showFieldError(error, form = null) {
|
|
let target = error?.fieldTarget; if (typeof target === "string") target = $(target);
|
|
if (!target || (form && !form.contains(target))) return false;
|
|
const focusTarget = target.matches?.("input,select,textarea,button") ? target : target.querySelector?.("input,select,textarea,button");
|
|
target.classList?.add("field-invalid"); target.setAttribute?.("aria-invalid", "true");
|
|
const clear = () => clearFieldError(target); target.addEventListener?.("input", clear, { once: true }); target.addEventListener?.("change", clear, { once: true });
|
|
target.scrollIntoView?.({ block: "center", behavior: "smooth" }); focusTarget?.focus?.({ preventScroll: true });
|
|
return true;
|
|
}
|
|
async function withFormSubmitState(form, submitter, task) {
|
|
const button = submitter || form?.querySelector('button[type="submit"]');
|
|
const originalText = button?.textContent ?? "", i18nKey = button?.dataset?.i18n || "";
|
|
clearFormFieldErrors(form);
|
|
if (button) { button.disabled = true; button.classList.add("is-saving"); button.textContent = t("saving"); }
|
|
try { return await task(); }
|
|
catch (error) { showFieldError(error, form); throw error; }
|
|
finally { if (button) { button.disabled = false; button.classList.remove("is-saving"); button.textContent = i18nKey ? t(i18nKey) : originalText; } }
|
|
}
|
|
function appConfirm(message, options = {}) {
|
|
const title = options.title || t("confirmTitle"), confirmLabel = options.confirmLabel || t("continueAction"), cancelLabel = options.cancelLabel || t("cancelAction");
|
|
return new Promise(resolve => {
|
|
const dialog = el("dialog", undefined, "app-confirm-dialog"); dialog.setAttribute("aria-modal", "true");
|
|
const card = el("div", undefined, "card app-confirm-card"), heading = el("div", undefined, "card-heading"), headingText = el("div");
|
|
headingText.append(el("span", options.danger ? t("warningLabel") : t("confirmationLabel"), `small-label${options.danger ? " status-error" : ""}`), el("h2", title));
|
|
heading.append(headingText); card.append(heading, el("p", String(message ?? ""), "app-confirm-message"));
|
|
const actions = el("div", undefined, "form-actions app-confirm-actions"), cancel = el("button", cancelLabel, "quiet"), accept = el("button", confirmLabel, options.danger ? "outline app-confirm-danger" : "primary");
|
|
cancel.type = "button"; accept.type = "button"; actions.append(cancel, accept); card.append(actions); dialog.append(card); document.body.append(dialog);
|
|
let settled = false;
|
|
const finish = value => { if (settled) return; settled = true; if (dialog.open) dialog.close(); dialog.remove(); resolve(value); };
|
|
cancel.addEventListener("click", () => finish(false)); accept.addEventListener("click", () => finish(true));
|
|
dialog.addEventListener("cancel", event => { event.preventDefault(); finish(false); });
|
|
dialog.addEventListener("close", () => finish(false));
|
|
dialog.showModal(); cancel.focus();
|
|
});
|
|
}
|
|
function apiArea(path) {
|
|
const section = path.match(/^\/api\/config\/([^/?]+)/)?.[1];
|
|
if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security", "user-agent-blocking": "security" })[section] || "configuration";
|
|
if (path === "/api/config") return "configuration";
|
|
if (path.startsWith("/api/traffic")) return "traffic";
|
|
if (path.startsWith("/api/cache")) return "cache";
|
|
if (path.startsWith("/api/tunnels")) return "overview";
|
|
if (path.startsWith("/api/transform")) return "transform";
|
|
if (path.startsWith("/api/certificates") || path.startsWith("/api/acme")) return "tls";
|
|
if (path.startsWith("/api/certificate-api")) return "settings";
|
|
if (path.startsWith("/api/docker")) return "dockerVhosts";
|
|
if (path.startsWith("/api/file-logs")) return "filelogs";
|
|
if (path.startsWith("/api/logs")) return "logging";
|
|
if (path.startsWith("/api/audit") || path.startsWith("/api/preferences") || path.startsWith("/api/auth/password") || path.startsWith("/api/auth/logout")) return "security";
|
|
return null;
|
|
}
|
|
function apiErrorMessage(path, response, value) {
|
|
const code = typeof value?.error === "string" ? value.error : "";
|
|
const translated = code ? t(code) : "";
|
|
const detail = response.status === 409 ? t("stale") : (translated && translated !== code ? translated : (code || `HTTP ${response.status}`));
|
|
const area = apiArea(path);
|
|
return area ? t("errorContext").replace("{area}", t(area)).replace("{message}", detail) : detail;
|
|
}
|
|
new MutationObserver(records => { if (!records.some(r => r.target.tagName === "DIALOG")) return; const node = $("toast"); if (!node.hidden) raiseToast(); }).observe(document.body, { subtree: true, attributes: true, attributeFilter: ["open"] });
|
|
async function api(path, options = {}) {
|
|
const started = performance.now(), trackLatency = path.startsWith("/api/");
|
|
try {
|
|
const response = await fetch(path, { ...options, cache: "no-store", credentials: "same-origin", headers: { "Content-Type": "application/json", "X-RustProxy-Request": "1", ...(csrf ? { "X-CSRF-Token": csrf } : {}), ...options.headers } });
|
|
const text = await response.text(); let value; try { value = JSON.parse(text); } catch { value = { error: text }; }
|
|
if (!response.ok) { if (response.status === 401 && authenticated) lock(); const error = new Error(apiErrorMessage(path, response, value)); error.status = response.status; error.code = value.error; throw error; } return value;
|
|
} finally {
|
|
if (trackLatency) { lastApiLatencyMs = performance.now() - started; if (latestStatus) renderFooter(latestStatus); }
|
|
}
|
|
}
|
|
function setPanel(name, navigate = true) {
|
|
if (name === "security") { setPanel("settings", navigate); window.rustProxySettings?.select("security"); return; }
|
|
if (!Object.hasOwn(PANEL_PATHS, name)) name = "overview";
|
|
const previousPanel = currentPanel; if (name !== "overview" && previousPanel === "overview") window.rustProxyInlineLayout?.cancel?.();
|
|
currentPanel = name;
|
|
$("overview-refresh").hidden = name !== "overview";
|
|
$("overview-samples-toolbar").hidden = name !== "overview";
|
|
for (const id of ["overview-layout-open", "overview-layout-unlock"]) if ($(id)) $(id).hidden = name !== "overview";
|
|
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
|
|
document.querySelectorAll(".panel").forEach(e => e.hidden = e.id !== `panel-${name}`);
|
|
$("page-label").textContent = t(name); $("page-title").textContent = t(`${name}Title`); $("page-description").textContent = t(`${name}Description`);
|
|
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
|
|
if (name === "web-server" && latestStatus) window.rustProxyWebServer?.renderLive?.(latestStatus, false);
|
|
if (name === "settings") window.rustProxySettings?.syncLocation?.();
|
|
window.rustProxyRefresh?.setPanel(name);
|
|
if (name === "overview" && previousPanel !== name && typeof latestStatus !== "undefined" && latestStatus) { window.rustProxyStreamDashboard?.render?.(latestStatus, null, false); window.rustProxyBackendDashboard?.render?.(latestStatus, null, false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
|
|
if (name === "docker-vhosts" && previousPanel !== name && authenticated && envelope) void window.rustProxyRefresh?.manual("docker");
|
|
if (name === "tls") window.rustProxyAcme?.syncLocation?.();
|
|
if (name === "filelogs") void window.rustProxyFileLogs?.refresh();
|
|
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) {
|
|
let seconds = Math.max(0, Math.floor(Number(value) || 0));
|
|
if (seconds < 60) return `${seconds}s`;
|
|
const units = [[365 * 86400, "y"], [86400, "d"], [3600, "h"], [60, "m"]], parts = [];
|
|
for (const [size, suffix] of units) {
|
|
const count = Math.floor(seconds / size);
|
|
if (count) { parts.push(`${count}${suffix}`); seconds %= size; }
|
|
if (parts.length === 2) break;
|
|
}
|
|
return parts.join(" ");
|
|
}
|
|
function rateBytes(value) { value = Math.max(0, Number(value) || 0); if (value === 0) return "0.0 KiB"; value /= 1024; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { if (value < 1024 || unit === "TiB") { if (unit === "KiB" && value < 0.1) return "<0.1 KiB"; const digits = value < 10 ? 1 : 0; return `${Number(value.toFixed(digits))} ${unit}`; } value /= 1024; } }
|
|
function el(tag, text, cls) { const e = document.createElement(tag); if (text !== undefined) e.textContent = text; if (cls) e.className = cls; return e; }
|
|
function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
|
|
const label = input?.closest("label"); if (!label) return () => { };
|
|
label.classList.add("vh-enable-control", "vh-wide");
|
|
let hint = label.querySelector(".vh-enable-hint");
|
|
if (!hint) { hint = el("small", undefined, "vh-enable-hint"); label.append(hint); }
|
|
hint.dataset.i18n = hintKey; hint.textContent = t(hintKey);
|
|
let state = label.querySelector(".vh-enable-state");
|
|
if (!state) { state = el("strong", undefined, "vh-enable-state"); label.append(state); }
|
|
const sync = () => {
|
|
const key = input.checked ? "vhEnabled" : "vhDisabled";
|
|
label.classList.toggle("is-disabled", !input.checked); label.classList.toggle("is-enabled", input.checked);
|
|
state.dataset.i18n = key; state.textContent = t(key);
|
|
};
|
|
input.addEventListener("change", sync); sync(); return sync;
|
|
}
|
|
window.rustProxyEnableControl = enhanceEnabledControl;
|
|
function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; }
|
|
function redrawOverviewHistoryCharts() {
|
|
const connectionPeak = Math.max(1, ...connectionOpenSamples, ...connectionExchangeSamples);
|
|
const connectionPoints = values => values.map((value, index) => `${index * 300 / Math.max(1, values.length - 1)},${95 - value / connectionPeak * 85}`).join(" ");
|
|
$("connection-open-sparkline").setAttribute("points", connectionPoints(connectionOpenSamples));
|
|
$("connection-exchange-sparkline").setAttribute("points", connectionPoints(connectionExchangeSamples));
|
|
const peak = Math.max(1, ...samples, ...transformSamples);
|
|
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
|
|
$("sparkline").setAttribute("points", points(samples));
|
|
$("sparkline-h1").setAttribute("points", points(protocolSamples.h1));
|
|
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
|
|
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
|
|
$("sparkline-transform").setAttribute("points", points(transformSamples));
|
|
for (const key of ["h1", "h2", "h3"]) {
|
|
$("http-protocol-" + key).setAttribute("points", sparklinePoints(protocolSamples[key], 300, 60, 5));
|
|
$("ws-protocol-" + key).setAttribute("points", sparklinePoints(websocketProtocolSamples[key], 300, 60, 5));
|
|
}
|
|
$("chart-peak").textContent = `max ${Math.max(0, ...samples).toFixed(1)} req/s`;
|
|
}
|
|
window.rustProxyOverviewSamples.onChange(() => {
|
|
for (const history of [samples, transformSamples, connectionOpenSamples, connectionExchangeSamples,
|
|
...Object.values(protocolSamples), ...Object.values(websocketProtocolSamples)]) {
|
|
window.rustProxyOverviewSamples.trim(history);
|
|
}
|
|
redrawOverviewHistoryCharts();
|
|
});
|
|
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;
|
|
$("metric-ws-h2").textContent = m.websocket_h2_active || 0;
|
|
$("metric-ws-h3").textContent = m.websocket_h3_active || 0;
|
|
$("metric-ws-total").textContent = (m.websocket_total || 0).toLocaleString(locale());
|
|
$("metric-h2").textContent = m.http2_requests || 0; $("metric-h2-active").textContent = m.http2_active_connections || 0; $("metric-tls-errors").textContent = m.tls_handshake_errors || 0;
|
|
$("metric-requests").textContent = m.requests.toLocaleString(locale()); $("metric-active").textContent = m.active; $("metric-bytes").textContent = bytes(m.bytes_in + m.bytes_out); $("metric-bytes-down").textContent = bytes(m.bytes_out); $("metric-bytes-up").textContent = bytes(m.bytes_in); $("metric-errors").textContent = m.errors;
|
|
const openConnections = Math.max(0, Number(m.active_connections) || 0), activeExchanges = Math.max(0, Number(m.active) || 0);
|
|
connectionOpenSamples.push(openConnections); connectionOpenSamples = connectionOpenSamples.slice(-window.rustProxyOverviewSamples.getLimit());
|
|
connectionExchangeSamples.push(activeExchanges); connectionExchangeSamples = connectionExchangeSamples.slice(-window.rustProxyOverviewSamples.getLimit());
|
|
$("connection-current").textContent = fmt(openConnections); $("connection-open-value").textContent = fmt(openConnections); $("connection-exchange-value").textContent = fmt(activeExchanges); $("connection-backends").textContent = fmt(s.backends);
|
|
const now = performance.now();
|
|
const transformed = s.transform_metrics || {};
|
|
const counters = { total: Number(m.requests) || 0, h1: Number(m.http1_requests) || 0, h2: Number(m.http2_requests) || 0, h3: Number(m.http3_requests) || 0, transform: (Number(transformed.applied) || 0) + (Number(transformed.cache_hits) || 0) };
|
|
const keys = ["total", "h1", "h2", "h3", "transform"]; const rateSample = rustProxyRateSampling.difference(previous, keys.map(key => counters[key]), now, rustProxyRateSampling.cadence("overview_seconds"));
|
|
const pausedRate = previous !== null && rateSample.deltas === null; previous = rateSample.next;
|
|
const rates = Object.fromEntries(keys.map((key, i) => [key, rateSample.deltas ? rateSample.deltas[i] / rateSample.seconds : 0]));
|
|
if (!pausedRate) {
|
|
samples.push(rates.total); samples = samples.slice(-window.rustProxyOverviewSamples.getLimit());
|
|
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-window.rustProxyOverviewSamples.getLimit()); }
|
|
}
|
|
websocketProtocolSamples.h1.push(Number(m.websocket_h1_active) || 0); websocketProtocolSamples.h1 = websocketProtocolSamples.h1.slice(-window.rustProxyOverviewSamples.getLimit());
|
|
websocketProtocolSamples.h2.push(Number(m.websocket_h2_active) || 0); websocketProtocolSamples.h2 = websocketProtocolSamples.h2.slice(-window.rustProxyOverviewSamples.getLimit());
|
|
websocketProtocolSamples.h3.push(Number(m.websocket_h3_active) || 0); websocketProtocolSamples.h3 = websocketProtocolSamples.h3.slice(-window.rustProxyOverviewSamples.getLimit());
|
|
if (!pausedRate) { transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-window.rustProxyOverviewSamples.getLimit()); }
|
|
redrawOverviewHistoryCharts();
|
|
const rateText = key => pausedRate ? "-" : `${rates[key].toFixed(1)}/s`;
|
|
$("request-rate").textContent = pausedRate ? "- req/s" : `${rates.total.toFixed(1)} req/s`;
|
|
$("rate-total").textContent = rateText("total"); $("rate-h1").textContent = rateText("h1"); $("rate-h2").textContent = rateText("h2"); $("rate-h3").textContent = rateText("h3"); $("rate-transform").textContent = rateText("transform");
|
|
$("metric-http-proto-total").textContent = rateText("total"); $("metric-http-proto-h1").textContent = rateText("h1");
|
|
$("metric-http-proto-h2").textContent = rateText("h2"); $("metric-http-proto-h3").textContent = rateText("h3");
|
|
const wsTotalActive = (Number(m.websocket_h1_active) || 0) + (Number(m.websocket_h2_active) || 0) + (Number(m.websocket_h3_active) || 0);
|
|
$("metric-ws-proto-total").textContent = wsTotalActive.toLocaleString(locale());
|
|
$("metric-ws-proto-h1").textContent = (Number(m.websocket_h1_active) || 0).toLocaleString(locale());
|
|
$("metric-ws-proto-h2").textContent = (Number(m.websocket_h2_active) || 0).toLocaleString(locale());
|
|
$("metric-ws-proto-h3").textContent = (Number(m.websocket_h3_active) || 0).toLocaleString(locale());
|
|
renderFooter(s); renderDiagnostics(s, true, systemSample); window.rustProxyVhosts?.renderStatus(s); window.rustProxyStreams?.renderStatus(s); window.rustProxyStreamDashboard?.render(s); window.rustProxyBackendDashboard?.render(s); window.rustProxyExtraOverview?.render(s); window.rustProxyWideOverview?.render(s); window.rustProxyWebOverview?.render(s); window.rustProxyWebServer?.renderLive?.(s);
|
|
}
|
|
function renderFooter(s) {
|
|
const apiLatency = lastApiLatencyMs == null ? "API -" : `API ${lastApiLatencyMs.toFixed(lastApiLatencyMs < 10 ? 1 : 0)} ms`;
|
|
const footer = $("footer-status");
|
|
footer.replaceChildren(document.createTextNode(`${apiLatency} / ${t("logsDropped")}: ${s.logs_dropped}`));
|
|
const update = s.software_update;
|
|
if (update?.update_available && update.latest_version) {
|
|
footer.append(document.createTextNode(" / "));
|
|
const label = `${t("updateAvailable")}: ${update.latest_version}`;
|
|
if (update.repository_url) {
|
|
const link = el("a", label, "footer-update");
|
|
link.href = update.repository_url;
|
|
link.target = "_blank";
|
|
link.rel = "noopener noreferrer";
|
|
footer.append(link);
|
|
} else {
|
|
footer.append(document.createTextNode(label));
|
|
}
|
|
}
|
|
}
|
|
let lastLogs = null;
|
|
function renderLogs(data) { lastLogs = data; window.rustProxyLogs?.render(data); }
|
|
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 || 3;
|
|
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;
|
|
try {
|
|
const value = await api("/api/certificates");
|
|
if (!authenticated || epoch !== authEpoch) return;
|
|
certificateInventory = new Map((value.certificates || []).map(cert => [cert.id, cert]));
|
|
} catch (e) {
|
|
if (e.status !== 401 && authenticated && epoch === authEpoch) certificateInventory = new Map();
|
|
}
|
|
}
|
|
async function loadConfig() { const epoch = authEpoch; const value = await api("/api/config"); if (!authenticated || epoch !== authEpoch) return; envelope = value; await refreshCertificateInventory(); if (!authenticated || epoch !== authEpoch) return; renderConfig(); }
|
|
function rulesText(acl) { return (acl?.rules || []).map(r => `${r.action} ${r.network}`).join("\n"); }
|
|
function normalizeAclRuleText(value) {
|
|
return String(value ?? "").normalize("NFKC")
|
|
.replace(/[\u200B-\u200D\u2060\uFEFF]/g, "")
|
|
.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, " ");
|
|
}
|
|
function parseRules(text, target = null) {
|
|
return normalizeAclRuleText(text).split(/\r?\n/).map((value, index) => ({ line: value.trim(), number: index + 1 })).filter(item => item.line).map(item => {
|
|
const match = item.line.match(/^(allow|deny)\s+(\S+)$/i);
|
|
if (!match) throw fieldError(target, `${t("ruleSyntaxLine")} ${item.number}: ${item.line}`);
|
|
return { action: match[1].toLowerCase(), network: match[2] };
|
|
});
|
|
}
|
|
function renderConfig() {
|
|
if (!envelope) return; window.rustProxyCachePanel?.configure(); renderCertificates(); renderSettings(); window.rustProxyPages?.render(); window.rustProxyAclProfiles?.render(); window.rustProxyVhosts?.render(); window.rustProxyWebServer?.render(); window.rustProxyDocker?.render(); window.rustProxyRedirects?.render(); window.rustProxyNotFound?.render(); window.rustProxyStreams?.render(); const config = envelope.config; if (!settingsDirty.has("config-editor")) $("config-editor").value = JSON.stringify(config, null, 2);
|
|
$("host-policy-list").replaceChildren(...(config.host_policies || []).map((policy, index) => { const box = el("div", undefined, "entity"); box.append(el("span", policy.hosts.join(", "), "entity-title"), el("p", `${t("aclDefault")}: ${t(policy.acl.default || "deny")}\n${rulesText(policy.acl)}`, "entity-details")); const buttons = el("div", undefined, "entity-actions"); buttons.append(action(t("edit"), () => { $("policy-hosts").value = policy.hosts.join(", "); $("policy-default").value = policy.acl.default || "deny"; $("policy-rules").value = rulesText(policy.acl); const editor = $("security-policy-editor"); if (editor) { editor.open = true; editor.scrollIntoView({ block: "nearest" }); } }), action(t("remove"), async () => { if (await appConfirm(t("confirmDelete"), { danger: true })) { const next = structuredClone(config); next.host_policies.splice(index, 1); await saveSection("host-policies", next.host_policies); } })); box.append(buttons); return box; }));
|
|
if (!config.host_policies?.length) $("host-policy-list").append(el("p", t("noPolicies"), "empty"));
|
|
if ($("security-policy-count")) $("security-policy-count").textContent = String((config.host_policies || []).length);
|
|
const uaPolicy = config.user_agent_blocking || {};
|
|
if ($("global-ua-block-enabled")) $("global-ua-block-enabled").checked = uaPolicy.enabled === true;
|
|
if ($("global-ua-block-patterns")) $("global-ua-block-patterns").value = (uaPolicy.patterns || []).join("\n");
|
|
}
|
|
async function saveRequest(path, payload) { if (busy) { toast(t("busy"), true); return false; } if (!authenticated || !envelope) return false; busy = true; const epoch = authEpoch; try { const value = await api(path, { method: "PUT", body: JSON.stringify(payload) }); if (!authenticated || epoch !== authEpoch) return false; envelope = value; if (path === "/api/config" || path === "/api/config/tls") await refreshCertificateInventory(); if (!authenticated || epoch !== authEpoch || !envelope) return false; renderConfig(); toast(t("saved")); await refresh(true); return true; } catch (e) { toast(e.message, true); if (e.status === 401) lock(); return false; } finally { busy = false; } }
|
|
async function save(config) { return saveRequest("/api/config", { expected_revision: envelope?.revision, config }); }
|
|
async function saveSection(section, value) { return saveRequest(`/api/config/${section}`, { expected_revision: envelope?.revision, value }); }
|
|
function syncAuthForm() { $("setup-fields").hidden = !setupRequired; $("confirm-field").hidden = !setupRequired; $("setup-token").required = setupRequired; $("password-confirm").required = setupRequired; $("password").minLength = setupRequired ? 12 : 1; $("password").autocomplete = setupRequired ? "new-password" : "current-password"; $("auth-title").textContent = t(setupRequired ? "setupTitle" : "signIn"); $("login-submit").textContent = t(setupRequired ? "createAndSignIn" : "signIn"); $("auth-badge").textContent = setupRequired ? "SETUP" : "LOCAL"; }
|
|
function renderSession() { $("session-user").textContent = sessionInfo?.username || ""; $("session-expiry").textContent = sessionInfo ? t("expires") + formatDateTime(sessionInfo.expires_at * 1000) : ""; }
|
|
function authReturnDestination() {
|
|
if (typeof location === "undefined") return "/";
|
|
if (location.pathname === "/logging") return "/settings?tab=logging";
|
|
const allowed = new Set([...Object.values(PANEL_PATHS), "/configuration"]);
|
|
const path = allowed.has(location.pathname) ? location.pathname : "/";
|
|
if (path !== "/settings") return path;
|
|
const tab = window.rustProxySettings?.current?.() || new URLSearchParams(location.search).get("tab");
|
|
return tab ? `/settings?tab=${encodeURIComponent(tab)}` : "/settings";
|
|
}
|
|
function lock(notify = true) {
|
|
window.rustProxyInlineLayout?.cancel?.();
|
|
window.rustProxyRefresh?.stop();
|
|
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
|
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); window.rustProxyDrafts?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); window.rustProxyStreamDashboard?.reset(); window.rustProxyBackendDashboard?.reset(); window.rustProxyExtraOverview?.reset(); window.rustProxyWideOverview?.reset(); window.rustProxyWebOverview?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; websocketProtocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
|
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
|
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
|
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
|
document.querySelectorAll("dialog").forEach(dialog => { dialog.close(); dialog.querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => input.value = ""); });
|
|
renderSession(); syncAuthForm();
|
|
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
|
|
}
|
|
|
|
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();
|
|
document.documentElement.classList.remove("app-loading");
|
|
document.documentElement.classList.remove("app-startup-failed");
|
|
rustProxyRefresh.start();
|
|
const preferences = await loadAdminPreferences();
|
|
if (!authenticated || epoch !== authEpoch) return false;
|
|
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);
|
|
for (let attempt = 0; attempt < 3 && authenticated && epoch === authEpoch; attempt++) {
|
|
try { await loadConfig(); break; }
|
|
catch (error) {
|
|
if (!authenticated || epoch !== authEpoch || error?.status === 401) return false;
|
|
if (attempt === 2) { showStartupFailure(error); return false; }
|
|
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 400));
|
|
}
|
|
}
|
|
if (!authenticated || epoch !== authEpoch) return false;
|
|
void refresh(true).catch(error => {
|
|
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
|
|
});
|
|
return 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); } }
|
|
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
|
|
let tunnelsLoading = false;
|
|
async function loadTunnels(context = null) {
|
|
if (!authenticated || tunnelsLoading) return;
|
|
tunnelsLoading = true;
|
|
const epoch = authEpoch;
|
|
try {
|
|
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)]) {
|
|
const td = el("td", String(value)); td.title = String(value); tr.append(td);
|
|
}
|
|
const actions = el("td");
|
|
const kill = el("button", t("tunnelKill"), "quiet"); kill.type = "button";
|
|
kill.addEventListener("click", async () => {
|
|
if (!(await appConfirm(t("tunnelKillConfirm"), { danger: true }))) return;
|
|
kill.disabled = true; tunnelMessage();
|
|
try {
|
|
const reason = $("tunnel-kill-reason")?.value.trim() || t("tunnelKillReasonDefault");
|
|
await api(`/api/tunnels/${encodeURIComponent(item.id)}/kill`, { method: "POST", body: JSON.stringify({ reason }) });
|
|
tunnelMessage(t("tunnelKilled"));
|
|
await loadTunnels();
|
|
} catch (error) { tunnelMessage(error.message, true); }
|
|
finally { kill.disabled = false; }
|
|
});
|
|
actions.append(kill); tr.append(actions); return tr;
|
|
});
|
|
$("tunnels-rows").replaceChildren(...rows); $("tunnels-empty").hidden = rows.length > 0;
|
|
} catch (error) { if (error?.name !== "AbortError" && authenticated && epoch === authEpoch && $("tunnels-dialog")?.open) tunnelMessage(error.message, true); }
|
|
finally { tunnelsLoading = false; }
|
|
}
|
|
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(); });
|
|
$("about-close").addEventListener("click", () => $("about-dialog").close());
|
|
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");
|
|
function setMobileMenu(open) {
|
|
if (!mobileMenuToggle || !dashboardSidebar) return;
|
|
dashboardSidebar.classList.toggle("mobile-nav-open", open);
|
|
mobileMenuToggle.setAttribute("aria-expanded", String(open));
|
|
}
|
|
mobileMenuToggle?.addEventListener("click", event => { event.stopPropagation(); setMobileMenu(!dashboardSidebar.classList.contains("mobile-nav-open")); });
|
|
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => { setPanel(button.dataset.panel); setMobileMenu(false); }));
|
|
document.querySelector('.brand[href="/"]')?.addEventListener("click", event => {
|
|
if (!authenticated || event.defaultPrevented || event.button !== 0 ||
|
|
event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
|
event.preventDefault();
|
|
setPanel("overview");
|
|
setMobileMenu(false);
|
|
});
|
|
document.addEventListener("click", event => { if (dashboardSidebar?.classList.contains("mobile-nav-open") && !dashboardSidebar.contains(event.target)) setMobileMenu(false); });
|
|
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-ua-block-form")?.addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const patterns = [...new Set($("global-ua-block-patterns").value.split(/\r?\n/).map(value => value.trim()).filter(Boolean))]; await saveSection("user-agent-blocking", { enabled: $("global-ua-block-enabled").checked, patterns }); }); } catch (e) { toast(e.message, true); } });
|
|
$("password-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; try { await withFormSubmitState(form, event.submitter, async () => { if ($("new-password").value !== $("new-password-confirm").value) throw fieldError("new-password-confirm", t("passwordMismatch")); await api("/api/auth/password", { method: "POST", body: JSON.stringify({ current_password: $("current-password").value, new_password: $("new-password").value }) }); lock(); toast(t("passwordChanged")); }); } catch (e) { toast(e.message, true); } });
|
|
$("revoke-sessions").addEventListener("click", async () => { if (!(await appConfirm(t("confirmRevoke"), { danger: true }))) return; try { await api("/api/auth/logout-all", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { toast(e.message, true); } });
|
|
$("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 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); } });
|
|
$("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); });
|