This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 09:30:21 +02:00
1 parent 1dea7f55c8
commit c819f0a335
151 files changed
+7610 -4264

No files matched your search

+13 -2
View File
@@ -390,6 +390,8 @@ function stopTunnelRefresh() { if (tunnelRefreshTimer) clearInterval(tunnelRefre
function startTunnelRefresh() { stopTunnelRefresh(); tunnelRefreshTimer = setInterval(() => { if ($("tunnels-dialog")?.open) void loadTunnels(); }, 2000); }
$("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());
$("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);
@@ -398,8 +400,17 @@ $("ui-theme").addEventListener("change", async () => {
catch (error) { applyAdminTheme(previousTheme); if (error.status !== 401) toast(error.message, true); }
finally { select.disabled = false; }
});
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => setPanel(button.dataset.panel)));
window.addEventListener("popstate", () => setPanel(panelFromLocation(), false));
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.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); } });
$("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); } });
+1 -1
View File
@@ -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.25.4";
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.5";
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
}
+3 -2
View File
@@ -176,6 +176,7 @@ main {
background: #101923;
color: #fff
}
.docs-tabs {
display: flex;
gap: 8px;
@@ -312,7 +313,7 @@ main {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace
}
.markdown-reader :not(pre) > code {
.markdown-reader :not(pre)>code {
padding: 2px 4px;
background: #edf1f4
}
@@ -342,4 +343,4 @@ main {
.markdown-reader {
padding: 18px
}
}
}
+14 -6
View File
@@ -1,5 +1,6 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -10,10 +11,12 @@
<script defer src="/docs/swagger-ui-bundle.js"></script>
<script defer src="/docs/init.js"></script>
</head>
<body>
<header class="docs-bar"><a class="docs-name" href="/">RustProxy</a>
<h1 data-i18n="docsTitle">Documentation</h1>
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout" data-i18n="docsLogout">Sign out</button>
<a href="/" data-i18n="docsPanel">Back to panel</a><button type="button" id="docs-logout"
data-i18n="docsLogout">Sign out</button>
</header>
<main>
<section class="docs-session">
@@ -22,14 +25,16 @@
</section>
<p id="docs-error" role="alert"></p>
<nav class="docs-tabs" aria-label="Documentation sections">
<button type="button" id="docs-tab-reader" class="active" data-docs-tab="reader" data-i18n="docsReader">Reader</button>
<button type="button" id="docs-tab-reader" class="active" data-docs-tab="reader"
data-i18n="docsReader">Reader</button>
<button type="button" id="docs-tab-api" data-docs-tab="api" data-i18n="docsApi">API</button>
</nav>
<section id="docs-reader-pane" class="docs-pane">
<div class="reader-layout">
<aside class="reader-sidebar">
<label for="docs-reader-search" data-i18n="docsReaderSearch">Search documentation</label>
<input id="docs-reader-search" type="search" autocomplete="off" data-i18n-placeholder="docsReaderSearchPlaceholder" placeholder="Search title or path">
<input id="docs-reader-search" type="search" autocomplete="off"
data-i18n-placeholder="docsReaderSearchPlaceholder" placeholder="Search title or path">
<div id="docs-reader-list" class="reader-list"></div>
</aside>
<article id="docs-reader-content" class="markdown-reader" aria-live="polite"></article>
@@ -37,12 +42,15 @@
</section>
<section id="docs-api-pane" class="docs-pane" hidden>
<section class="docs-api-controls">
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write operations</span></label>
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed here.</p>
<label><input type="checkbox" id="docs-writes"><span data-i18n="docsWrites">Enable write
operations</span></label>
<p data-i18n="docsReadOnly">Read-only execution by default. SSE streams and login/setup are not executed
here.</p>
</section>
<div id="swagger-ui"></div>
<p><a href="/docs/licenses.txt" data-i18n="docsLicenses">Swagger UI licenses</a></p>
</section>
</main>
</body>
</html>
</html>
+577 -248
View File
File diff suppressed because it is too large. Load diff
+3 -3
View File
@@ -126,7 +126,7 @@ curl --fail --show-error --request POST \
catch (error) { $("certificate-api-status").textContent = error.message; }
finally { certificateApiLoading = false; }
}
const defaults = { enabled: false, listen: "127.0.0.1:9191", path: "/metrics", allowed_cidrs: ["127.0.0.0/8", "::1/128"], bearer_token_env: null, allow_trusted_network_without_token: false, include_system: true, include_vhosts: true, include_backends: true, include_docker_vhosts: true, include_cache_domains: true, include_streams: true, include_logging: true };
const defaults = { enabled: false, listen: "127.0.0.1:9191", path: "/metrics", allowed_cidrs: ["127.0.0.0/8", "::1/128"], bearer_token_env: null, allow_trusted_network_without_token: false, include_system: true, include_vhosts: true, include_traffic: true, include_backends: true, include_docker_vhosts: true, include_cache_domains: true, include_streams: true, include_logging: true };
function example() {
const addr = $("prom-listen").value.trim(), path = $("prom-path").value.trim(), env = $("prom-token-env").value.trim();
$("prom-scrape-example").textContent = `scrape_configs:\n - job_name: rustproxy\n metrics_path: ${JSON.stringify(path)}\n static_configs:\n - targets: [${JSON.stringify(addr)}]${env ? "\n authorization:\n credentials_file: /etc/prometheus/rustproxy.token" : ""}`;
@@ -136,7 +136,7 @@ curl --fail --show-error --request POST \
if (!settingsDirty.has("prometheus-form")) {
$("prom-enabled").checked = p.enabled; $("prom-listen").value = p.listen; $("prom-path").value = p.path;
$("prom-cidrs").value = p.allowed_cidrs.join("\n"); $("prom-token-env").value = p.bearer_token_env || ""; $("prom-trusted-no-token").checked = !!p.allow_trusted_network_without_token;
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
for (const [id, key] of [["system", "include_system"], ["vhosts", "include_vhosts"], ["traffic", "include_traffic"], ["backends", "include_backends"], ["docker-vhosts", "include_docker_vhosts"], ["domains", "include_cache_domains"], ["streams", "include_streams"], ["logging", "include_logging"]]) $(`prom-${id}`).checked = p[key];
}
const status = latestStatus?.prometheus;
$("prom-status").textContent = status?.restart_required ? t("restartPending") : status?.active?.enabled ? `${t("prometheusActive")}: ${status.active.listen}${p.path}` : t("prometheusDisabled"); example();
@@ -170,7 +170,7 @@ curl --fail --show-error --request POST \
bindSettingsForm("prometheus-form", next => {
next.prometheus = {
enabled: $("prom-enabled").checked, listen: $("prom-listen").value.trim(), path: $("prom-path").value.trim(), allowed_cidrs: $("prom-cidrs").value.split(/[\n,]/).map(v => v.trim()).filter(Boolean), bearer_token_env: $("prom-token-env").value.trim() || null, allow_trusted_network_without_token: $("prom-trusted-no-token").checked,
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
include_system: $("prom-system").checked, include_vhosts: $("prom-vhosts").checked, include_traffic: $("prom-traffic").checked, include_backends: $("prom-backends").checked, include_docker_vhosts: $("prom-docker-vhosts").checked, include_cache_domains: $("prom-domains").checked, include_streams: $("prom-streams").checked, include_logging: $("prom-logging").checked
};
}, "prometheus");
bindSettingsForm("administration-form", next => {
+230 -28
View File
@@ -3781,7 +3781,7 @@ body[data-density="dense"] .stat strong {
max-width: none
}
.settings-page>.edit-form + .edit-form {
.settings-page>.edit-form+.edit-form {
margin-top: var(--ui-gap)
}
@@ -3920,7 +3920,8 @@ body[data-density="dense"] .stat strong {
dialog.vhost-dialog,
dialog.logs-dialog,
dialog.audit-dialog,
dialog.certificate-dialog {
dialog.certificate-dialog,
dialog.config-history-dialog {
border-radius: 0;
background: var(--bg);
color: var(--text);
@@ -4692,11 +4693,16 @@ dialog .window-heading {
}
@media(max-width:1100px) {
.traffic-wide-chart { grid-column: span 1 }
.traffic-wide-chart {
grid-column: span 1
}
}
@media(max-width:650px) {
.traffic-bar-row { grid-template-columns: minmax(88px, 1.2fr) minmax(90px, 2fr) minmax(60px, auto); gap: 7px }
.traffic-bar-row {
grid-template-columns: minmax(88px, 1.2fr) minmax(90px, 2fr) minmax(60px, auto);
gap: 7px
}
}
.logging-grid label {
@@ -4720,6 +4726,7 @@ dialog .window-heading {
}
@media(max-width:720px) {
.traffic-chart-grid,
.logging-grid {
grid-template-columns: 1fr
@@ -6778,7 +6785,6 @@ body[data-density="compact"] .page-footer {
}
}
/* Certificates workspace (0.20.0): use the same tab language as Settings. */
#panel-tls .certificate-workspace-heading {
align-items: flex-start;
margin-bottom: 12px
@@ -7031,7 +7037,6 @@ body[data-density="compact"] .page-footer {
}
}
/* Inline operation feedback mirrors the login feedback pattern and remains local to the action. */
.operation-message:empty {
display: none;
}
@@ -7330,7 +7335,6 @@ body[data-density="compact"] .page-footer {
white-space: normal;
}
/* 0.24.3 dashboard refinements */
.payload-split {
display: flex !important;
align-items: center;
@@ -7515,7 +7519,7 @@ body[data-density="compact"] .page-footer {
color: var(--notice-text);
font-weight: 600;
}
/* 0.24.8 cache occupancy charts and compact memory-accounting help */
.mini-heading-actions {
display: inline-flex;
align-items: center;
@@ -7607,7 +7611,6 @@ body[data-density="compact"] .page-footer {
color: var(--transform-chart);
}
/* Web Service editor mirrors the vhost tabbed configuration layout. */
.web-service-pages {
min-width: 0;
}
@@ -7628,21 +7631,10 @@ body[data-density="compact"] .page-footer {
}
.config-history-dialog {
color: var(--text);
background: var(--bg);
border: 1px solid var(--line);
padding: 0;
width: min(1180px, calc(100vw - 24px));
max-width: calc(100vw - 24px);
max-height: calc(100dvh - 28px);
overflow: hidden;
border-radius: 7px;
box-shadow: 0 18px 70px var(--shadow-strong);
}
.config-history-dialog::backdrop {
background: var(--shadow-strong);
backdrop-filter: blur(2px);
}
.config-history-layout {
@@ -7667,7 +7659,6 @@ body[data-density="compact"] .page-footer {
.config-history-month {
border: 1px solid var(--line);
border-radius: 6px;
padding: 9px;
background: var(--panel);
}
@@ -7695,7 +7686,6 @@ body[data-density="compact"] .page-footer {
min-height: 34px;
padding: 4px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--muted);
font: 10px var(--mono);
@@ -7769,7 +7759,6 @@ button.config-history-day.is-selected {
.config-history-change {
border: 1px solid var(--line);
border-radius: 5px;
padding: 8px;
background: var(--panel);
}
@@ -7797,9 +7786,222 @@ button.config-history-day.is-selected {
}
@media (max-width: 860px) {
.config-history-layout { grid-template-columns: 1fr; overflow: auto; }
.config-history-browser, .config-history-detail { overflow: visible; }
.config-history-change-values { grid-template-columns: 1fr; }
.config-history-detail-head { flex-direction: column; }
.config-history-detail-actions { align-items: flex-start; text-align: left; }
.config-history-layout {
grid-template-columns: 1fr;
overflow: auto;
}
.config-history-browser,
.config-history-detail {
overflow: visible;
}
.config-history-change-values {
grid-template-columns: 1fr;
}
.config-history-detail-head {
flex-direction: column;
}
.config-history-detail-actions {
align-items: flex-start;
text-align: left;
}
}
.mobile-menu-toggle {
display: none;
}
@media(max-width:850px) {
.sidebar {
position: sticky;
top: 0;
z-index: 60;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px 12px;
padding: 10px 12px;
}
.sidebar .brand {
min-width: 0;
}
.mobile-menu-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 9px;
min-height: 36px;
padding: 7px 10px;
border: 1px solid var(--line);
background: transparent;
color: var(--muted);
font: 600 11px var(--mono);
}
.mobile-menu-toggle:hover,
.mobile-menu-toggle[aria-expanded="true"] {
color: var(--text);
border-color: var(--border-hover);
background: var(--nav-active-bg);
}
.mobile-menu-icon {
display: block;
flex: 0 0 auto;
width: 15px;
height: 1px;
background: currentColor;
box-shadow: 0 -5px currentColor, 0 5px currentColor;
}
.sidebar .main-navigation {
grid-column: 1 / -1;
display: none;
width: 100%;
margin: 0;
padding-top: 10px;
border-top: 1px solid var(--line);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px;
}
.sidebar.mobile-nav-open .main-navigation {
display: grid;
}
.sidebar .main-navigation .nav {
width: 100%;
min-width: 0;
justify-content: flex-start;
gap: 8px;
padding: 9px 10px;
border: 1px solid transparent;
border-left: 2px solid transparent;
text-align: left;
}
.sidebar .main-navigation .nav.active {
border-color: var(--line);
border-left-color: var(--green);
background: var(--nav-active-bg);
}
}
@media(max-width:600px) {
.sidebar .main-navigation .nav span {
display: none;
}
}
.about-dialog {
width: min(560px, calc(100vw - 24px));
overflow: hidden;
}
.about-dialog .window-heading button {
margin-inline-start: auto;
}
.about-card {
display: grid;
gap: 16px;
margin: 0;
padding: 18px;
border: 0;
box-shadow: none;
}
.about-product {
display: flex;
align-items: center;
gap: 12px;
}
.about-product>div {
display: grid;
gap: 4px;
}
.about-product strong {
font: 600 16px var(--mono);
}
.about-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1px;
margin: 0;
border: 1px solid var(--line);
background: var(--line);
}
.about-meta>div {
min-width: 0;
padding: 10px 12px;
background: var(--panel);
}
.about-meta dt {
margin-bottom: 5px;
color: var(--muted);
font: 9px var(--mono);
letter-spacing: .8px;
text-transform: uppercase;
}
.about-meta dd {
margin: 0;
overflow-wrap: anywhere;
}
.about-card>p {
margin: 0;
color: var(--muted);
line-height: 1.65;
}
.about-repository {
justify-content: space-between;
gap: 12px;
width: 100%;
white-space: normal;
text-align: left;
}
.about-repository span:last-child {
color: var(--muted);
font: 10px var(--mono);
overflow-wrap: anywhere;
}
@media(max-width:620px) {
.topbar {
height: auto;
min-height: 46px;
padding: 7px 0;
}
.topbar .breadcrumb {
display: none;
}
.top-actions {
width: 100%;
flex-wrap: wrap;
justify-content: flex-end;
}
.about-meta {
grid-template-columns: 1fr;
}
.about-repository {
align-items: flex-start;
flex-direction: column;
}
}
+2 -2
View File
@@ -38,12 +38,12 @@
liveChartLimit = LIVE_CHART_LIMITS.has(parsed) ? parsed : 60;
for (const key of Object.keys(liveChartSamples)) liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit);
if ($("transform-live-samples")) $("transform-live-samples").value = String(liveChartLimit);
if (persist) { try { localStorage.setItem(LIVE_CHART_STORAGE_KEY, String(liveChartLimit)); } catch {} }
if (persist) { try { localStorage.setItem(LIVE_CHART_STORAGE_KEY, String(liveChartLimit)); } catch { } }
redrawLiveChart();
}
function initializeLiveChartLimit() {
let saved = 60;
try { saved = Number(localStorage.getItem(LIVE_CHART_STORAGE_KEY) || 60); } catch {}
try { saved = Number(localStorage.getItem(LIVE_CHART_STORAGE_KEY) || 60); } catch { }
setLiveChartLimit(saved, false);
}
function renderLiveChart(counters) {