v0.25.7
This commit is contained in:
1 parent
e1a59f6e56
commit
d168ee2afa
49 files changed
+5608
-4680
No files matched your search
@@ -1,3 +1,87 @@
|
||||
const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
|
||||
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] });
|
||||
let poolTablePrefs = defaultPoolTablePrefs(), poolPreferenceSaveRunning = false, poolPreferenceSaveQueued = false;
|
||||
function normalizePoolTablePrefs(preferences) {
|
||||
const selected = Array.isArray(preferences?.backend_table_columns) ? preferences.backend_table_columns.filter(key => poolColumns.includes(key)) : [];
|
||||
return {
|
||||
sort: poolColumns.includes(preferences?.backend_table_sort) ? preferences.backend_table_sort : "vhost",
|
||||
descending: preferences?.backend_table_descending === true,
|
||||
columns: selected.length ? selected : [...poolColumns],
|
||||
};
|
||||
}
|
||||
function applyPoolPreferences(preferences) {
|
||||
poolTablePrefs = normalizePoolTablePrefs(preferences);
|
||||
const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort;
|
||||
syncPoolColumns(); syncPoolSortDirection(); applyPoolTableView();
|
||||
}
|
||||
async function savePoolTablePrefs() {
|
||||
if (!authenticated) return;
|
||||
poolPreferenceSaveQueued = true;
|
||||
if (poolPreferenceSaveRunning) return;
|
||||
poolPreferenceSaveRunning = true;
|
||||
try {
|
||||
while (poolPreferenceSaveQueued && authenticated) {
|
||||
poolPreferenceSaveQueued = false;
|
||||
const saved = await window.rustProxyPreferences.update({
|
||||
backend_table_columns: poolColumns.filter(key => poolTablePrefs.columns.includes(key)),
|
||||
backend_table_sort: poolTablePrefs.sort,
|
||||
backend_table_descending: poolTablePrefs.descending,
|
||||
});
|
||||
if (!authenticated) return;
|
||||
if (!poolPreferenceSaveQueued) poolTablePrefs = normalizePoolTablePrefs(saved);
|
||||
}
|
||||
syncPoolColumns(); syncPoolSortDirection();
|
||||
} catch (error) { if (error.status !== 401) toast(error.message, true); }
|
||||
finally { poolPreferenceSaveRunning = false; }
|
||||
}
|
||||
function syncPoolColumns() {
|
||||
const visible = new Set(poolTablePrefs.columns);
|
||||
document.querySelectorAll("[data-pool-column]").forEach(node => { node.hidden = !visible.has(node.dataset.poolColumn); });
|
||||
document.querySelectorAll("[data-pool-column-toggle]").forEach(input => { input.checked = visible.has(input.dataset.poolColumnToggle); });
|
||||
}
|
||||
function syncPoolSortDirection() {
|
||||
const control = $("pool-sort-direction"); if (!control) return;
|
||||
control.textContent = poolTablePrefs.descending ? "Z-A" : "A-Z";
|
||||
control.setAttribute("aria-label", t(poolTablePrefs.descending ? "backendSortDescending" : "backendSortAscending"));
|
||||
}
|
||||
function applyPoolTableView() {
|
||||
const body = $("pool-rows"); if (!body) return;
|
||||
const query = ($("pool-search")?.value || "").trim().toLowerCase();
|
||||
const key = $("pool-sort")?.value || poolTablePrefs.sort;
|
||||
const rows = [...body.querySelectorAll("tr")];
|
||||
rows.sort((left, right) => {
|
||||
const a = left.poolSort?.[key] ?? "", b = right.poolSort?.[key] ?? "";
|
||||
const result = typeof a === "number" && typeof b === "number" ? a - b : String(a).localeCompare(String(b), locale(), { numeric: true, sensitivity: "base" });
|
||||
return poolTablePrefs.descending ? -result : result;
|
||||
});
|
||||
body.replaceChildren(...rows);
|
||||
let visible = 0;
|
||||
for (const row of rows) { const match = !query || (row.poolSearchValue || "").includes(query); row.hidden = !match; if (match) visible++; }
|
||||
syncPoolColumns(); syncPoolSortDirection();
|
||||
const empty = $("pool-empty");
|
||||
if (empty) { empty.textContent = query && rows.length && !visible ? t("backendSearchEmpty") : t("vhHealthEmpty"); empty.hidden = visible > 0 || (!rows.length && !document.querySelector("#backend-diagnostics-card:not([hidden])")); }
|
||||
}
|
||||
function initPoolTableTools() {
|
||||
const sort = $("pool-sort"), direction = $("pool-sort-direction"), search = $("pool-search");
|
||||
if (sort) { sort.value = poolTablePrefs.sort; sort.addEventListener("change", () => { poolTablePrefs.sort = sort.value; void savePoolTablePrefs(); applyPoolTableView(); }); }
|
||||
direction?.addEventListener("click", () => { poolTablePrefs.descending = !poolTablePrefs.descending; void savePoolTablePrefs(); applyPoolTableView(); });
|
||||
search?.addEventListener("input", applyPoolTableView);
|
||||
document.querySelectorAll("[data-pool-column-toggle]").forEach(input => input.addEventListener("change", () => {
|
||||
const visible = new Set(poolTablePrefs.columns);
|
||||
if (input.checked) visible.add(input.dataset.poolColumnToggle); else visible.delete(input.dataset.poolColumnToggle);
|
||||
if (!visible.size) { input.checked = true; toast(t("backendColumnsRequired"), true); return; }
|
||||
poolTablePrefs.columns = poolColumns.filter(key => visible.has(key)); void savePoolTablePrefs(); syncPoolColumns();
|
||||
}));
|
||||
syncPoolColumns(); syncPoolSortDirection();
|
||||
}
|
||||
async function toggleVhostEnabled(vhost) {
|
||||
if (!envelope || busy) return;
|
||||
const next = structuredClone(envelope.config), target = (next.vhosts || []).find(item => item.id === vhost.id);
|
||||
if (!target) return;
|
||||
target.enabled = vhost.enabled === false;
|
||||
if (await saveSection("vhosts", next.vhosts || [])) render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!envelope) return;
|
||||
if (!initialized) load(defaults(), [], false);
|
||||
@@ -33,7 +117,9 @@
|
||||
const browseTls = configuredEndpoints.some(endpoint => endpoint.binding?.tls === true);
|
||||
const openButton = button("vhOpenBrowser", () => { if (browseHost) window.open(`${browseTls ? "https" : "http"}://${browseHost}/`, "_blank", "noopener,noreferrer"); });
|
||||
openButton.disabled = !browseHost || v.enabled === false;
|
||||
actions.append(openButton, button("edit", async () => { if (await allowDiscard()) load(structuredClone(v)); }), button("remove", async () => {
|
||||
const quickToggle = button(v.enabled === false ? "vhQuickEnable" : "vhQuickDisable", () => toggleVhostEnabled(v), `quiet vhost-quick-toggle ${v.enabled === false ? "is-enable" : "is-disable"}`);
|
||||
quickToggle.setAttribute("aria-pressed", String(v.enabled !== false));
|
||||
actions.append(quickToggle, openButton, button("edit", async () => { if (await allowDiscard()) load(structuredClone(v)); }), button("remove", async () => {
|
||||
if (busy || !(await appConfirm(t("vhDelete"), { danger: true }))) return;
|
||||
const next = structuredClone(envelope.config); next.vhosts = (next.vhosts || []).filter(item => item.id !== v.id);
|
||||
if (await saveSection("vhosts", next.vhosts || [])) { if (editing === v.id) load(); render(); }
|
||||
@@ -147,7 +233,9 @@
|
||||
const generationAgeText = generationAgeRaw == null || !Number.isFinite(generationAge) ? "-" : generationAge >= 3600 ? `${Math.floor(generationAge / 3600)}h ${Math.floor((generationAge % 3600) / 60)}m` : generationAge >= 60 ? `${Math.floor(generationAge / 60)}m ${Math.floor(generationAge % 60)}s` : `${Math.floor(generationAge)}s`;
|
||||
const row = el("tr"); if (vhostDisabled) row.classList.add("pool-vhost-disabled"); const poolDomain = status.backend_pool_domains?.[pool.id]; const poolLabel = poolDomain ? `${pool.id} (${poolDomain})` : pool.id; const cells = [poolLabel, `${member.id}${member.backup ? " / " + t("vhBackup") : ""}`, label, activeLimit, member.requests, member.errors,
|
||||
member.ewma_latency_ms > 0 ? `${member.ewma_latency_ms.toFixed(1)} ms` : "-", `${rateBytes(memberTraffic)}/s`, member.upstream_client_generation ?? "-", member.upstream_client_generation_requests ?? "-", generationAgeText, member.upstream_client_rotations_total ?? "-", lastFailure, lastRecovery];
|
||||
cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); cell.title = index === 2 ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : index === 12 ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (index === 2 && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (index === 12 && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row);
|
||||
const sortValues = { vhost: poolLabel.toLowerCase(), backend: String(member.id || "").toLowerCase(), status: label.toLowerCase(), active: Number(member.active) || 0, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic, generation: Number(member.upstream_client_generation) || 0, generationRequests: Number(member.upstream_client_generation_requests) || 0, generationAge: Number.isFinite(generationAge) ? generationAge : -1, rotations: Number(member.upstream_client_rotations_total) || 0, lastFailure: Number(member.last_failure_at_ms) || 0, lastRecovery: Number(member.last_recovered_ms) || 0 };
|
||||
row.poolSort = sortValues; row.poolSearchValue = [poolLabel, member.id, label, member.status, member.last_failure_class, member.last_failure_detail].filter(Boolean).join(" ").toLowerCase();
|
||||
cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); cell.dataset.poolColumn = poolColumns[index]; cell.title = index === 2 ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : index === 12 ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (index === 2 && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (index === 12 && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row);
|
||||
if (!vhostDisabled && member.status === "up") up++; active += Number(member.active) || 0; requests += Number(member.requests) || 0; errors += Number(member.errors) || 0; traffic += memberTraffic;
|
||||
const latency = Number(member.ewma_latency_ms || member.latency_ms) || 0; if (latency > 0) { const weight = Math.max(1, Number(member.requests) || 1); weightedLatency += latency * weight; latencyWeight += weight; }
|
||||
}
|
||||
@@ -190,9 +278,9 @@
|
||||
vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy")
|
||||
);
|
||||
$("pool-rows").replaceChildren(...rows);
|
||||
$("pool-empty").hidden = rows.length > 0 || !hasConfiguredProxySource;
|
||||
const backendDiagnosticsCard = $("backend-diagnostics-card");
|
||||
if (backendDiagnosticsCard) backendDiagnosticsCard.hidden = !hasConfiguredProxySource;
|
||||
applyPoolTableView();
|
||||
document.querySelectorAll("[data-health-for]").forEach(node => node.textContent = summaries.get(node.dataset.healthFor) || t("vhHealthEmpty"));
|
||||
const fill = (attr, format) => document.querySelectorAll(`[data-${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)}]`).forEach(node => { const data = aggregate.get(node.dataset[attr]); node.textContent = data ? format(data) : "-"; });
|
||||
fill("vhostHealth", d => d.disabled ? t("vhDisabled") : d.staticOnly ? t("vhUp") : d.healthMode === "off" ? t("vhHealthDisabled") : `${d.up}/${d.total} ${t("vhUp")}`);
|
||||
@@ -242,6 +330,7 @@
|
||||
load(saved ? structuredClone(saved) : defaults());
|
||||
});
|
||||
$("pool-open").addEventListener("click", () => setPanel("vhosts"));
|
||||
initPoolTableTools();
|
||||
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
|
||||
window.rustProxyVhosts = { render, renderStatus, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); } };
|
||||
window.rustProxyVhosts = { render, renderStatus, applyPreferences: applyPoolPreferences, reset() { dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; poolTablePrefs = defaultPoolTablePrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); vhostTrafficHistory.clear(); container.replaceChildren(); $("vhost-search").value = ""; if ($("pool-search")) $("pool-search").value = ""; $("vhost-list").replaceChildren(); $("pool-rows").replaceChildren(); const sort = $("pool-sort"); if (sort) sort.value = poolTablePrefs.sort; applyPoolTableView(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
Reference in new issue
Block a user