v0.26.3
This commit is contained in:
1 parent
32e6875980
commit
cf25423dde
68 files changed
+2314
-422
No files matched your search
@@ -1,7 +1,9 @@
|
||||
let configHistoryEntries = [], configHistorySelectedRevision = null;
|
||||
function historyDate(value, options = {}) {
|
||||
try { return new Intl.DateTimeFormat(locale(), options).format(new Date(Number(value) * 1000)); }
|
||||
catch { return new Date(Number(value) * 1000).toLocaleString(); }
|
||||
const effective = { ...options };
|
||||
if (Object.hasOwn(effective, "timeStyle") || Object.hasOwn(effective, "hour")) effective.hour12 = (adminPreferences?.time_format || "24h") === "12h";
|
||||
try { return new Intl.DateTimeFormat(locale(), effective).format(new Date(Number(value) * 1000)); }
|
||||
catch { return formatDateTime(Number(value) * 1000); }
|
||||
}
|
||||
function historyDayKey(value) {
|
||||
const date = new Date(Number(value) * 1000);
|
||||
|
||||
+11
-57
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let language = null;
|
||||
try { language = localStorage.getItem("rp-language"); } catch { }
|
||||
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) {
|
||||
@@ -20,47 +20,6 @@ function recoverInvalidOrigin(error) {
|
||||
function clearInvalidOriginRecovery() {
|
||||
try { sessionStorage.removeItem(ORIGIN_RECOVERY_KEY); } catch { recoverInvalidOrigin.attempted = false; }
|
||||
}
|
||||
const ADMIN_THEMES = new Set(["dark", "light"]);
|
||||
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve();
|
||||
function normalizeAdminTheme(value) { return ADMIN_THEMES.has(value) ? value : "dark"; }
|
||||
function applyAdminTheme(value, cache = true) {
|
||||
const theme = normalizeAdminTheme(value);
|
||||
document.documentElement.dataset.theme = theme;
|
||||
const select = $("ui-theme"); if (select) select.value = theme;
|
||||
if (cache) { try { localStorage.setItem("rp-theme", theme); } catch { } }
|
||||
return theme;
|
||||
}
|
||||
try { applyAdminTheme(localStorage.getItem("rp-theme") || "dark", false); } catch { applyAdminTheme("dark", false); }
|
||||
async function loadAdminPreferences(force = false) {
|
||||
if (!authenticated) return adminPreferences;
|
||||
if (adminPreferences && !force) return adminPreferences;
|
||||
if (adminPreferencesLoading) return adminPreferencesLoading;
|
||||
const epoch = authEpoch;
|
||||
adminPreferencesLoading = (async () => {
|
||||
const value = await api("/api/preferences");
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
adminPreferences = { ...value, theme: applyAdminTheme(value?.theme) };
|
||||
return adminPreferences;
|
||||
})();
|
||||
try { return await adminPreferencesLoading; } finally { adminPreferencesLoading = null; }
|
||||
}
|
||||
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();
|
||||
if (!current || !authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
|
||||
const next = { ...current, ...patch }; next.theme = normalizeAdminTheme(next.theme);
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
adminPreferences = { ...saved, theme: applyAdminTheme(saved?.theme) };
|
||||
return adminPreferences;
|
||||
});
|
||||
adminPreferencesWrite = task.catch(() => undefined);
|
||||
return task;
|
||||
}
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
|
||||
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme };
|
||||
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;
|
||||
@@ -83,8 +42,9 @@ async function loadLanguages() {
|
||||
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;
|
||||
$("language").replaceChildren(...packs.map(pack => { const option = el("option", pack.meta.native_name); option.value = pack.id; return option; }));
|
||||
$("language").value = language;
|
||||
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;
|
||||
@@ -94,7 +54,9 @@ function translate() {
|
||||
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)));
|
||||
$("language").value = language; setPanel(currentPanel, false); syncAuthForm(); renderSession();
|
||||
$("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); } if (lastLogs) renderLogs(lastLogs);
|
||||
syncSettingsDirtyIndicators();
|
||||
@@ -332,7 +294,7 @@ async function saveRequest(path, payload) { if (busy) { toast(t("busy"), true);
|
||||
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") + new Date(sessionInfo.expires_at * 1000).toLocaleString(locale()) : ""; }
|
||||
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";
|
||||
@@ -344,7 +306,7 @@ function authReturnDestination() {
|
||||
}
|
||||
function lock(notify = true) {
|
||||
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(); $("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("");
|
||||
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(); }
|
||||
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
||||
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
||||
@@ -356,7 +318,7 @@ function lock(notify = true) {
|
||||
}
|
||||
|
||||
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); 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 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 [new Date(item.time * 1000).toLocaleString(locale()), 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); } }
|
||||
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() {
|
||||
@@ -394,14 +356,6 @@ $("login-form").addEventListener("submit", async event => { event.preventDefault
|
||||
$("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());
|
||||
$("language").addEventListener("change", () => { language = $("language").value; try { localStorage.setItem("rp-language", language); } catch { } translate(); });
|
||||
$("ui-theme").addEventListener("change", async () => {
|
||||
const select = $("ui-theme"), previousTheme = adminPreferences?.theme || document.documentElement.dataset.theme || "dark", nextTheme = normalizeAdminTheme(select.value);
|
||||
applyAdminTheme(nextTheme); select.disabled = true;
|
||||
try { await updateAdminPreferences({ theme: nextTheme }); }
|
||||
catch (error) { applyAdminTheme(previousTheme); if (error.status !== 401) toast(error.message, true); }
|
||||
finally { select.disabled = false; }
|
||||
});
|
||||
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");
|
||||
function setMobileMenu(open) {
|
||||
if (!mobileMenuToggle || !dashboardSidebar) return;
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
const ADMIN_THEMES = new Set(["dark", "light"]);
|
||||
const ADMIN_TIME_FORMATS = new Set(["24h", "12h"]);
|
||||
const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool"]);
|
||||
const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]);
|
||||
const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]);
|
||||
const ADMIN_DASHBOARD_WIDGETS = Object.freeze(["requests", "active", "bandwidth", "errors", "cache", "memory", "websocket", "http2", "http3", "tls", "http_protocol", "websocket_protocol"]);
|
||||
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
||||
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
||||
button_style: "normal", table_density: "normal", dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [],
|
||||
});
|
||||
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();
|
||||
|
||||
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"; }
|
||||
function normalizeAdminUiScale(value) { const numeric = Number(value); return Number.isInteger(numeric) ? Math.min(125, Math.max(70, numeric)) : 100; }
|
||||
function normalizeAdminTimeFormat(value) { return ADMIN_TIME_FORMATS.has(value) ? value : "24h"; }
|
||||
function normalizeAdminChartStyle(value) { return ADMIN_CHART_STYLES.has(value) ? value : "default"; }
|
||||
function normalizeAdminButtonStyle(value) { return ADMIN_BUTTON_STYLES.has(value) ? value : "normal"; }
|
||||
function normalizeAdminDensity(value, fallback = "normal") { return ADMIN_DENSITIES.has(value) ? value : fallback; }
|
||||
function normalizeDashboardWidgets(value) {
|
||||
const result = [], seen = new Set();
|
||||
for (const id of Array.isArray(value) ? value : []) if (ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id)) { seen.add(id); result.push(id); }
|
||||
for (const id of ADMIN_DASHBOARD_WIDGETS) if (!seen.has(id)) result.push(id);
|
||||
return result;
|
||||
}
|
||||
function normalizeHiddenDashboardWidgets(value) {
|
||||
const seen = new Set();
|
||||
return (Array.isArray(value) ? value : []).filter(id => ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id) && seen.add(id));
|
||||
}
|
||||
function browserLanguage() {
|
||||
const packs = [...languagePacks.values()], candidates = (navigator.languages || [navigator.language]).filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase());
|
||||
return candidates.map(value => packs.find(pack => pack.meta.locale.toLowerCase() === value || pack.id.replaceAll("_", "-").toLowerCase() === value) || packs.find(pack => pack.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean)?.id || defaultLanguage;
|
||||
}
|
||||
function applyAdminTheme(value, cache = true) {
|
||||
const theme = normalizeAdminTheme(value); document.documentElement.dataset.theme = theme;
|
||||
const select = $("ui-theme"); if (select) select.value = theme;
|
||||
if (cache) { try { localStorage.setItem("rp-theme", theme); } catch { } }
|
||||
return theme;
|
||||
}
|
||||
function applyAdminLanguage(value, cache = true, rerender = true) {
|
||||
const preference = normalizeAdminLanguage(value); languagePreference = preference;
|
||||
if (languagePacks.size) language = preference === "auto" ? browserLanguage() : (languagePacks.has(preference) ? preference : browserLanguage());
|
||||
const select = $("language"); if (select) select.value = preference === "auto" ? "auto" : language;
|
||||
if (cache) { try { localStorage.setItem("rp-language", preference); } catch { } }
|
||||
if (rerender && languagePacks.size) translate();
|
||||
return preference;
|
||||
}
|
||||
function applyAdminUiScale(value, cache = true) {
|
||||
const scale = normalizeAdminUiScale(value), root = document.documentElement, fieldScale = Math.min(1.1, Math.max(.9, 1 + (scale - 100) / 300));
|
||||
root.style.setProperty("--ui-scale", String(scale / 100));
|
||||
root.style.setProperty("--ui-field-font-size", `${(12 * fieldScale).toFixed(2)}px`);
|
||||
root.style.setProperty("--ui-field-pad-y", `${(6 * fieldScale).toFixed(2)}px`);
|
||||
root.style.setProperty("--ui-field-pad-x", `${(8 * fieldScale).toFixed(2)}px`);
|
||||
root.style.setProperty("--ui-field-min-height", `${(27 * fieldScale).toFixed(2)}px`);
|
||||
root.style.setProperty("--ui-check-size", `${(15 * fieldScale).toFixed(2)}px`);
|
||||
root.style.setProperty("--ui-range-height", `${(18 * fieldScale).toFixed(2)}px`);
|
||||
const slider = $("ui-scale"), output = $("ui-scale-value"); if (slider) slider.value = String(scale); if (output) output.textContent = `${scale}%`;
|
||||
if (cache) { try { localStorage.setItem("rp-ui-scale", String(scale)); } catch { } }
|
||||
return scale;
|
||||
}
|
||||
function applyAdminTimeFormat(value) { const format = normalizeAdminTimeFormat(value); const select = $("ui-time-format"); if (select) select.value = format; return format; }
|
||||
function applyAdminChartStyle(value) {
|
||||
const style = normalizeAdminChartStyle(value); document.documentElement.dataset.chartStyle = style;
|
||||
const select = $("ui-chart-style"); if (select) select.value = style; refreshChartStylePalette(style); return style;
|
||||
}
|
||||
function applyAdminButtonStyle(value) {
|
||||
const style = normalizeAdminButtonStyle(value); document.documentElement.dataset.buttonStyle = style;
|
||||
const select = $("ui-button-style"); if (select) select.value = style; return style;
|
||||
}
|
||||
function applyAdminTableDensity(value) {
|
||||
const density = normalizeAdminDensity(value); document.documentElement.dataset.tableDensity = density;
|
||||
const select = $("ui-table-density"); if (select) select.value = density; return density;
|
||||
}
|
||||
function applyAdminDashboardDensity(value) {
|
||||
const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = density;
|
||||
const select = $("ui-dashboard-density"); if (select) select.value = density; return density;
|
||||
}
|
||||
function applyDashboardLayout(order, hidden) {
|
||||
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden));
|
||||
const grid = document.querySelector("#panel-overview .stats-grid"); if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet] };
|
||||
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
|
||||
for (const id of normalizedOrder) { const node = nodes.get(id); if (!node) continue; node.hidden = hiddenSet.has(id); grid.append(node); }
|
||||
return { order: normalizedOrder, hidden: [...hiddenSet] };
|
||||
}
|
||||
function dashboardWidgetLabel(id) {
|
||||
const node = document.querySelector(`[data-dashboard-widget="${CSS.escape(id)}"]`);
|
||||
return node?.querySelector("[data-i18n]")?.textContent?.trim() || id;
|
||||
}
|
||||
function renderDashboardWidgetSettings() {
|
||||
const list = $("dashboard-widget-list"); if (!list) return;
|
||||
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = normalizeDashboardWidgets(current.dashboard_widgets), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
|
||||
const rows = order.map((id, index) => {
|
||||
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
|
||||
const visible = document.createElement("input"); visible.type = "checkbox"; visible.checked = !hidden.has(id); visible.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
|
||||
const label = el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label");
|
||||
const actions = el("span", undefined, "dashboard-widget-setting-actions");
|
||||
const up = el("button", "↑", "quiet"); up.type = "button"; up.disabled = index === 0; up.setAttribute("aria-label", t("moveUp"));
|
||||
const down = el("button", "↓", "quiet"); down.type = "button"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("moveDown"));
|
||||
visible.addEventListener("change", () => {
|
||||
const previous = [...hidden], next = new Set(hidden); if (visible.checked) next.delete(id); else next.add(id);
|
||||
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next]); renderDashboardWidgetSettings();
|
||||
void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, visible, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous); renderDashboardWidgetSettings(); });
|
||||
});
|
||||
const move = delta => {
|
||||
const previous = [...order], next = [...order], at = next.indexOf(id), target = at + delta; if (at < 0 || target < 0 || target >= next.length) return;
|
||||
[next[at], next[target]] = [next[target], next[at]]; if (adminPreferences) adminPreferences.dashboard_widgets = next;
|
||||
applyDashboardLayout(next, [...hidden]); renderDashboardWidgetSettings();
|
||||
void saveAppearancePreference({ dashboard_widgets: next }, null, () => { if (adminPreferences) adminPreferences.dashboard_widgets = previous; applyDashboardLayout(previous, [...hidden]); renderDashboardWidgetSettings(); });
|
||||
};
|
||||
up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(visible, label, actions); return row;
|
||||
});
|
||||
list.replaceChildren(...rows);
|
||||
}
|
||||
function refreshChartStylePalette(active = adminPreferences?.chart_style || "default") {
|
||||
const target = $("chart-style-palette"); if (!target || !languagePacks.size) return;
|
||||
const buttons = [...ADMIN_CHART_STYLES].map(style => {
|
||||
const button = el("button", undefined, "chart-style-choice"); button.type = "button"; button.dataset.chartPreset = style; button.classList.toggle("active", style === active); button.setAttribute("aria-pressed", String(style === active));
|
||||
const swatches = el("span", undefined, "chart-style-swatches"); for (let i = 1; i <= 4; i++) swatches.append(el("i"));
|
||||
button.append(swatches, el("span", t(CHART_STYLE_KEYS[style]))); button.addEventListener("click", () => { const select = $("ui-chart-style"); if (!select) return; select.value = style; select.dispatchEvent(new Event("change", { bubbles: true })); }); return button;
|
||||
});
|
||||
target.replaceChildren(...buttons);
|
||||
}
|
||||
function refreshAppearanceUi() { refreshChartStylePalette(); renderDashboardWidgetSettings(); 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) {
|
||||
if (!value) return value;
|
||||
const dashboard = applyDashboardLayout(value.dashboard_widgets, value.dashboard_hidden_widgets);
|
||||
const normalized = {
|
||||
...value,
|
||||
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;
|
||||
if (languagePacks.size) translate();
|
||||
refreshAppearanceUi(); return normalized;
|
||||
}
|
||||
try { applyAdminTheme(localStorage.getItem("rp-theme") || "dark", false); } catch { applyAdminTheme("dark", false); }
|
||||
try { applyAdminUiScale(localStorage.getItem("rp-ui-scale") || 100, false); } catch { applyAdminUiScale(100, false); }
|
||||
|
||||
async function loadAdminPreferences(force = false) {
|
||||
if (!authenticated) return adminPreferences;
|
||||
if (adminPreferences && !force) return adminPreferences;
|
||||
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 { return await adminPreferencesLoading; } finally { adminPreferencesLoading = null; }
|
||||
}
|
||||
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();
|
||||
if (!current || !authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
|
||||
const next = { ...current, ...patch };
|
||||
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);
|
||||
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
||||
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
||||
return applyAdminPreferences(saved);
|
||||
});
|
||||
adminPreferencesWrite = task.catch(() => undefined); return task;
|
||||
}
|
||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
|
||||
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme, refreshAppearanceUi };
|
||||
|
||||
async function saveAppearancePreference(patch, control, rollback) {
|
||||
if (control) control.disabled = true;
|
||||
try { await updateAdminPreferences(patch); }
|
||||
catch (error) { rollback?.(); if (error.status !== 401) toast(error.message, true); }
|
||||
finally { if (control) control.disabled = false; }
|
||||
}
|
||||
$("language")?.addEventListener("change", () => {
|
||||
const control = $("language"), previous = adminPreferences?.language || "auto", next = normalizeAdminLanguage(control.value);
|
||||
applyAdminLanguage(next); void saveAppearancePreference({ language: next }, control, () => applyAdminLanguage(previous));
|
||||
});
|
||||
$("ui-theme")?.addEventListener("change", () => {
|
||||
const control = $("ui-theme"), previous = adminPreferences?.theme || "dark", next = normalizeAdminTheme(control.value);
|
||||
applyAdminTheme(next); void saveAppearancePreference({ theme: next }, control, () => applyAdminTheme(previous));
|
||||
});
|
||||
$("ui-scale")?.addEventListener("input", () => applyAdminUiScale($("ui-scale").value));
|
||||
$("ui-scale")?.addEventListener("change", () => {
|
||||
const control = $("ui-scale"), previous = adminPreferences?.ui_scale || 100, next = normalizeAdminUiScale(control.value);
|
||||
void saveAppearancePreference({ ui_scale: next }, control, () => applyAdminUiScale(previous));
|
||||
});
|
||||
$("ui-time-format")?.addEventListener("change", () => {
|
||||
const control = $("ui-time-format"), previous = adminPreferences?.time_format || "24h", next = normalizeAdminTimeFormat(control.value);
|
||||
applyAdminTimeFormat(next); if (adminPreferences) adminPreferences.time_format = next; translate();
|
||||
void saveAppearancePreference({ time_format: next }, control, () => { if (adminPreferences) adminPreferences.time_format = previous; applyAdminTimeFormat(previous); translate(); });
|
||||
});
|
||||
$("ui-chart-style")?.addEventListener("change", () => {
|
||||
const control = $("ui-chart-style"), previous = adminPreferences?.chart_style || "default", next = normalizeAdminChartStyle(control.value);
|
||||
applyAdminChartStyle(next); if (adminPreferences) adminPreferences.chart_style = next; void saveAppearancePreference({ chart_style: next }, control, () => { if (adminPreferences) adminPreferences.chart_style = previous; applyAdminChartStyle(previous); });
|
||||
});
|
||||
$("ui-button-style")?.addEventListener("change", () => {
|
||||
const control = $("ui-button-style"), previous = adminPreferences?.button_style || "normal", next = normalizeAdminButtonStyle(control.value);
|
||||
applyAdminButtonStyle(next); if (adminPreferences) adminPreferences.button_style = next; void saveAppearancePreference({ button_style: next }, control, () => { if (adminPreferences) adminPreferences.button_style = previous; applyAdminButtonStyle(previous); });
|
||||
});
|
||||
$("ui-table-density")?.addEventListener("change", () => {
|
||||
const control = $("ui-table-density"), previous = adminPreferences?.table_density || "normal", next = normalizeAdminDensity(control.value);
|
||||
applyAdminTableDensity(next); if (adminPreferences) adminPreferences.table_density = next; void saveAppearancePreference({ table_density: next }, control, () => { if (adminPreferences) adminPreferences.table_density = previous; applyAdminTableDensity(previous); });
|
||||
});
|
||||
$("ui-dashboard-density")?.addEventListener("change", () => {
|
||||
const control = $("ui-dashboard-density"), previous = adminPreferences?.dashboard_density || "compact", next = normalizeAdminDensity(control.value, "compact");
|
||||
applyAdminDashboardDensity(next); if (adminPreferences) adminPreferences.dashboard_density = next; void saveAppearancePreference({ dashboard_density: next }, control, () => { if (adminPreferences) adminPreferences.dashboard_density = previous; applyAdminDashboardDensity(previous); });
|
||||
});
|
||||
async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") {
|
||||
if (control) control.disabled = true; let previous = null;
|
||||
try {
|
||||
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets] } : null;
|
||||
if (current) applyAdminPreferences({ ...current, ...patch }); await updateAdminPreferences(patch); toast(t(messageKey));
|
||||
} catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); }
|
||||
finally { if (control) control.disabled = false; }
|
||||
}
|
||||
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
|
||||
const patches = {
|
||||
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", button_style: "normal" }, charts: { chart_style: "default" }, tables: { table_density: "normal" },
|
||||
dashboard: { dashboard_density: "compact", dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [] },
|
||||
};
|
||||
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
||||
}));
|
||||
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [] }, $("appearance-reset"), "appearanceResetDone"));
|
||||
refreshAppearanceUi();
|
||||
@@ -62,7 +62,7 @@ function renderCertificates() {
|
||||
const details = el("details", undefined, "certificate-details");
|
||||
details.append(el("summary", t("certificateDetails")));
|
||||
const detailBody = el("div", undefined, "certificate-details-body");
|
||||
const stamp = value => Number(value) ? new Date(Number(value) * 1000).toLocaleString(locale()) : "-";
|
||||
const stamp = value => Number(value) ? formatDateTime(Number(value) * 1000) : "-";
|
||||
detailBody.append(
|
||||
el("p", `${t("certificateBindings")}: ${bindings.join(", ") || "-"}`, "entity-details"),
|
||||
el("p", `${t("certificateFiles")}: ${cert.cert_file} / ${cert.key_file}`, "entity-details"),
|
||||
@@ -304,7 +304,7 @@ function renderDiagnostics(status, appendSample = true) {
|
||||
setOptionalMetric("process-threads", d.process_threads != null, fmt(d.process_threads));
|
||||
const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||
setOptionalMetric("process-virtual-memory", virtualMemory != null, virtualMemory);
|
||||
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : new Date(Number(d.process_start_time_seconds) * 1000).toLocaleString(locale()));
|
||||
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000));
|
||||
setOptionalMetric("process-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds));
|
||||
setOptionalMetric("process-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes));
|
||||
const memory = status.memory_accounting || {};
|
||||
|
||||
@@ -6,7 +6,7 @@ function renderSettings() {
|
||||
if (!settingsDirty.has("response-form")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.2";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.3";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user