This commit is contained in:
Mateusz Gruszczyński committed 2026-11-10 00:15:50 +01:00
1 parent 2789991452
commit e260612ef9
60 files changed
+69 -339

No files matched your search

-5
View File
@@ -215,7 +215,6 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
const connection = el("div", undefined, "vh-wide vh-backend-switches vh-backend-connection"); main.append(connection);
controls["member-private"] = field(connection, "member-private", "vhPrivate", value.allow_private, "checkbox");
const preserve = field(connection, "member-host", "vhPreserveHost", value.preserve_host === true, "checkbox");
// Preserve Host also applies to HTTP/2 :authority.
const advanced = el("details", undefined, "vh-wide vh-backend-advanced"), extra = el("div", undefined, "vh-grid"); advanced.append(text("summary", "advanced"), extra);
field(extra, "member-allowlist", "vhAddressAllowlist", (value.address_allowlist || []).join("\n"), "textarea", { rows: 2, maxLength: 8192 });
@@ -229,7 +228,6 @@ function memberRow(parent, value = { id: `origin-${parent.children.length + 1}`,
field(extra, "member-max-concurrent", "vhMaxConcurrentRequests", value.max_concurrent_requests ?? 0, "number", { required: true, min: 0, max: 1000000 });
field(extra, "member-queue-timeout", "vhBackendQueueTimeout", value.queue_timeout_ms ?? 0, "number", { required: true, min: 0, max: 60000 });
note(extra, "vhBackendCapacityHelp");
// One persistent form per backend keeps unsaved edits when switching.
const healthPanel = el("section", undefined, "vh-grid vh-backend-health-panel");
const customHealth = field(healthPanel, "member-health-custom", "backendHealthOverride", value.health_check != null, "checkbox");
const health = value.health_check || {};
@@ -305,7 +303,6 @@ function debugOriginIssues(members) {
let parsed; try { parsed = new URL(member.url); } catch { return false; }
const host = parsed.hostname.replace(/^\[|\]$/g, ""), ipHost = /^\d{1,3}(?:\.\d{1,3}){3}$/.test(host) || host.includes(":");
if (parsed.protocol !== "https:" || !ipHost) return false;
// Warn only when TLS verification of an IP host needs an explicit server name.
return member.tls_verify === true && !String(member.tls_server_name || "").trim();
});
}
@@ -496,11 +493,9 @@ function locationsEditor(parent, values) {
row.addEventListener("input", () => { syncUi(); sync(); }); row.addEventListener("change", () => { syncUi(); sync(); });
rows.append(row); syncUi();
}
// The main backend is an internal fallback, never an editable location.
for (const value of values) add(value);
const bottom = el("div", undefined, "vh-wide location-toolbar location-toolbar-bottom"); bottom.append(button("locationAdd", () => { add(); sync(); }, "primary")); parent.append(bottom);
form.querySelector(".vh-members").addEventListener("input", () => { for (const select of rows.querySelectorAll('[name="location-backend"]')) memberOptions(select); });
// Advanced JSON remains an explicit escape hatch; edits rebuild visual rows.
read("paths").addEventListener("change", () => { try { const data = JSON.parse(str("paths")); if (!Array.isArray(data) || data.length > 64) return; reloading = true; rows.replaceChildren(); for (const value of data) add(value); } catch { } finally { reloading = false; } });
sync(); return readRows;
}
-1
View File
@@ -275,7 +275,6 @@ function load(value = defaults(), migrationIds = [], show = true) {
field(g, "cookie-ttl", "vhCookieTtl", sticky.ttl_seconds ?? 3600, "number", { required: true, min: 60, max: 604800 }); note(g, "vhStickyNote");
const rate = value.rate_limit || {}, access = rate.country_access || {}, methodPolicy = value.method_policy || {};
g = group("vhTrafficPolicy");
// Policies share their original form/data model; the boundaries are presentational only.
const aclFields = subsection(g, "vhAclProfile");
const selectedProfiles = new Set([value.acl_profile_id, ...(value.acl_profile_ids || [])].filter(Boolean));
const profileBox = el("div", undefined, "vh-wide vh-acl-profile-picker");
+2 -8
View File
@@ -92,7 +92,7 @@ function refreshVhostBackendPopup(status = latestStatus) {
scheme = url.protocol.replace(":", "").toUpperCase();
const port = url.port || (scheme === "HTTPS" ? "443" : scheme === "HTTP" ? "80" : "");
address = `${url.hostname}${port ? `:${port}` : ""}`;
} catch { /* Configuration may have changed while status was refreshing. */ }
} catch { }
const upstream = upstreamProtocolBadgeText(backend, member);
const protocol = `${scheme} · ${upstream.mode}${upstream.observed ? ` → ${upstream.observed}` : ""}`;
const statusCode = Number(member?.last_status);
@@ -256,8 +256,6 @@ function initPoolTableTools() {
}));
syncPoolColumns(); syncPoolSortDirection();
}
// Backends overview (dashboard card "backend_table"): a richer view of the same backend data. It shares the sort,
// direction and column preferences with the compact "Tiny backends" table and follows table density and corner style.
const BO_SORT_LABELS = { vhost: "vhVhost", backend: "backend", status: "status", active: "vhActive", requests: "vhRequests", errors: "vhErrors", latency: "vhLatency", traffic: "vhTraffic", generation: "vhClientGeneration", generationRequests: "vhGenerationRequests", generationAge: "vhGenerationAge", rotations: "vhClientRotations", lastFailure: "vhLastFailure", lastRecovery: "vhLastRecovery" };
const BO_DETAIL_COLUMNS = ["generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
const BO_SAMPLES = 30;
@@ -282,7 +280,7 @@ const BO_SVG = "http://www.w3.org/2000/svg";
function boSvg(width, kind) {
const svg = document.createElementNS(BO_SVG, "svg");
svg.setAttribute("viewBox", `0 0 ${width} 20`); svg.setAttribute("class", `bo-spark bo-spark-${kind}`); svg.setAttribute("aria-hidden", "true");
boBaseline(svg, width, kind === "latency" ? 10 : 19); // idle or not-yet-sampled rows still show where the chart is
boBaseline(svg, width, kind === "latency" ? 10 : 19);
return svg;
}
function boBaseline(svg, width, y) {
@@ -426,7 +424,6 @@ function applyBackendOverviewView() {
const scroller = body.closest(".table-scroll"), scrollTop = scroller?.scrollTop ?? 0, scrollLeft = scroller?.scrollLeft ?? 0;
body.replaceChildren(...rows);
if (scroller) { scroller.scrollTop = scrollTop; scroller.scrollLeft = scrollLeft; }
// restore keyboard focus without scrolling: the table is rebuilt on every refresh and a plain focus() jumped back to the focused row
if (hadFocus) [...body.querySelectorAll(".bo-expand")].find(button => button.dataset.boKey === focusKey)?.focus({ preventScroll: true });
const empty = $("bo-empty");
if (empty) { empty.textContent = boItems.length ? t("backendSearchEmpty") : t("vhHealthEmpty"); empty.hidden = list.length > 0 || !boHasProxy; }
@@ -600,8 +597,6 @@ function render() {
policy.append(el("small", `WebSocket · ${transports.join("+")}`, "pill"));
if (v.websocket_legacy_support) policy.append(el("small", t("vhWebsocketLegacyBadge"), "pill"));
}
// Keep the upstream protocol indicators with the other Vhost policy pills.
// Retain their own badge class so observed H1/H2 keeps its green styling.
if (hasProxySource) {
for (const backend of v.backends || []) {
const badge = el("button", undefined, "vhost-backend-protocol-badge");
@@ -632,7 +627,6 @@ function backendTrafficRate(poolId, member, snapshot, now, seen) {
let inRate = sampled && bytesIn >= previous.bytesIn ? (bytesIn - previous.bytesIn) / seconds : 0;
let outRate = sampled && bytesOut >= previous.bytesOut ? (bytesOut - previous.bytesOut) / seconds : 0;
const rate = reported > 0 ? reported : observed;
// keep the in/out split consistent with the total shown elsewhere (the server-reported rate wins when present)
if (rate > 0 && inRate + outRate === 0) { inRate = rate * (total > 0 ? bytesIn / total : 0.5); outRate = rate - inRate; }
else if (reported > 0 && inRate + outRate > 0) { const scale = reported / (inRate + outRate); inRate *= scale; outRate *= scale; }
backendTrafficHistory.set(key, { ...rustProxyRateSampling.stamp(now), total, bytesIn, bytesOut, snapshot, rate, inRate, outRate });