This commit is contained in:
Mateusz Gruszczyński committed 2026-11-10 11:36:23 +01:00
1 parent e260612ef9
commit 643999f17e
25 files changed
+421 -86

No files matched your search

+1
View File
@@ -2,6 +2,7 @@
let basicEditor = null, locationsReader = () => [], aclControl = null, editing = null, revision = null, dirty = false, initialized = false, readBindings = () => [], readHosts = () => [], countryRatesReader = () => [], uaRatesReader = () => [], pagesEditor = null, stagedCertificates = [], uploading = false, draftEpoch = 0, backendHealthEditorControl = null;
const chartHistory = new Map();
const backendTrafficHistory = new Map();
const backendRequestHistory = new Map();
const vhostTrafficHistory = new Map();
const dialog = $("vhost-dialog");
const form = $("vhost-form"), container = $("vhost-fields");
+127 -11
View File
@@ -151,9 +151,11 @@ document.addEventListener("scroll", event => {
if (activeVhostBackendPopup && !activeVhostBackendPopup.node.contains(event.target)) positionVhostBackendPopup();
}, true);
const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
const poolColumns = ["vhost", "backend", "status", "active", "requests", "errors", "latency", "traffic", "rate", "statuses", "p95", "cache", "probe", "causes", "protocol", "generation", "generationRequests", "generationAge", "rotations", "lastFailure", "lastRecovery"];
const poolOptionalColumns = ["rate", "p95", "probe", "causes", "protocol"];
const vhostSortColumns = ["id", "hostname", "status", "listeners", "certificate", "backends"];
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] });
const defaultPoolColumns = () => poolColumns.filter(key => !poolOptionalColumns.includes(key));
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: defaultPoolColumns() });
const defaultVhostListPrefs = () => ({ sort: "id", descending: false });
let poolTablePrefs = defaultPoolTablePrefs(), vhostListPrefs = defaultVhostListPrefs(), poolPreferenceSaveRunning = false, poolPreferenceSaveQueued = false;
function normalizePoolTablePrefs(preferences) {
@@ -161,7 +163,7 @@ function normalizePoolTablePrefs(preferences) {
return {
sort: poolColumns.includes(preferences?.backend_table_sort) ? preferences.backend_table_sort : "vhost",
descending: preferences?.backend_table_descending === true,
columns: selected.length ? selected : [...poolColumns],
columns: selected.length ? selected : defaultPoolColumns(),
};
}
function normalizeVhostListPrefs(preferences) {
@@ -256,7 +258,7 @@ function initPoolTableTools() {
}));
syncPoolColumns(); syncPoolSortDirection();
}
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_SORT_LABELS = { vhost: "vhVhost", backend: "backend", status: "status", active: "vhActive", requests: "vhRequests", errors: "vhErrors", latency: "vhLatency", traffic: "vhTraffic", rate: "backendColRate", statuses: "backendColStatuses", p95: "backendColP95", cache: "backendColCache", probe: "backendColProbe", causes: "backendColCauses", protocol: "backendColProtocol", 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;
const boOpen = new Set(), boSeries = new Map();
@@ -270,6 +272,54 @@ function buildBackendOverviewTools() {
input.type = "checkbox"; input.dataset.poolColumnToggle = key; text.dataset.i18n = BO_SORT_LABELS[key]; label.append(input, text); options.append(label);
}
}
const BO_CAUSES = [["http_5xx_total", "5xx"], ["timeouts_total", "timeout"], ["connect_errors_total", "connect"], ["tls_errors_total", "TLS"], ["dns_errors_total", "DNS"], ["io_errors_total", "I/O"], ["queue_timeouts_total", "queue"], ["capacity_rejections_total", "limit"], ["retry_attempts_total", "retry"]];
function boMs(value) {
if (!(value > 0)) return "-";
if (value >= 10000) return ">5 s";
return value >= 1000 ? `${Number((value / 1000).toFixed(1))} s` : `${Math.round(value)} ms`;
}
function boPercent(part, total) {
if (!(total > 0) || !(part > 0)) return "0%";
const pct = part / total * 100;
return pct < 0.1 ? "<0.1%" : `${pct.toFixed(pct < 10 ? 1 : 0)}%`;
}
function backendRequestRate(key, requests, snapshot, now, seen) {
seen.add(key);
const previous = backendRequestHistory.get(key);
if (previous?.snapshot === snapshot) return previous.rate;
const continuous = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds"));
const rate = continuous && requests >= previous.requests && now > previous.at ? (requests - previous.requests) / ((now - previous.at) / 1000) : previous?.rate || 0;
backendRequestHistory.set(key, { ...rustProxyRateSampling.stamp(now), requests, snapshot, rate: continuous ? rate : 0 });
return continuous ? rate : 0;
}
function backendExtraMetrics(pool, member, status, snapshot, now, seen) {
const vhost = status.vhost_metrics?.[pool.id] || {}, cacheTotals = status.vhost_cache_totals?.[pool.id] || null;
const classes = Array.isArray(vhost.status_classes) ? vhost.status_classes.map(value => Math.max(0, Number(value) || 0)) : [0, 0, 0, 0, 0];
const classTotal = classes.reduce((sum, value) => sum + value, 0);
const hits = Number(cacheTotals?.hits) || 0, misses = Number(cacheTotals?.misses) || 0, bypasses = Number(cacheTotals?.bypasses) || 0, lookups = hits + misses + bypasses;
const cacheBytes = Math.max(0, Number(cacheTotals?.saved_body_bytes) || 0), outBytes = Math.max(cacheBytes, Number(vhost.bytes_out) || 0), originBytes = Math.max(0, outBytes - cacheBytes);
const p95 = Number(vhost.p95_latency_ms) || 0, p99 = Number(vhost.p99_latency_ms) || 0;
const probeOff = member.health_mode === "off", probeLatency = Number(member.latency_ms) || 0, probeAt = Number(member.last_probe_ms) || 0;
const causes = BO_CAUSES.map(([field, label]) => ({ label, value: Math.max(0, Number(member[field]) || 0) })).filter(cause => cause.value > 0).sort((a, b) => b.value - a.value);
const causeTotal = causes.filter(cause => cause.label !== "retry").reduce((sum, cause) => sum + cause.value, 0);
const h1 = Math.max(0, Number(member.upstream_protocol_http1_total) || 0), h2 = Math.max(0, Number(member.upstream_protocol_http2_total) || 0);
const protocol = member.upstream_protocol_observed === "h1" || member.upstream_protocol_observed === "h2" ? member.upstream_protocol_observed.toUpperCase() : "";
const rate = backendRequestRate(`${pool.id}\u0000${member.id}`, Math.max(0, Number(member.requests) || 0), snapshot, now, seen);
const hitRatio = lookups ? hits / lookups : -1, errorShare = classTotal ? (classes[3] + classes[4]) / classTotal : -1;
return {
rate, classes, classTotal, hits, misses, bypasses, lookups, cacheBytes, originBytes, outBytes, p95, p99, probeOff, probeLatency, probeAt, lastProbeResult: member.last_result || "", causes, causeTotal, h1, h2, protocol,
sort: { rate, statuses: errorShare, p95, cache: hitRatio, probe: probeOff ? -1 : probeLatency, causes: causeTotal, protocol: h1 + h2 ? h2 / (h1 + h2) : -1 },
text: {
rate: `${rate.toFixed(rate < 10 ? 1 : 0)}/s`,
statuses: classTotal ? ["2xx", "3xx", "4xx", "5xx"].map((label, index) => `${label} ${boPercent(classes[index + 1], classTotal)}`).join(" · ") : "-",
p95: p95 ? `${boMs(p95)} / ${boMs(p99)}` : "-",
cache: lookups || cacheBytes ? `HIT ${boPercent(hits, lookups)} · ${bytes(cacheBytes)} / ${bytes(originBytes)}` : "-",
probe: probeOff || !(probeLatency > 0) ? "-" : `${probeLatency.toFixed(1)} ms`,
causes: causes.length ? causes.map(cause => `${cause.label} ${cause.value}`).join(" · ") : "-",
protocol: protocol ? `${protocol}${h1 + h2 ? ` · H2 ${boPercent(h2, h1 + h2)}` : ""}` : "-",
},
};
}
function boSeriesOf(item) { return item.series || { latency: [], inRate: [], outRate: [], requests: [] }; }
function boAgo(ms) {
const seconds = Math.max(0, Math.floor((Date.now() - Number(ms)) / 1000));
@@ -341,6 +391,67 @@ function renderBackendOverview(items, hasProxy, snapshot = null) {
$("backend-overview-card")?.classList.toggle("is-no-proxy", !boHasProxy);
applyBackendOverviewView();
}
function boStack(parts) {
const track = el("div", undefined, "bo-stack"), total = parts.reduce((sum, [value]) => sum + value, 0);
if (total > 0) for (const [value, kind] of parts) if (value > 0) { const part = el("i", undefined, `is-${kind}`); part.style.flexGrow = String(value); part.style.minWidth = "2px"; track.append(part); }
return track;
}
function boRing(ratio) {
const svg = document.createElementNS(BO_SVG, "svg"), radius = 8, length = 2 * Math.PI * radius;
svg.setAttribute("viewBox", "0 0 20 20"); svg.setAttribute("class", "bo-ring"); svg.setAttribute("aria-hidden", "true");
for (const kind of ["track", "value"]) {
const circle = document.createElementNS(BO_SVG, "circle");
circle.setAttribute("cx", "10"); circle.setAttribute("cy", "10"); circle.setAttribute("r", String(radius)); circle.setAttribute("class", `bo-ring-${kind}`);
if (kind === "value") { circle.setAttribute("stroke-dasharray", `${(Math.max(0, Math.min(1, ratio)) * length).toFixed(2)} ${length.toFixed(2)}`); circle.setAttribute("transform", "rotate(-90 10 10)"); }
svg.append(circle);
}
return svg;
}
const BO_EXTRA_CELLS = {
rate(cell, extra) { cell.className = "is-num"; cell.textContent = extra.text.rate; if (!(extra.rate > 0)) cell.classList.add("is-zero"); },
statuses(cell, extra) {
cell.className = "bo-statuses"; cell.title = `${t("backendVhostScope")}\n${["1xx", "2xx", "3xx", "4xx", "5xx"].map((label, index) => `${label}: ${extra.classes[index]}`).join("\n")}`;
if (!extra.classTotal) { cell.append(el("span", "-", "is-zero")); return; }
const top = el("div", undefined, "bo-meter-top"), issues = el("small");
top.append(el("span", `2xx ${boPercent(extra.classes[1], extra.classTotal)}`));
for (const [index, kind] of [[3, "warn"], [4, "bad"]]) if (extra.classes[index] > 0) issues.append(el("b", `${index + 1}xx ${boPercent(extra.classes[index], extra.classTotal)}`, `is-${kind}`));
if (!issues.children.length) issues.textContent = "4xx/5xx 0%";
top.append(issues); cell.append(top, boStack([[extra.classes[0] + extra.classes[1], "ok"], [extra.classes[2], "info"], [extra.classes[3], "warn"], [extra.classes[4], "bad"]]));
},
p95(cell, extra) {
cell.className = "bo-pair"; cell.title = `${t("backendVhostScope")} · ${t("backendBucketBound")}`;
if (!extra.p95) { cell.append(el("span", "-", "is-zero")); return; }
cell.append(el("span", boMs(extra.p95)), el("small", `p99 ${boMs(extra.p99)}`));
},
probe(cell, extra) {
cell.className = "bo-pair";
if (extra.probeOff || !(extra.probeLatency > 0)) { cell.append(el("span", "-", "is-zero")); return; }
cell.title = extra.lastProbeResult; cell.append(el("span", `${extra.probeLatency.toFixed(1)} ms`));
if (extra.probeAt > 0) cell.append(el("small", boAgo(extra.probeAt)));
},
cache(cell, extra) {
cell.className = "bo-cache";
cell.title = `${t("backendVhostScope")}\nHIT ${extra.hits} · MISS ${extra.misses} · BYPASS ${extra.bypasses}\nCACHE ${bytes(extra.cacheBytes)} (${boPercent(extra.cacheBytes, extra.outBytes)}) · ORIGIN ${bytes(extra.originBytes)}`;
if (!extra.lookups && !extra.cacheBytes) { cell.append(el("span", "-", "is-zero")); return; }
const wrap = el("div", undefined, "bo-chartcell"), hit = el("div", undefined, "bo-cache-hit"), text = el("div", undefined, "bo-traffic-text");
hit.append(boRing(extra.lookups ? extra.hits / extra.lookups : 0), el("span", extra.lookups ? boPercent(extra.hits, extra.lookups) : "-"));
const line = (kind, label, value) => { const row = el("div", undefined, `bo-${kind}`); row.append(el("span", label), el("b", bytes(value), value > 0 ? "" : "is-zero")); return row; };
text.append(line("hit", "CACHE", extra.cacheBytes), line("origin", "ORIGIN", extra.originBytes));
wrap.append(hit, text); cell.append(wrap);
},
causes(cell, extra) {
cell.className = "bo-causes"; cell.title = extra.text.causes;
if (!extra.causes.length) { cell.append(el("span", "-", "is-zero")); return; }
for (const cause of extra.causes.slice(0, 3)) { const chip = el("span", undefined, `bo-cause${cause.label === "retry" ? " is-soft" : ""}`); chip.append(cause.label, el("b", String(cause.value))); cell.append(chip); }
if (extra.causes.length > 3) cell.append(el("small", `+${extra.causes.length - 3}`));
},
protocol(cell, extra) {
cell.className = "bo-pair"; cell.title = `H1 ${extra.h1} · H2 ${extra.h2}`;
if (!extra.protocol) { cell.append(el("span", "-", "is-zero")); return; }
cell.append(el("span", extra.protocol));
if (extra.h1 + extra.h2) cell.append(el("small", `H2 ${boPercent(extra.h2, extra.h1 + extra.h2)}`));
},
};
function applyBackendOverviewView() {
const body = $("bo-rows"); if (!body) return;
const visibleColumns = new Set(poolTablePrefs.columns);
@@ -370,7 +481,9 @@ function applyBackendOverviewView() {
const columns = [
expandable && ["toggle", ""], showIdentity && ["identity", `${visibleColumns.has("vhost") ? t("vhVhost") : ""}${visibleColumns.has("vhost") && visibleColumns.has("backend") ? " / " : ""}${visibleColumns.has("backend") ? t("backend") : ""}`],
visibleColumns.has("status") && ["status", t("status")], visibleColumns.has("active") && ["active", t("vhActive"), true], visibleColumns.has("requests") && ["requests", t("vhRequests"), true],
visibleColumns.has("errors") && ["errors", t("vhErrors")], visibleColumns.has("latency") && ["latency", t("vhLatency")], visibleColumns.has("traffic") && ["traffic", t("vhTraffic"), true], showEvent && ["event", t("backendLastEvent")],
visibleColumns.has("rate") && ["rate", t("backendColRate"), true], visibleColumns.has("errors") && ["errors", t("vhErrors")], visibleColumns.has("statuses") && ["statuses", t("backendColStatuses")], visibleColumns.has("latency") && ["latency", t("vhLatency")],
visibleColumns.has("p95") && ["p95", t("backendColP95"), true], visibleColumns.has("probe") && ["probe", t("backendColProbe"), true], visibleColumns.has("cache") && ["cache", t("backendColCache")], visibleColumns.has("traffic") && ["traffic", t("vhTraffic"), true],
visibleColumns.has("causes") && ["causes", t("backendColCauses")], visibleColumns.has("protocol") && ["protocol", t("backendColProtocol")], showEvent && ["event", t("backendLastEvent")],
].filter(Boolean);
const head = $("bo-head"); if (head) head.replaceChildren(...columns.map(([id, label, numeric]) => { const th = el("th", label, numeric ? "is-num" : ""); th.dataset.boColumn = id; if (id === "toggle") th.setAttribute("aria-label", t("backendDetailsToggle")); return th; }));
const rows = [];
@@ -405,6 +518,7 @@ function applyBackendOverviewView() {
text.append(line("in", "IN", item.trafficIn || 0), line("out", "OUT", item.trafficOut || 0));
wrap.append(boTrafficChart(boSeriesOf(item).inRate, boSeriesOf(item).outRate), text); cell.append(wrap);
}
else if (id in BO_EXTRA_CELLS) BO_EXTRA_CELLS[id](cell, item.extra);
else if (id === "event") {
cell.className = "bo-event";
if (item.lastFailureMs && item.lastFailureMs >= item.lastRecoveryMs) { const line = el("span", item.lastFailureClass || t("backendEventFailed"), "is-failed"); line.title = item.lastFailureDetail || ""; cell.append(line, el("small", boAgo(item.lastFailureMs))); }
@@ -651,7 +765,7 @@ function renderStatus(status) {
refreshVhostBackendPopup(status);
form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.());
const rows = [], summaries = new Map(), aggregate = new Map(), overviewItems = [];
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set();
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set(), seenRequests = new Set();
const statusKeys = { up: "vhUp", down: "vhDown", checking: "vhChecking", draining: "vhDraining", disabled: "vhDisabled" };
const configuredVhosts = new Map((envelope?.config.vhosts || []).map(vhost => [vhost.id, vhost]));
for (const pool of status.backend_pools || []) {
@@ -659,6 +773,7 @@ function renderStatus(status) {
let up = 0, active = 0, requests = 0, errors = 0, traffic = 0, weightedLatency = 0, latencyWeight = 0;
for (const member of members) {
const memberTraffic = backendTrafficRate(pool.id, member, trafficSnapshot, trafficNow, seenTraffic);
const extra = backendExtraMetrics(pool, member, status, trafficSnapshot, trafficNow, seenRequests);
const label = vhostDisabled ? t("vhDisabled") : (member.health_mode === "off" && member.status === "up" ? t("vhHealthDisabled") : t(statusKeys[member.status] || "vhHealthUnknown"));
const activeLimit = Number(member.max_concurrent_requests) > 0 ? `${member.active ?? 0}/${member.max_concurrent_requests}` : `${member.active ?? 0}/∞`;
const lastFailure = Number(member.last_failure_at_ms) > 0 ? `${member.last_failure_class || "-"} · ${formatTime(member.last_failure_at_ms)}` : "-";
@@ -668,17 +783,17 @@ function renderStatus(status) {
const generationAge = Math.max(0, Number(generationAgeRaw));
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];
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 };
member.ewma_latency_ms > 0 ? `${member.ewma_latency_ms.toFixed(1)} ms` : "-", `${rateBytes(memberTraffic)}/s`, extra.text.rate, extra.text.statuses, extra.text.p95, extra.text.cache, extra.text.probe, extra.text.causes, extra.text.protocol, member.upstream_client_generation ?? "-", member.upstream_client_generation_requests ?? "-", generationAgeText, member.upstream_client_rotations_total ?? "-", lastFailure, lastRecovery];
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, ...extra.sort, 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);
cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); const column = poolColumns[index]; cell.dataset.poolColumn = column; cell.title = column === "status" ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : column === "lastFailure" ? (member.last_failure_detail || lastFailure) : String(value ?? "-"); if (column === "status" && !vhostDisabled) cell.className = member.status === "down" ? "status-error" : member.status === "up" ? "status-good" : ""; if (column === "lastFailure" && member.last_failure_at_ms) cell.classList.add("backend-last-failure"); row.append(cell); }); rows.push(row);
const tone = vhostDisabled ? "muted" : member.health_mode === "off" && member.status === "up" ? "muted" : member.status === "up" ? "up" : member.status === "down" ? "down" : member.status === "checking" || member.status === "draining" ? "warn" : "muted";
overviewItems.push({
key: `${pool.id}\u0000${member.id}`, vhost: poolLabel, backend: `${member.id}${member.backup ? " / " + t("vhBackup") : ""}`, disabled: vhostDisabled, tone, group: tone === "up" ? "up" : tone === "down" ? "down" : "other",
label, title: vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`, activeText: activeLimit, requests: Number(member.requests) || 0, errors: Number(member.errors) || 0, latency: Number(member.ewma_latency_ms) || 0, traffic: memberTraffic, ...(({ inRate, outRate }) => ({ trafficIn: inRate, trafficOut: outRate }))(backendTrafficSplit(pool.id, member)),
generation: member.upstream_client_generation ?? "-", generationRequests: member.upstream_client_generation_requests ?? "-", generationAgeText, rotations: member.upstream_client_rotations_total ?? "-",
lastFailureMs: Number(member.last_failure_at_ms) || 0, lastFailureClass: member.last_failure_class || "", lastFailureDetail: member.last_failure_detail || "", lastFailureText: lastFailure, lastRecoveryMs: Number(member.last_recovered_ms) || 0, lastRecoveryText: lastRecovery,
sort: sortValues, search: row.poolSearchValue,
extra, sort: sortValues, search: row.poolSearchValue,
});
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; }
@@ -698,6 +813,7 @@ function renderStatus(status) {
});
}
for (const key of backendTrafficHistory.keys()) if (!seenTraffic.has(key)) backendTrafficHistory.delete(key);
for (const key of backendRequestHistory.keys()) if (!seenRequests.has(key)) backendRequestHistory.delete(key);
for (const [id, vhost] of configuredVhosts) {
const existing = aggregate.get(id) || {};
const metrics = status.vhost_metrics?.[id];
@@ -822,5 +938,5 @@ $("pool-open").addEventListener("click", () => setPanel("vhosts"));
initPoolTableTools();
$("location-help-close")?.addEventListener("click", () => $("location-help-dialog")?.close());
window.rustProxyDrafts.register("vhost", { labelKey: "vhosts", isOpen: () => dialog.open, isDirty: () => dirty, suspend: suspendDraft, restore: restoreDraft, close: () => { dirty = false; if (dialog.open) dialog.close(); } });
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { closeVhostBackendPopup(); dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); 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; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
window.rustProxyVhosts = { render, renderStatus, applyPreferences, reset() { closeVhostBackendPopup(); dialog.close(); draftEpoch++; stagedCertificates = []; uploading = false; editing = null; revision = null; dirty = false; initialized = false; backendHealthEditorControl = null; poolTablePrefs = defaultPoolTablePrefs(); vhostListPrefs = defaultVhostListPrefs(); poolPreferenceSaveRunning = false; poolPreferenceSaveQueued = false; chartHistory.clear(); backendTrafficHistory.clear(); backendRequestHistory.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; const vhostSort = $("vhost-sort"); if (vhostSort) vhostSort.value = vhostListPrefs.sort; syncVhostSortDirection(); applyPoolTableView(); } };
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);