v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -0,0 +1,244 @@
|
||||
function render() {
|
||||
if (!envelope) return;
|
||||
if (!initialized) load(defaults(), [], false);
|
||||
const config = envelope.config;
|
||||
const configuredHasProxySource = (config.vhosts || []).some(vhost =>
|
||||
vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy")
|
||||
);
|
||||
const diagnosticsCard = $("backend-diagnostics-card");
|
||||
if (diagnosticsCard) diagnosticsCard.hidden = !configuredHasProxySource;
|
||||
const query = ($("vhost-search")?.value || "").trim().toLowerCase();
|
||||
const visibleVhosts = (config.vhosts || []).filter(v => {
|
||||
if (!query) return true;
|
||||
const haystack = [
|
||||
v.id,
|
||||
...(v.frontend?.hosts || []),
|
||||
...(v.backends || []).flatMap(backend => [backend.id, backend.url]),
|
||||
v.source?.web_service_id,
|
||||
...(v.routes || []).map(route => route.source?.web_service_id),
|
||||
].filter(Boolean).join(" ").toLowerCase();
|
||||
return haystack.includes(query);
|
||||
});
|
||||
const cards = visibleVhosts.map(v => {
|
||||
const box = el("article", undefined, "vhost-card"); box.dataset.vhostCard = v.id;
|
||||
const head = el("div", undefined, "vhost-card-head"), identity = el("div", undefined, "vhost-card-identity");
|
||||
const title = el("div", undefined, "vhost-title-line"); title.append(el("strong", v.id, "entity-title"));
|
||||
const state = el("span", t(v.enabled === false ? "vhDisabled" : "vhEnabled"), `pill ${v.enabled === false ? "" : "live"}`); title.append(state);
|
||||
if (v.source?.type === "web_server") title.append(el("span", `${t("webServer")}: ${v.source.web_service_id}`, "pill"));
|
||||
identity.append(title, el("div", (v.frontend?.hosts || []).join(", ") || "-", "vhost-hosts")); head.append(identity);
|
||||
const actions = el("div", undefined, "entity-actions");
|
||||
const configuredHosts = (v.frontend?.hosts || []).filter(Boolean);
|
||||
const browseHost = configuredHosts.find(host => !host.startsWith("*.")) || configuredHosts.find(host => host.startsWith("*."))?.slice(2) || null;
|
||||
const configuredEndpoints = (v.frontend?.listener_ids || []).map(id => config.listeners?.endpoints?.find(endpoint => endpoint.id === id)).filter(Boolean);
|
||||
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 () => {
|
||||
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(); }
|
||||
}), button("liveLogs", () => window.rustProxyLogs.open(v.id)), button("vhPurgeCache", () => purge(v.id))); head.append(actions); box.append(head);
|
||||
|
||||
const details = el("div", undefined, "vhost-detail-strip");
|
||||
const selectedEndpoints = (v.frontend?.listener_ids || []).map(id =>
|
||||
config.listeners?.endpoints?.find(e => e.id === id) || { id }
|
||||
);
|
||||
const listeners = selectedEndpoints.map(endpoint => {
|
||||
const addresses = [endpoint.binding?.address, ...(endpoint.additional_addresses || [])].filter(Boolean).join(", ");
|
||||
return `${endpoint.name || endpoint.id} · ${addresses || t("listenerMissing")}`;
|
||||
});
|
||||
const supportsH2 = selectedEndpoints.some(endpoint => endpoint.binding?.http2);
|
||||
const supportsH3 = selectedEndpoints.some(endpoint => endpoint.binding?.http3);
|
||||
const protocols = [
|
||||
"HTTP/1.1",
|
||||
v.http2_enabled !== false && supportsH2 ? "H2" : null,
|
||||
v.http3_enabled !== false && supportsH3 ? "H3" : null,
|
||||
].filter(Boolean).join(" + ");
|
||||
const tlsPolicy = v.tls_policy ? [v.tls_policy.tls12 ? "TLS 1.2" : null, v.tls_policy.tls13 ? "TLS 1.3" : null].filter(Boolean).join(" + ") : t("inherit");
|
||||
const detailItems = [
|
||||
["vhListeners", listeners.join(" | ") || "-"],
|
||||
["vhProtocols", protocols],
|
||||
["tlsPolicy", tlsPolicy],
|
||||
["vhCertificate", v.certificate_id || t("vhCertificateDefault")],
|
||||
["vhAclProfile", v.acl_profile_id ? ((config.acl_profiles || []).find(profile => profile.id === v.acl_profile_id)?.name || v.acl_profile_id) : t("vhAclProfileNone")],
|
||||
["cache", v.cache?.enabled === true ? t("enabled") : v.cache?.enabled === false ? t("notEnabled") : t("inherit")],
|
||||
["vhBackendPool", v.source?.type === "web_server"
|
||||
? ((v.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy") && (v.backends || []).length
|
||||
? `${t("webServer")} + ${(v.backends || []).length}`
|
||||
: 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); }
|
||||
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); };
|
||||
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);
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 320 54"); svg.setAttribute("preserveAspectRatio", "none"); svg.dataset.chartFor = v.id; svg.setAttribute("aria-label", t("vhLifetimeChart")); const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("class", "vhost-chart-line"); svg.append(polyline); chart.append(svg); box.append(chart);
|
||||
const hasProxySource = v.source?.type !== "web_server" || (v.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy");
|
||||
if (hasProxySource) {
|
||||
const health = el("p", t("vhHealthEmpty"), "vh-health-summary"); health.dataset.healthFor = v.id; box.append(health);
|
||||
}
|
||||
const policy = el("div", undefined, "vhost-policy-pills");
|
||||
if (v.basic_auth?.enabled) policy.append(el("small", t("authBasic"), "pill"));
|
||||
if (v.tinyauth?.enabled) policy.append(el("small", "Tinyauth", "pill"));
|
||||
if (v.rate_limit?.enabled) policy.append(el("small", t("vhRateEnabled"), "pill"));
|
||||
if (v.acl_profile_id) policy.append(el("small", `${t("vhAclProfile")}: ${v.acl_profile_id}`, "pill"));
|
||||
if (v.force_ssl) policy.append(el("small", t("vhForceSsl"), "pill"));
|
||||
if (v.forward_client_ip === false) policy.append(el("small", t("vhClientIpHidden"), "pill"));
|
||||
if (v.websockets) {
|
||||
const transports = ["H1"];
|
||||
if (v.websocket_rfc8441) transports.push("H2/RFC8441");
|
||||
if (v.websocket_rfc9220) transports.push("H3/RFC9220");
|
||||
policy.append(el("small", `WebSocket · ${transports.join("+")}`, "pill"));
|
||||
if (v.websocket_legacy_support) policy.append(el("small", t("vhWebsocketLegacyBadge"), "pill"));
|
||||
}
|
||||
if (policy.children.length) box.append(policy);
|
||||
return box;
|
||||
});
|
||||
$("vhost-list").replaceChildren(...(cards.length ? cards : [text("p", query ? "vhSearchEmpty" : "vhEmpty", "fine")]));
|
||||
$("vhost-editing").textContent = t(editing ? "vhEditLabel" : "vhNewLabel");
|
||||
if (latestStatus) renderStatus(latestStatus);
|
||||
}
|
||||
// SSE snapshots arrive slower than the backend sampler, so a short burst can be zeroed before the UI sees it.
|
||||
// Fall back to the byte-counter delta for this dashboard interval when the reported instantaneous rate is zero.
|
||||
function backendTrafficRate(poolId, member, snapshot, now, seen) {
|
||||
const key = `${poolId}\u0000${member.id}`; seen.add(key);
|
||||
const total = Math.max(0, Number(member.bytes_in) || 0) + Math.max(0, Number(member.bytes_out) || 0);
|
||||
const reported = Math.max(0, Number(member.traffic_bytes_per_second) || 0);
|
||||
const previous = backendTrafficHistory.get(key);
|
||||
if (previous?.snapshot === snapshot) return previous.rate;
|
||||
const elapsed = previous ? Math.max(.001, (now - previous.at) / 1000) : 0;
|
||||
const observed = previous && total >= previous.total ? (total - previous.total) / elapsed : 0;
|
||||
const rate = reported > 0 ? reported : observed;
|
||||
backendTrafficHistory.set(key, { total, at: now, snapshot, rate });
|
||||
return rate;
|
||||
}
|
||||
function vhostTrafficRate(id, metrics, snapshot, now, seen) {
|
||||
seen.add(id);
|
||||
const total = Math.max(0, Number(metrics?.bytes_in) || 0) + Math.max(0, Number(metrics?.bytes_out) || 0);
|
||||
const previous = vhostTrafficHistory.get(id);
|
||||
if (previous?.snapshot === snapshot) return previous.rate;
|
||||
const elapsed = previous ? Math.max(.001, (now - previous.at) / 1000) : 0;
|
||||
const rate = previous && total >= previous.total ? (total - previous.total) / elapsed : 0;
|
||||
vhostTrafficHistory.set(id, { total, at: now, snapshot, rate });
|
||||
return rate;
|
||||
}
|
||||
function renderStatus(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();
|
||||
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 || []) {
|
||||
const members = pool.members || [], vhostDisabled = configuredVhosts.get(pool.id)?.enabled === false;
|
||||
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 label = vhostDisabled ? t("vhDisabled") : (pool.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 || "-"} · ${new Date(member.last_failure_at_ms).toLocaleTimeString(locale())}` : "-";
|
||||
const lastRecovery = Number(member.last_recovered_ms) > 0 ? new Date(member.last_recovered_ms).toLocaleTimeString(locale()) : "-";
|
||||
const healthDetail = `${t("vhActiveHealth")}: ${member.last_result || "-"} · ${t("vhPassiveFailures")}: ${member.passive_failures ?? 0} (${member.last_passive_result || "-"})`;
|
||||
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`, lastFailure, lastRecovery];
|
||||
cells.forEach((value, index) => { const cell = el("td", String(value ?? "-")); cell.title = index === 2 ? (vhostDisabled ? t("vhDisabled") : `${label} · ${healthDetail}`) : index === 8 ? (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 === 8 && 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; }
|
||||
}
|
||||
const summary = vhostDisabled ? t("vhDisabled") : members.map(member => `${member.id}: ${pool.health_mode === "off" && member.status === "up" ? t("vhHealthDisabled") : t(statusKeys[member.status] || "vhHealthUnknown")} / ${Number(member.latency_ms ?? member.ewma_latency_ms) > 0 ? `${Number(member.latency_ms ?? member.ewma_latency_ms).toFixed(1)} ms` : "-"} / ${member.active ?? 0} ${t("vhActive")}`).join(" | ");
|
||||
summaries.set(pool.id, summary || t("vhHealthEmpty"));
|
||||
aggregate.set(pool.id, {
|
||||
disabled: vhostDisabled,
|
||||
healthMode: pool.health_mode,
|
||||
up,
|
||||
total: members.length,
|
||||
active,
|
||||
requests,
|
||||
errors,
|
||||
traffic,
|
||||
latency: latencyWeight ? weightedLatency / latencyWeight : 0,
|
||||
});
|
||||
}
|
||||
for (const key of backendTrafficHistory.keys()) if (!seenTraffic.has(key)) backendTrafficHistory.delete(key);
|
||||
for (const [id, vhost] of configuredVhosts) {
|
||||
const existing = aggregate.get(id) || {};
|
||||
const metrics = status.vhost_metrics?.[id];
|
||||
const hasProxySource = vhost.source?.type !== "web_server" || (vhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy");
|
||||
const disabled = vhost.enabled === false;
|
||||
aggregate.set(id, {
|
||||
...existing,
|
||||
disabled,
|
||||
staticOnly: !hasProxySource,
|
||||
healthMode: existing.healthMode,
|
||||
up: existing.up ?? 0,
|
||||
total: existing.total ?? 0,
|
||||
active: metrics ? Number(metrics.active) || 0 : existing.active ?? 0,
|
||||
requests: metrics ? Number(metrics.requests) || 0 : existing.requests ?? 0,
|
||||
errors: metrics ? Number(metrics.errors) || 0 : existing.errors ?? 0,
|
||||
traffic: metrics ? vhostTrafficRate(id, metrics, trafficSnapshot, trafficNow, seenVhostTraffic) : existing.traffic ?? 0,
|
||||
latency: metrics ? Number(metrics.ewma_latency_ms) || 0 : existing.latency ?? 0,
|
||||
});
|
||||
}
|
||||
for (const key of vhostTrafficHistory.keys()) if (!seenVhostTraffic.has(key)) vhostTrafficHistory.delete(key);
|
||||
const hasConfiguredProxySource = [...configuredVhosts.values()].some(vhost =>
|
||||
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;
|
||||
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")}`);
|
||||
fill("vhostRequests", d => fmt(d.requests)); fill("vhostActive", d => fmt(d.active)); fill("vhostErrors", d => fmt(d.errors));
|
||||
fill("vhostLatency", d => d.latency > 0 ? `${fmt(d.latency, 1)} ms` : "-"); fill("vhostTraffic", d => `${rateBytes(d.traffic)}/s`);
|
||||
const now = performance.now();
|
||||
for (const [id, data] of aggregate) {
|
||||
const history = chartHistory.get(id) || { lastTime: now, lastRequests: data.requests, points: [], bucketSize: 1, pendingSum: 0, pendingCount: 0 };
|
||||
const elapsed = Math.max(.001, (now - history.lastTime) / 1000); const rate = data.requests >= history.lastRequests ? (data.requests - history.lastRequests) / elapsed : 0; history.lastTime = now; history.lastRequests = data.requests;
|
||||
history.pendingSum += rate; history.pendingCount += 1;
|
||||
if (history.pendingCount >= history.bucketSize) { history.points.push(history.pendingSum / history.pendingCount); history.pendingSum = 0; history.pendingCount = 0; }
|
||||
if (history.points.length >= 120) { const compacted = []; for (let i = 0; i < history.points.length; i += 2) compacted.push((history.points[i] + history.points[i + 1]) / 2); history.points = compacted; history.bucketSize *= 2; }
|
||||
chartHistory.set(id, history);
|
||||
const displayPoints = history.pendingCount ? [...history.points, history.pendingSum / history.pendingCount] : history.points; const actualPeak = Math.max(0, ...displayPoints), scalePeak = Math.max(1, actualPeak); const points = displayPoints.map((value, index) => `${index * 320 / Math.max(1, displayPoints.length - 1)},${50 - value / scalePeak * 44}`).join(" ");
|
||||
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`); svg?.querySelector("polyline")?.setAttribute("points", points); const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`); if (label) label.textContent = `${rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${actualPeak.toFixed(1)}`;
|
||||
}
|
||||
const editingVhost = editing ? configuredVhosts.get(editing) : null;
|
||||
const editingHasProxy = editingVhost && (editingVhost.source?.type !== "web_server" || (editingVhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy"));
|
||||
$("vhost-health-live").hidden = !!editingVhost && !editingHasProxy;
|
||||
$("vhost-health-live").textContent = editing ? (editingHasProxy ? (summaries.get(editing) || t("vhHealthEmpty")) : "") : t("vhNewHealthHint");
|
||||
}
|
||||
form.addEventListener("input", changed); form.addEventListener("change", changed);
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault(); if (!envelope || busy || uploading) return;
|
||||
const invalid = [...form.elements].find(input => input.willValidate && !input.checkValidity()); if (invalid) { activateTab(invalid.closest(".vh-group")?.dataset.tab || "details"); const details = invalid.closest("details"); if (details) details.open = true; invalid.reportValidity(); return; }
|
||||
if (revision !== envelope.revision) { toast(t("stale"), true); return; }
|
||||
try {
|
||||
const epoch = draftEpoch; uploading = true;
|
||||
try { await basicEditor.prepare(); } finally { uploading = false; }
|
||||
if (epoch !== draftEpoch || revision !== envelope.revision || !dialog.open) throw new Error(t("stale"));
|
||||
const value = collect(), next = structuredClone(envelope.config); next.vhosts ||= [];
|
||||
const debugIssues = debugOriginIssues(value.backends); if (debugIssues.length && !(await appConfirm(`${t("vhDebugOriginMismatch")}\n\n${debugIssues.map(member => member.id).join(", ")}`, { danger: true }))) return;
|
||||
if (next.vhosts.some(v => v.id === value.id && v.id !== editing)) throw new Error(t("vhConflict"));
|
||||
if (editing && value.id !== editing && !(await appConfirm(t("vhIdRenameConfirm").replace("{old}", editing).replace("{new}", value.id), { danger: true }))) return;
|
||||
const addsCertificate = stagedCertificates.some(c => c.id === value.certificate_id);
|
||||
if (addsCertificate) { next.tls ||= {}; next.tls.certificates ||= []; for (const cert of stagedCertificates.filter(c => c.id === value.certificate_id)) if (!next.tls.certificates.some(c => c.id === cert.id)) next.tls.certificates.push(cert); }
|
||||
const index = next.vhosts.findIndex(v => v.id === editing); if (index < 0) next.vhosts.push(value); else next.vhosts[index] = value;
|
||||
const saved = addsCertificate ? await save(next) : await saveSection("vhosts", next.vhosts || []);
|
||||
if (saved) { load(value, [], false); dialog.close(); render(); }
|
||||
} catch (error) { toast(error.message, true); }
|
||||
});
|
||||
$("vhost-new").addEventListener("click", async () => { if (await allowDiscard()) load(); });
|
||||
$("vhost-search").addEventListener("input", render);
|
||||
$("vhost-reset").addEventListener("click", async () => {
|
||||
if (!(await allowDiscard())) return;
|
||||
const saved = editing ? (envelope?.config.vhosts || []).find(item => item.id === editing) : null;
|
||||
load(saved ? structuredClone(saved) : defaults());
|
||||
});
|
||||
$("pool-open").addEventListener("click", () => setPanel("vhosts"));
|
||||
$("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(); } };
|
||||
if (envelope) render(); if (latestStatus) renderStatus(latestStatus);
|
||||
Reference in new issue
Block a user