v0.33.7
This commit is contained in:
1 parent
162e986d37
commit
6b937aaf85
54 files changed
+3527
-1325
No files matched your search
@@ -1,3 +1,158 @@
|
||||
// Shared, anchored Vhost popover for backend quick-view and upstream HTTP
|
||||
// protocol observations. Both consume existing live status without extra probes.
|
||||
let activeVhostBackendPopup = null;
|
||||
function closeVhostBackendPopup(returnFocus = false) {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
activeVhostBackendPopup = null;
|
||||
current.trigger.setAttribute("aria-expanded", "false");
|
||||
current.node.remove();
|
||||
if (returnFocus && current.trigger.isConnected) current.trigger.focus();
|
||||
}
|
||||
function positionVhostBackendPopup() {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
const rect = current.trigger.getBoundingClientRect(), edge = 12, gap = 6;
|
||||
const width = Math.min(430, Math.max(0, window.innerWidth - edge * 2));
|
||||
const height = Math.min(current.node.scrollHeight, 360, Math.max(100, window.innerHeight - edge * 2));
|
||||
const below = window.innerHeight - rect.bottom - edge - gap;
|
||||
const above = rect.top - edge - gap;
|
||||
const placeAbove = below < height && above > below;
|
||||
const desiredTop = placeAbove ? rect.top - height - gap : rect.bottom + gap;
|
||||
current.node.style.width = `${width}px`;
|
||||
current.node.style.left = `${Math.max(edge, Math.min(rect.left, window.innerWidth - width - edge))}px`;
|
||||
current.node.style.top = `${Math.max(edge, Math.min(desiredTop, window.innerHeight - height - edge))}px`;
|
||||
}
|
||||
function vhostPopupHeading(label) {
|
||||
const heading = el("h4", label);
|
||||
heading.id = "vhost-backend-popup-heading";
|
||||
const headingRow = el("div", undefined, "vhost-backend-popup-heading");
|
||||
const closeButton = el("button", "×", "vhost-backend-popup-close");
|
||||
closeButton.type = "button";
|
||||
closeButton.setAttribute("aria-label", t("close"));
|
||||
closeButton.addEventListener("click", () => closeVhostBackendPopup(true));
|
||||
headingRow.append(heading, closeButton);
|
||||
return { heading, headingRow };
|
||||
}
|
||||
function refreshVhostProtocolPopup(current, vhost, members) {
|
||||
const backend = (vhost.backends || []).find(item => item.id === current.backendId);
|
||||
if (!backend) { closeVhostBackendPopup(); return; }
|
||||
const member = members.get(backend.id);
|
||||
const observed = upstreamProtocolBadgeText(backend, member);
|
||||
if (!current.protocolView) {
|
||||
const { heading, headingRow } = vhostPopupHeading(observed.label);
|
||||
const info = el("p", "", "fine vhost-backend-popup-hint");
|
||||
const list = el("ul", undefined, "vhost-backend-popup-list");
|
||||
const row = el("li", undefined, "vhost-backend-popup-row");
|
||||
const columns = el("div", undefined, "vhost-backend-popup-columns");
|
||||
const counters = [];
|
||||
for (const protocol of ["H1", "H2"]) {
|
||||
const field = el("div", undefined, "vhost-backend-popup-field");
|
||||
const value = el("span", "0");
|
||||
field.append(el("span", protocol), value);
|
||||
columns.append(field); counters.push(value);
|
||||
}
|
||||
row.append(columns); list.append(row);
|
||||
current.node.append(headingRow, info, list);
|
||||
current.node.setAttribute("aria-labelledby", heading.id);
|
||||
current.protocolView = { heading, info, list, counters };
|
||||
}
|
||||
const view = current.protocolView;
|
||||
const explanation = t(observed.recent ? "vhBackendProtocolRecent" : "vhBackendProtocolUnobserved");
|
||||
if (view.heading.textContent !== observed.label) view.heading.textContent = observed.label;
|
||||
if (view.info.textContent !== explanation) view.info.textContent = explanation;
|
||||
view.list.hidden = !observed.recent;
|
||||
if (observed.recent) {
|
||||
const totals = [member.upstream_protocol_http1_total || 0, member.upstream_protocol_http2_total || 0];
|
||||
totals.forEach((total, index) => {
|
||||
const label = String(total);
|
||||
if (view.counters[index].textContent !== label) view.counters[index].textContent = label;
|
||||
});
|
||||
}
|
||||
positionVhostBackendPopup();
|
||||
}
|
||||
function refreshVhostBackendPopup(status = latestStatus) {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (!current) return;
|
||||
const vhost = (envelope?.config.vhosts || []).find(item => item.id === current.id);
|
||||
if (!vhost || !current.trigger.isConnected) { closeVhostBackendPopup(); return; }
|
||||
const pool = (status?.backend_pools || []).find(item => item.id === vhost.id);
|
||||
const members = new Map((pool?.members || []).map(member => [member.id, member]));
|
||||
if (current.backendId !== null) {
|
||||
refreshVhostProtocolPopup(current, vhost, members);
|
||||
return;
|
||||
}
|
||||
const { heading, headingRow } = vhostPopupHeading(`${t("vhBackendPopupTitle")} · ${vhost.id}`);
|
||||
const list = el("ul", undefined, "vhost-backend-popup-list");
|
||||
for (const backend of vhost.backends || []) {
|
||||
const member = members.get(backend.id);
|
||||
const row = el("li", undefined, "vhost-backend-popup-row");
|
||||
row.append(el("strong", backend.id, "vhost-backend-popup-name"));
|
||||
let address = "-", scheme = "-";
|
||||
try {
|
||||
const url = new URL(backend.url);
|
||||
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. */ }
|
||||
const upstream = upstreamProtocolBadgeText(backend, member);
|
||||
const protocol = `${scheme} · ${upstream.mode}${upstream.observed ? ` → ${upstream.observed}` : ""}`;
|
||||
const statusCode = Number(member?.last_status);
|
||||
const hasStatus = member && Number.isInteger(statusCode) && statusCode >= 100 && statusCode <= 599;
|
||||
const httpResponse = hasStatus ? `HTTP ${statusCode}` : t("vhBackendPopupNoResponse");
|
||||
const statusLabel = !member ? t("vhHealthUnknown") : t(({
|
||||
up: "vhUp", down: "vhDown", disabled: "vhDisabled", checking: "vhChecking", draining: "vhDraining"
|
||||
})[member.status] || "vhHealthUnknown");
|
||||
const columns = el("div", undefined, "vhost-backend-popup-columns");
|
||||
for (const [key, value] of [
|
||||
["vhBackendPopupAddress", address], ["vhBackendPopupProtocol", protocol],
|
||||
["vhBackendPopupLastHttp", httpResponse], ["status", statusLabel]
|
||||
]) {
|
||||
const field = el("div", undefined, "vhost-backend-popup-field");
|
||||
field.append(text("span", key), el("span", value));
|
||||
if (key === "vhBackendPopupLastHttp" && hasStatus) field.classList.add(statusCode >= 500 ? "has-error" : statusCode < 400 ? "has-success" : "");
|
||||
columns.append(field);
|
||||
}
|
||||
row.append(columns); list.append(row);
|
||||
}
|
||||
if (!list.children.length) list.append(el("li", t("vhBackendPopupEmpty"), "fine"));
|
||||
const foot = el("p", t("vhBackendPopupHint"), "fine vhost-backend-popup-hint");
|
||||
current.node.replaceChildren(headingRow, list, foot);
|
||||
current.node.setAttribute("aria-labelledby", heading.id);
|
||||
positionVhostBackendPopup();
|
||||
}
|
||||
function toggleVhostBackendPopup(id, trigger, backendId = null) {
|
||||
if (activeVhostBackendPopup?.trigger === trigger) { closeVhostBackendPopup(); return; }
|
||||
closeVhostBackendPopup();
|
||||
const node = el("section", undefined, "vhost-backend-popup");
|
||||
node.id = "vhost-backend-popup";
|
||||
node.setAttribute("role", "dialog");
|
||||
node.setAttribute("aria-modal", "false");
|
||||
trigger.setAttribute("aria-expanded", "true");
|
||||
activeVhostBackendPopup = { id, trigger, node, backendId, protocolView: null };
|
||||
document.body.append(node);
|
||||
refreshVhostBackendPopup();
|
||||
}
|
||||
function enableVhostBackendPopup(trigger, vhost, backendId = null) {
|
||||
trigger.dataset.vhostBackendTrigger = vhost.id;
|
||||
trigger.setAttribute("aria-haspopup", "dialog");
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
trigger.setAttribute("aria-controls", "vhost-backend-popup");
|
||||
if (backendId === null) trigger.setAttribute("aria-label", `${t("vhBackendPopupOpen")}: ${vhost.id}`);
|
||||
trigger.addEventListener("click", () => toggleVhostBackendPopup(vhost.id, trigger, backendId));
|
||||
}
|
||||
document.addEventListener("pointerdown", event => {
|
||||
const current = activeVhostBackendPopup;
|
||||
if (current && !current.node.contains(event.target) && !current.trigger.contains(event.target)) closeVhostBackendPopup();
|
||||
});
|
||||
document.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape" && activeVhostBackendPopup) { event.preventDefault(); event.stopPropagation(); closeVhostBackendPopup(true); }
|
||||
});
|
||||
window.addEventListener("resize", positionVhostBackendPopup);
|
||||
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 vhostSortColumns = ["id", "hostname", "status", "listeners", "certificate", "backends"];
|
||||
const defaultPoolTablePrefs = () => ({ sort: "vhost", descending: false, columns: [...poolColumns] });
|
||||
@@ -107,7 +262,8 @@ function upstreamProtocolBadgeText(backend, member, now = Date.now()) {
|
||||
const seen = Number(member?.upstream_protocol_last_seen_ms);
|
||||
const recent = member && (member.upstream_protocol_observed === "h1" || member.upstream_protocol_observed === "h2") &&
|
||||
Number.isFinite(seen) && seen > 0 && now >= seen && now - seen <= VHOST_PROTOCOL_FRESH_MS;
|
||||
return { label: `${backend.id} · ${mode} → ${recent ? member.upstream_protocol_observed.toUpperCase() : "?"}`, recent };
|
||||
const observed = recent ? member.upstream_protocol_observed.toUpperCase() : null;
|
||||
return { label: `${backend.id} · ${mode} → ${observed || "?"}`, recent, mode, observed };
|
||||
}
|
||||
function renderUpstreamProtocolBadges(status) {
|
||||
const members = new Map((status.backend_pools || []).flatMap(pool => (pool.members || []).map(member =>
|
||||
@@ -119,13 +275,12 @@ function renderUpstreamProtocolBadges(status) {
|
||||
if (!backend) return;
|
||||
const member = members.get(`${vhost}\u0000${id}`);
|
||||
const observed = upstreamProtocolBadgeText(backend, member, now);
|
||||
node.textContent = observed.label;
|
||||
if (node.textContent !== observed.label) node.textContent = observed.label;
|
||||
node.dataset.observed = String(!!observed.recent);
|
||||
const counts = member && observed.recent ? ` H1: ${member.upstream_protocol_http1_total || 0}; H2: ${member.upstream_protocol_http2_total || 0}.` : "";
|
||||
node.title = `${t(observed.recent ? "vhBackendProtocolRecent" : "vhBackendProtocolUnobserved")}${counts}`;
|
||||
});
|
||||
}
|
||||
function render() {
|
||||
closeVhostBackendPopup();
|
||||
if (!envelope) return;
|
||||
if (!initialized) load(defaults(), [], false);
|
||||
const config = envelope.config;
|
||||
@@ -209,10 +364,21 @@ function render() {
|
||||
: t("webServer"))
|
||||
: `${(v.backends || []).length}`],
|
||||
];
|
||||
for (const [key, value] of detailItems) { const item = el("div", undefined, "vhost-detail"); item.append(text("span", key, "small-label"), el("b", value)); details.append(item); }
|
||||
for (const [key, value] of detailItems) {
|
||||
const isBackend = key === "vhBackendPool";
|
||||
const item = el(isBackend ? "button" : "div", undefined, `vhost-detail${isBackend ? " vhost-backend-trigger" : ""}`);
|
||||
if (isBackend) { item.type = "button"; enableVhostBackendPopup(item, v); }
|
||||
item.append(text("span", key, "small-label"), el("b", value)); details.append(item);
|
||||
}
|
||||
box.append(details);
|
||||
const live = el("div", undefined, "vhost-live-grid");
|
||||
const metric = (key, suffix) => { const item = el("div", undefined, "vhost-live-metric"); item.append(text("span", key), el("strong", "-")); item.querySelector("strong").dataset[suffix] = v.id; live.append(item); };
|
||||
const metric = (key, suffix) => {
|
||||
const isHealth = key === "status";
|
||||
const item = el(isHealth ? "button" : "div", undefined, `vhost-live-metric${isHealth ? " vhost-backend-trigger" : ""}`);
|
||||
if (isHealth) { item.type = "button"; enableVhostBackendPopup(item, v); }
|
||||
item.append(text("span", key), el("strong", "-"));
|
||||
item.querySelector("strong").dataset[suffix] = v.id; live.append(item);
|
||||
};
|
||||
metric("status", "vhostHealth"); metric("vhRequests", "vhostRequests"); metric("vhActive", "vhostActive"); metric("vhErrors", "vhostErrors"); metric("vhLatency", "vhostLatency"); metric("vhTraffic", "vhostTraffic");
|
||||
box.append(live);
|
||||
const chart = el("div", undefined, "vhost-chart"); const chartHead = el("div", undefined, "vhost-chart-head"); chartHead.append(text("span", "vhLifetimeChart", "small-label")); const chartValue = el("span", "-", "fine"); chartValue.dataset.chartValueFor = v.id; chartHead.append(chartValue); chart.append(chartHead);
|
||||
@@ -243,11 +409,12 @@ function render() {
|
||||
// Retain their own badge class so observed H1/H2 keeps its green styling.
|
||||
if (hasProxySource) {
|
||||
for (const backend of v.backends || []) {
|
||||
const badge = el("span", undefined, "vhost-backend-protocol-badge");
|
||||
const badge = el("button", undefined, "vhost-backend-protocol-badge");
|
||||
badge.type = "button";
|
||||
badge.dataset.upstreamProtocolVhost = v.id;
|
||||
badge.dataset.upstreamProtocolBackend = backend.id;
|
||||
badge.textContent = upstreamProtocolBadgeText(backend, null).label;
|
||||
badge.title = t("vhBackendProtocolUnobserved");
|
||||
enableVhostBackendPopup(badge, v, backend.id);
|
||||
policy.append(badge);
|
||||
}
|
||||
}
|
||||
@@ -282,6 +449,7 @@ function vhostTrafficRate(id, metrics, snapshot, now, seen) {
|
||||
}
|
||||
function renderStatus(status) {
|
||||
renderUpstreamProtocolBadges(status);
|
||||
refreshVhostBackendPopup(status);
|
||||
form.querySelectorAll(".vh-member").forEach(row => row.syncLocalRisk?.());
|
||||
const rows = [], summaries = new Map(), aggregate = new Map();
|
||||
const trafficSnapshot = status, trafficNow = performance.now(), seenTraffic = new Set(), seenVhostTraffic = new Set();
|
||||
@@ -415,5 +583,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() { 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(); 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);
|
||||
Reference in new issue
Block a user