This commit is contained in:
Mateusz Gruszczyński committed 2026-10-12 11:22:42 +02:00
1 parent 68ada6680d
commit 180a0e4877
56 files changed
+9866 -205

No files matched your search

+4
View File
@@ -151,6 +151,7 @@ async function api(path, options = {}) {
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;
currentPanel = name;
$("overview-refresh").hidden = name !== "overview";
document.querySelectorAll(".nav").forEach(e => e.classList.toggle("active", e.dataset.panel === name));
@@ -159,6 +160,9 @@ function setPanel(name, navigate = true) {
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
if (name === "settings") window.rustProxySettings?.syncLocation?.();
window.rustProxyRefresh?.setPanel(name);
// Read the cached Docker status on each visit, independent of the Live-view toggle.
// This is GET /api/docker, not the POST that triggers an agent poll.
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(); });
+10 -4
View File
@@ -10,6 +10,7 @@
const HIDDEN_KEY = "rustproxy.docker.hidden-vhosts";
const cfg = () => envelope?.config?.docker_agents || { agents: [] };
const obs = id => observations.find(item => item.agent_id === id);
const hasUnobservedAgent = () => (cfg().agents || []).some(agent => agent.enabled !== false && !obs(agent.id));
const firstListener = () => envelope?.config?.listeners?.endpoints?.find(item => item.enabled !== false)?.id || "http";
const defaultHealth = () => ({ mode: "tcp", path: "/", host: null, interval_ms: 5000, timeout_ms: 2000, slow_response_ms: 1500, fall: 3, rise: 2, expected_status_min: 200, expected_status_max: 399 });
const defaultPassiveHealth = () => ({ enabled: false, count_connect_errors: true, count_timeouts: true, count_io_errors: true, count_http_5xx: true, count_slow_responses: false, fall: null, rise: null });
@@ -115,7 +116,7 @@
const https = String(agent?.endpoint || "").toLowerCase().startsWith("https://");
const pinned = !!agent?.tls_fingerprint_sha256;
if (!https) return { key: "dockerTlsPlain", cls: "warn", detail: t("dockerTlsPlainHelp") };
if (pinned) return { key: "dockerTlsPinned", cls: "live", detail: `${t("dockerTlsFingerprint")}: ${prettyFingerprint(agent.tls_fingerprint_sha256)}` };
if (pinned) return { key: "dockerTlsPinned", cls: state?.ok ? "live" : "", detail: `${t("dockerTlsFingerprint")}: ${prettyFingerprint(agent.tls_fingerprint_sha256)}` };
if (state?.ok) return { key: "dockerTlsPublicCa", cls: "live", detail: t("dockerTlsPublicCaHelp") };
return { key: "dockerTlsPublicCa", cls: "", detail: t("dockerTlsPublicCaHelp") };
}
@@ -326,7 +327,10 @@
const state = obs(agent.id), box = el("article", undefined, "card docker-agent-card"); box.dataset.agentId = agent.id; box.dataset.searchText = `${agent.id} ${agent.name || ""} ${agent.base_domain || ""}`.toLowerCase(); const head = el("div", undefined, "card-heading"), left = el("div"); left.append(el("span", agent.id, "small-label"), el("h2", agent.name));
const badges = el("div", undefined, "entity-actions"), connection = el("span", state?.ok ? t("dockerConnected") : state ? t("dockerError") : t("dockerWaiting"), `pill ${state?.ok ? "live" : ""}`), sec = securityInfo(agent, state); badges.append(el("span", t(sec.key), `pill ${sec.cls}`.trim()), connection); head.append(left, badges); box.append(head);
if (!agent.enabled) { box.append(el("p", t("dockerAgentDisabled"), "fine")); target.append(box); continue; }
if (!state?.ok) { const error = el("div", undefined, "docker-agent-error"); error.append(el("b", t("dockerAgentConnectionFailed")), el("p", state?.error || t("dockerNoObservation"), "fine"), el("small", sec.detail, "fine")); box.append(error); }
// No observation is the normal state while the first background poll is in flight.
// A configured TLS fingerprint alone is not proof that an agent was contacted.
if (!state) box.append(el("p", t("dockerNoObservation"), "fine"));
else if (!state.ok) { const error = el("div", undefined, "docker-agent-error"); error.append(el("b", t("dockerAgentConnectionFailed")), el("p", state.error || t("dockerUnknownPollError"), "fine")); box.append(error); }
if (!state?.inventory) { const rows = el("div", undefined, "docker-service-list"); for (const item of agent.assignments || []) rows.append(assignmentRow(agent, null, null, item, true)); if (!rows.children.length) rows.append(el("p", t("dockerNoPublishedPorts"), "empty")); box.append(rows); target.append(box); continue; }
const inv = state.inventory, defaultIp = agent.default_target_ip || agent.target_ip_override || t("dockerTargetAuto"), serviceCount = (inv.services || []).length, overview = el("div", undefined, "docker-agent-overview"); box.dataset.searchText += ` ${inv.hostname || ""}`.toLowerCase();
overview.append(el("span", `${t("dockerHost")}: ${inv.hostname}`), el("span", `${t("dockerServices")}: ${serviceCount}`), el("span", `${t("dockerLastChecked")}: ${checkedAgo(state.checked_at_unix_ms)}`), el("span", `*.${agent.base_domain}`)); box.append(overview);
@@ -406,8 +410,10 @@
$("docker-agents-form")?.addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { if (await saveSection("docker-agents", collectSettings())) { setSettingsDirty("docker-agents-form", false); await refresh(true); } }); } catch (error) { toast(error.message, true); } });
rustProxyRefresh.register("docker", {
interval: () => 5,
active: () => currentPanel === "docker-vhosts" && liveEnabled() && canLiveRender(),
// Even with local Live view off, observe the first startup result.
// Once every enabled agent has a first result, stop unless Live is enabled.
active: () => currentPanel === "docker-vhosts" && canLiveRender() && (liveEnabled() || hasUnobservedAgent()),
run: async context => refresh(false, context)
});
window.rustProxyDocker = { render() { syncLiveSwitch(); renderSettings(); renderDiscovery(); }, refresh, applyPreferences, reset() { observations = []; pools = []; poolRoutes = []; poolIndex = new Map(); showHidden = false; listPrefs = { sort: "service", descending: false }; hiddenVhosts = loadHidden(); if ($("docker-search")) $("docker-search").value = ""; if ($("docker-vhost-sort")) $("docker-vhost-sort").value = listPrefs.sort; syncSortDirection(); $("docker-discovery-list")?.replaceChildren(); $("docker-agent-settings-list")?.replaceChildren(); syncLiveSwitch(); applyViewFilters(); } };
window.rustProxyDocker = { render() { syncLiveSwitch(); renderSettings(); renderDiscovery(); if (currentPanel === "docker-vhosts" && authenticated) { rustProxyRefresh.sync(); void rustProxyRefresh.manual("docker"); } }, refresh, applyPreferences, reset() { observations = []; pools = []; poolRoutes = []; poolIndex = new Map(); showHidden = false; listPrefs = { sort: "service", descending: false }; hiddenVhosts = loadHidden(); if ($("docker-search")) $("docker-search").value = ""; if ($("docker-vhost-sort")) $("docker-vhost-sort").value = listPrefs.sort; syncSortDirection(); $("docker-discovery-list")?.replaceChildren(); $("docker-agent-settings-list")?.replaceChildren(); syncLiveSwitch(); applyViewFilters(); } };
})();
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>400 - Bad request - RustProxy</title>
<style>
body {
margin: 12vh auto;
padding: 24px;
max-width: 620px;
background: #131615;
color: #e5e9e6;
font: 15px/1.6 system-ui, sans-serif
}
h1 {
font-size: 24px;
margin: 0 0 8px
}
p {
margin: 0;
color: #b9c3bc
}
</style>
</head>
<body>
<main>
<h1>400 - Bad request</h1>
<p>The request could not be processed because it is invalid.</p>
</main>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>408 - Request timeout - RustProxy</title>
<style>
body {
margin: 12vh auto;
padding: 24px;
max-width: 620px;
background: #131615;
color: #e5e9e6;
font: 15px/1.6 system-ui, sans-serif
}
h1 {
font-size: 24px;
margin: 0 0 8px
}
p {
margin: 0;
color: #b9c3bc
}
</style>
</head>
<body>
<main>
<h1>408 - Request timeout</h1>
<p>The request was not completed in time. Please try again.</p>
</main>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>410 - Gone - RustProxy</title>
<style>
body {
margin: 12vh auto;
padding: 24px;
max-width: 620px;
background: #131615;
color: #e5e9e6;
font: 15px/1.6 system-ui, sans-serif
}
h1 {
font-size: 24px;
margin: 0 0 8px
}
p {
margin: 0;
color: #b9c3bc
}
</style>
</head>
<body>
<main>
<h1>410 - Gone</h1>
<p>The requested resource is no longer available.</p>
</main>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>413 - Payload too large - RustProxy</title>
<style>
body {
margin: 12vh auto;
padding: 24px;
max-width: 620px;
background: #131615;
color: #e5e9e6;
font: 15px/1.6 system-ui, sans-serif
}
h1 {
font-size: 24px;
margin: 0 0 8px
}
p {
margin: 0;
color: #b9c3bc
}
</style>
</head>
<body>
<main>
<h1>413 - Payload too large</h1>
<p>The request body exceeds the allowed size.</p>
</main>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>500 - Internal server error - RustProxy</title>
<style>
body {
margin: 12vh auto;
padding: 24px;
max-width: 620px;
background: #131615;
color: #e5e9e6;
font: 15px/1.6 system-ui, sans-serif
}
h1 {
font-size: 24px;
margin: 0 0 8px
}
p {
margin: 0;
color: #b9c3bc
}
</style>
</head>
<body>
<main>
<h1>500 - Internal server error</h1>
<p>The server encountered an unexpected error.</p>
</main>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>502 - Bad gateway - RustProxy</title>
<style>
body {
margin: 12vh auto;
padding: 24px;
max-width: 620px;
background: #131615;
color: #e5e9e6;
font: 15px/1.6 system-ui, sans-serif
}
h1 {
font-size: 24px;
margin: 0 0 8px
}
p {
margin: 0;
color: #b9c3bc
}
</style>
</head>
<body>
<main>
<h1>502 - Bad gateway</h1>
<p>The upstream service returned an invalid response.</p>
</main>
</body>
</html>
+25 -5
View File
@@ -11,6 +11,7 @@
]);
let availableColumns = new Set(defaultColumnIds);
let visibleColumns = new Set(defaultColumnIds);
let sortBy = "time", sortDescending = true, pendingRefresh = false;
const CONSOLIDATION_STORAGE_KEY = "rp-file-logs-consolidate-vhosts";
@@ -146,8 +147,22 @@
function syncHeaders() {
const row = $("file-logs-table-head").querySelector("tr");
const headers = orderedColumns().filter(id => visibleColumns.has(id)).map(id => {
const header = el("th", columnLabel(id)); header.dataset.fileLogColumn = id;
const key = specialLabels.get(id); if (key) header.dataset.i18n = key;
const header = el("th"); header.dataset.fileLogColumn = id;
header.setAttribute("aria-sort", sortBy === id ? (sortDescending ? "descending" : "ascending") : "none");
const button = el("button", undefined, "file-log-sort-button"); button.type = "button";
const caption = el("span", columnLabel(id));
const key = specialLabels.get(id); if (key) caption.dataset.i18n = key;
const arrow = el("span", sortBy === id ? (sortDescending ? "↓" : "↑") : "↕", "file-log-sort-indicator");
arrow.setAttribute("aria-hidden", "true");
const willDescend = sortBy === id ? !sortDescending : false;
button.title = `${t(willDescend ? "fileLogsSortDescending" : "fileLogsSortAscending")}: ${columnLabel(id)}`;
button.setAttribute("aria-label", button.title);
button.addEventListener("click", () => {
sortDescending = sortBy === id ? !sortDescending : false;
sortBy = id; selectedIndex = null; offset = 0;
syncHeaders(); void savePreferences(); void refresh(true);
});
button.append(caption, arrow); header.append(button);
return header;
});
row.replaceChildren(...headers);
@@ -172,6 +187,8 @@
? preferences.file_log_columns.filter(id => typeof id === "string" && id.length <= 128)
: [];
visibleColumns = new Set(selected.length ? selected : defaultColumnIds);
sortBy = typeof preferences?.file_log_sort === "string" && /^[a-z0-9_.-]{1,128}$/i.test(preferences.file_log_sort) ? preferences.file_log_sort : "time";
sortDescending = preferences?.file_log_descending !== false;
for (const id of visibleColumns) availableColumns.add(id);
preferencesLoaded = true;
syncColumnPicker(); syncHeaders(); renderRows();
@@ -184,7 +201,7 @@
do {
preferenceSaveQueued = false;
const selected = orderedColumns().filter(id => visibleColumns.has(id));
const preferences = await window.rustProxyPreferences.update({ file_log_columns: selected });
const preferences = await window.rustProxyPreferences.update({ file_log_columns: selected, file_log_sort: sortBy, file_log_descending: sortDescending });
if (!preferenceSaveQueued) applyPreferences(preferences);
} while (preferenceSaveQueued);
} catch (error) { toast(error.message, true); }
@@ -267,12 +284,14 @@
params.set("aggregate_min_count", String(Math.max(2, Number($("file-log-aggregate-min")?.value) || 2)));
params.set("aggregate_limit", String(Math.max(1, Number($("file-log-aggregate-limit")?.value) || 50)));
}
params.set("sort_by", sortBy); params.set("sort_descending", String(sortDescending));
params.set("offset", String(offset)); params.set("limit", String(currentLimit()));
return params.toString();
}
async function refresh(reset = false) {
if (!authenticated || busy) return;
if (!authenticated) return;
if (reset) offset = 0;
if (busy) { pendingRefresh = true; return; }
busy = true; $("file-logs-refresh").disabled = true;
try {
if (!inventory) await loadInventory();
@@ -289,10 +308,11 @@
$("file-logs-prev").disabled = offset === 0;
$("file-logs-next").disabled = offset + currentLimit() >= lastTotal;
} catch (error) { toast(error.message, true); }
finally { busy = false; $("file-logs-refresh").disabled = false; }
finally { busy = false; $("file-logs-refresh").disabled = false; if (pendingRefresh) { pendingRefresh = false; void refresh(); } }
}
function reset() {
inventory = null; offset = 0; lastTotal = 0; currentEntries = []; selectedIndex = null; aggregateField = "";
sortBy = "time"; sortDescending = true; pendingRefresh = false;
preferencesLoaded = false; preferenceSaveRunning = false; preferenceSaveQueued = false;
availableColumns = new Set(defaultColumnIds); visibleColumns = new Set(defaultColumnIds);
sourceSelect().replaceChildren(Object.assign(el("option", t("fileLogsAllSources")), { value: "all" }));
+37
View File
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>200 - OK - RustProxy</title>
<style>
body {
margin: 12vh auto;
padding: 24px;
max-width: 620px;
background: #131615;
color: #e5e9e6;
font: 15px/1.6 system-ui, sans-serif
}
h1 {
font-size: 24px;
margin: 0 0 8px
}
p {
margin: 0;
color: #b9c3bc
}
</style>
</head>
<body>
<main>
<h1>200 - OK</h1>
<p>The request completed successfully.</p>
</main>
</body>
</html>
+19
View File
@@ -8692,6 +8692,25 @@ button.config-history-day.is-selected {
box-shadow: 0 1px 0 var(--line);
}
/* Stored Logs sorting is performed by the bounded server scan before pagination. */
.file-logs-table th .file-log-sort-button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 2px;
font: inherit;
font-weight: inherit;
color: inherit;
border: 0;
background: transparent;
cursor: pointer;
text-align: left;
}
.file-logs-table th .file-log-sort-button:hover { color: var(--accent); }
.file-logs-table th[aria-sort="ascending"] .file-log-sort-button,
.file-logs-table th[aria-sort="descending"] .file-log-sort-button { color: var(--accent); }
.file-logs-table .file-log-sort-indicator { font-size: 11px; opacity: .8; }
.file-logs-table tbody tr:not(.file-log-detail-row) {
cursor: pointer;
}