function verifyAdminModules() { const required = [ ["rustProxyIntegrations", "bindings"], ["rustProxyVhosts", "render"], ["rustProxySettings", "render"], ["rustProxyCachePanel", "refresh"], ["rustProxyTrafficPanel", "refresh"], ["rustProxyPreferences", "load"], ["rustProxyRefresh", "start"], ]; const missing = required.find(([module, method]) => typeof window[module]?.[method] !== "function"); if (missing) throw new Error(`Panel resources incomplete (${missing[0]}). Reload to retry.`); } async function startAdminPanel() { try { verifyAdminModules(); const info = await api("/api/auth/session"); await loadLanguages(); setupRequired = false; translate(); syncAuthForm(); if (await activate(info)) clearInvalidOriginRecovery(); } catch (e) { if (recoverInvalidOrigin(e)) return; if (e.status === 401) { location.replace(`${location.pathname}${location.search}`); return; } showStartupFailure(e); } } if (document.readyState === "complete") startAdminPanel(); else document.addEventListener("DOMContentLoaded", startAdminPanel, { once: true }); function certificateUsesExternalFiles(cert) { return String(cert?.cert_file || "").startsWith("/") && String(cert?.key_file || "").startsWith("/"); } function syncCertificateFileSource() { const external = $("certificate-file-source").value === "external"; const cert = $("certificate-file"), key = $("certificate-key"); for (const input of [cert, key]) { input.maxLength = external ? 4096 : 96; if (external) input.removeAttribute("pattern"); else input.pattern = "[A-Za-z0-9_.-]+"; } cert.placeholder = external ? "/etc/letsencrypt/live/example.com/fullchain.pem" : "app.pem"; key.placeholder = external ? "/etc/letsencrypt/live/example.com/privkey.pem" : "app.key"; const helpKey = external ? "certificateSourceExternalHelp" : "certificateSourceManagedHelp"; $("certificate-file-source-help").dataset.i18n = helpKey; $("certificate-file-source-help").textContent = t(helpKey); $("certificate-external-acl-example").hidden = !external; } $("certificate-file-source").addEventListener("change", syncCertificateFileSource); function renderCertificates() { if (!envelope) return; const settings = envelope.config.tls || {}; const entries = (settings.certificates || []).map(cert => { const inventory = certificateInventory.get(cert.id); const coverage = (inventory?.covered_hosts?.length ? inventory.covered_hosts : (cert.covered_hosts || [])); const bindings = cert.hosts || []; const used = []; for (const vhost of envelope.config.vhosts || []) if (vhost.certificate_id === cert.id) used.push(`vhost:${vhost.id}`); for (const redirect of envelope.config.redirects || []) if (redirect.certificate_id === cert.id) used.push(`redirect:${redirect.id}`); for (const host of envelope.config.not_found_hosts || []) if (host.certificate_id === cert.id) used.push(`404:${host.id}`); if (envelope.config.administration?.certificate_id === cert.id) used.push(t("administration")); if (settings.default_certificate === cert.id) used.push(t("defaultLabel")); const managed = !!cert.issuer, inUse = used.length > 0; const box = el("div", undefined, "entity certificate-card"); const head = el("div", undefined, "certificate-card-head"); const identity = el("div", undefined, "certificate-card-identity"); identity.append(el("span", cert.id, "entity-title")); const statePills = el("div", undefined, "vhost-policy-pills certificate-state-pills"); if (managed) statePills.append(el("small", `${t("certificateManaged")} · ${cert.issuer.provider} / ${cert.issuer.environment}`, "pill")); else if (certificateUsesExternalFiles(cert)) statePills.append(el("small", t("certificateExternalFiles"), "pill")); if (settings.default_certificate === cert.id) statePills.append(el("small", t("defaultLabel"), "pill")); if (inventory?.expiry_status === "expired") statePills.append(el("small", t("certificateExpired"), "pill status-error")); else if (inventory?.expiry_status === "warning") statePills.append(el("small", t("certificateExpiresSoon"), "pill status-error")); if (statePills.children.length) identity.append(statePills); head.append(identity); box.append(head); const coverageBlock = el("div", undefined, "certificate-coverage"); coverageBlock.append(el("span", t("certificateCovers"), "small-label")); const domainPills = el("div", undefined, "vhost-policy-pills certificate-domain-pills"); if (coverage.length) { for (const host of coverage) domainPills.append(el("small", host, "pill")); } else { domainPills.append(el("small", t("certificateCoverageUnknown"), "certificate-coverage-unknown")); } coverageBlock.append(domainPills); box.append(coverageBlock); const details = el("details", undefined, "certificate-details"); details.append(el("summary", t("certificateDetails"))); const detailBody = el("div", undefined, "certificate-details-body"); const stamp = value => Number(value) ? formatDateTime(Number(value) * 1000) : "-"; detailBody.append( el("p", `${t("certificateBindings")}: ${bindings.join(", ") || "-"}`, "entity-details"), el("p", `${t("certificateFiles")}: ${cert.cert_file} / ${cert.key_file}`, "entity-details"), el("p", `${t("certAssigned")}: ${used.join(", ") || t("certUnassigned")}`, "entity-details"), el("p", `${t("certificateIssuerName")}: ${inventory?.certificate_issuer || "-"}`, "entity-details"), el("p", `${t("certificateValidFrom")}: ${stamp(inventory?.not_before_unix)}`, "entity-details"), el("p", `${t("renewalExpires")}: ${stamp(inventory?.not_after_unix)}`, "entity-details"), el("p", `${t("certificateDaysRemaining")}: ${inventory?.days_remaining ?? "-"}`, "entity-details"), el("p", `${t("certificateRenewalStatus")}: ${inventory?.renewal_status || "-"}`, "entity-details"), el("p", `${t("renewalLast")}: ${stamp(inventory?.last_renewal_attempt)}`, "entity-details"), el("p", `${t("certificateRenewalError")}: ${inventory?.last_renewal_error ? t(inventory.last_renewal_error) : "-"}`, "entity-details"), ); details.append(detailBody); box.append(details); const buttons = el("div", undefined, "entity-actions"); const assignPane = el("div", undefined, "certificate-bulk-assign"); assignPane.hidden = true; const assignmentList = el("div", undefined, "certificate-bulk-list"); for (const host of envelope.config.vhosts || []) { const label = el("label", undefined, "check"); const check = document.createElement("input"); check.type = "checkbox"; check.value = host.id; check.checked = host.certificate_id === cert.id; label.append(check, el("span", `${host.id} · ${(host.frontend?.hosts || []).join(", ")}`)); assignmentList.append(label); } const applyBindings = action(t("certAssignApply"), async () => { if (busy) return; const selected = new Set([...assignmentList.querySelectorAll('input:checked')].map(input => input.value)); const next = structuredClone(envelope.config); for (const host of next.vhosts) { if (selected.has(host.id)) host.certificate_id = cert.id; else if (host.certificate_id === cert.id) host.certificate_id = null; } await saveSection("vhosts", next.vhosts); }); assignPane.append(el("p", t("certAssignHelp"), "fine"), assignmentList, applyBindings); const assignButton = action(t("certAssignVhosts"), () => { assignPane.hidden = !assignPane.hidden; }); assignButton.disabled = !(envelope.config.vhosts || []).length; buttons.append(assignButton); if (!managed) { buttons.append(action(t("reloadCertificate"), async () => { if (busy) return; try { busy = true; const result = await api(`/api/certificates/${encodeURIComponent(cert.id)}/reload`, { method: "POST", body: "{}" }); await refreshCertificateInventory(); renderCertificates(); toast(`${t("reloadCertificate")}: ${result.reloaded ? "OK" : "-"}`); if (result.administration_restart_required) toast(t("certificateAdminRestartRequired"), true); } catch (error) { toast(error.message, true); } finally { busy = false; } })); buttons.append(action(t("edit"), () => { window.rustProxyCertificates?.open("existing"); $("certificate-advanced").open = true; $("certificate-id").value = cert.id; $("certificate-hosts").value = cert.hosts.join(", "); $("certificate-file-source").value = certificateUsesExternalFiles(cert) ? "external" : "managed"; syncCertificateFileSource(); $("certificate-file").value = cert.cert_file; $("certificate-key").value = cert.key_file; $("certificate-default").checked = settings.default_certificate === cert.id; })); } const remove = action(t("remove"), async () => { if (inUse) { toast(t("certInUse"), true); return; } if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.tls.certificates = next.tls.certificates.filter(v => v.id !== cert.id); await saveSection("tls", next.tls); }); remove.disabled = inUse; remove.title = inUse ? t("certInUse") : ""; buttons.append(remove); box.append(buttons, assignPane); return box; }); $("certificate-list").replaceChildren(...entries); if (!entries.length) $("certificate-list").append(el("p", t("emptyCertificates"), "empty")); } $("certificate-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const next = structuredClone(envelope.config); next.tls = next.tls || {}; next.tls.certificates = next.tls.certificates || []; const cert = { id: $("certificate-id").value.trim(), hosts: $("certificate-hosts").value.split(",").map(v => v.trim()).filter(Boolean), cert_file: $("certificate-file").value.trim(), key_file: $("certificate-key").value.trim() }; const index = next.tls.certificates.findIndex(v => v.id === cert.id); if (index >= 0 && next.tls.certificates[index].issuer) throw fieldError($("certificate-id"), t("certManagedReadOnly")); if (index < 0) next.tls.certificates.push(cert); else { const old = next.tls.certificates[index]; const sameFiles = old.cert_file === cert.cert_file && old.key_file === cert.key_file; next.tls.certificates[index] = { ...old, ...cert, covered_hosts: sameFiles ? (old.covered_hosts || []) : [], issuer: sameFiles ? old.issuer : null }; } if ($("certificate-default").checked) next.tls.default_certificate = cert.id; else if (next.tls.default_certificate === cert.id) delete next.tls.default_certificate; if (await saveSection("tls", next.tls)) { form.reset(); syncCertificateFileSource(); window.rustProxyCertificates?.close(); } }); } catch (error) { toast(error.message, true); } }); $("reload-certificates").addEventListener("click", async () => { if (!envelope || busy) return; try { busy = true; const result = await api("/api/certificates/reload", { method: "POST", body: "{}" }); await refreshCertificateInventory(); renderCertificates(); toast(`${t("reloadCertificates")}: ${result.reloaded ? "OK" : "-"}`); if (result.administration_restart_required) toast(t("certificateAdminRestartRequired"), true); } catch (error) { toast(error.message, true); } finally { busy = false; } }); $("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); }); syncCertificateFileSource(); let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null, lastCacheOutcomeRates = null, processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set(); const hiddenNetworkInterfaces = (() => { try { const parsed = JSON.parse(localStorage.getItem("rp-hidden-network-interfaces") || "[]"); return new Set(Array.isArray(parsed) ? parsed.filter(value => typeof value === "string") : []); } catch { return new Set(); } })(); function saveHiddenNetworkInterfaces() { try { localStorage.setItem("rp-hidden-network-interfaces", JSON.stringify([...hiddenNetworkInterfaces].sort())); } catch { } } function syncSettingsDirtyIndicators() { const dirtyTabs = new Set(); for (const id of settingsDirty) { const page = $(id)?.closest?.("[data-settings-page]"); if (page?.dataset.settingsPage) dirtyTabs.add(page.dataset.settingsPage); } document.querySelectorAll("[data-settings-tab]").forEach(button => { const dirty = dirtyTabs.has(button.dataset.settingsTab); button.classList.toggle("has-unsaved", dirty); if (dirty) button.title = t("unsavedChanges"); else button.removeAttribute("title"); }); window.rustProxyIntegrations?.syncHttp2Availability?.(); window.rustProxyIntegrations?.syncHttp3Availability?.(); } function setSettingsDirty(id, dirty = true) { if (dirty) settingsDirty.add(id); else settingsDirty.delete(id); syncSettingsDirtyIndicators(); } function clearSettingsDirty() { settingsDirty.clear(); syncSettingsDirtyIndicators(); } const fmt = (value, digits = 0) => value == null ? t("unavailable") : Number(value).toLocaleString(locale(), { maximumFractionDigits: digits }); const fmtBytes = value => value == null ? t("unavailable") : bytes(value); function setOptionalMetric(id, supported, text) { const node = $(id); if (!node) return; const row = node.closest("div"); if (row) row.hidden = !supported; if (supported) node.textContent = text; } function knownPair(left, right, format, zeroRight = null) { if (left == null && right == null) return null; if (left == null) return format(right); if (right == null) return format(left); const rightText = right === 0 && zeroRight ? zeroRight : format(right); return `${format(left)} / ${rightText}`; } function occupancyText(used, limit) { if (used == null) return "-"; const usedNumber = Number(used), limitNumber = Number(limit); if (!Number.isFinite(limitNumber) || limitNumber <= 0) return fmtBytes(usedNumber); return `${fmtBytes(usedNumber)} / ${fmtBytes(limitNumber)} · ${fmt(usedNumber / limitNumber * 100, 1)}%`; } function sparklinePoints(values, width = 300, height = 60, padding = 5, peakOverride = null) { const peak = Math.max(1, peakOverride ?? Math.max(0, ...values)); return values.map((value, index) => `${index * width / Math.max(1, values.length - 1)},${height - padding - Math.max(0, value) / peak * (height - padding * 2)}`).join(" "); } function svgNode(tag, attrs = {}) { const node = document.createElementNS("http://www.w3.org/2000/svg", tag); for (const [name, value] of Object.entries(attrs)) node.setAttribute(name, value); return node; } function setChartText(node, value) { const text = String(value); if (node.firstChild?.nodeType === 3 && node.firstChild === node.lastChild) { if (node.firstChild.data !== text) node.firstChild.data = text; } else if (node.textContent !== text) { node.textContent = text; } } let latestNetworkInterfaceSnapshot = []; function renderNetworkInterfaceCharts(interfaces, appendSample) { const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : []; latestNetworkInterfaceSnapshot = current; const names = new Set(current.map(item => item.name)); for (const name of [...networkInterfaceSamples.keys()]) if (!names.has(name)) networkInterfaceSamples.delete(name); if (appendSample) { for (const item of current) { if (!item?.name) continue; const history = networkInterfaceSamples.get(item.name) || { rx: [], tx: [] }; const rx = Number(item.rx_bytes_per_second), tx = Number(item.tx_bytes_per_second); if (Number.isFinite(rx)) { history.rx.push(rx); history.rx = history.rx.slice(-window.rustProxyOverviewSamples.getLimit()); } if (Number.isFinite(tx)) { history.tx.push(tx); history.tx = history.tx.slice(-window.rustProxyOverviewSamples.getLimit()); } networkInterfaceSamples.set(item.name, history); } } const target = $("network-interface-charts"); if (!target) return; const visible = current.filter(item => item?.name && !hiddenNetworkInterfaces.has(item.name)); setChartText($("network-interface-chart-value"), t("networkInterfacesVisibleCount").replace("{visible}", visible.length).replace("{total}", current.length)); const existing = new Map([...target.children].filter(node => node.classList.contains("network-interface-row")) .map(node => [node.dataset.interfaceName, node])); const children = []; if (!current.length) { const empty = target.querySelector(":scope > .network-interface-empty") || el("p", undefined, "empty network-interface-empty"); empty.textContent = t("networkNoInterfaces"); children.push(empty); } else { for (const item of visible) { const name = item.name; let row = existing.get(name); if (!row) { row = el("div", undefined, "network-interface-row"); row.dataset.interfaceName = name; const head = el("div", undefined, "mini-live-chart-head network-interface-row-head"); const rate = el("span", undefined, "fine network-interface-rate"); const hide = action(t("hideMetric"), () => { hiddenNetworkInterfaces.add(name); saveHiddenNetworkInterfaces(); renderNetworkInterfaceCharts(latestNetworkInterfaceSnapshot, false); }); hide.classList.add("network-interface-hide"); const actions = el("span", undefined, "network-interface-head-actions"); actions.append(rate, hide); head.append(el("b", name, "mono"), actions); const svg = svgNode("svg", { viewBox: "0 0 300 60", preserveAspectRatio: "none", role: "img", "aria-label": `${name} ${t("networkInterfacesHistory")}` }); svg.append(svgNode("path", { class: "chart-grid", d: "M0 15H300 M0 30H300 M0 45H300" })); svg.append(svgNode("polyline", { class: "diagnostic-line network-rx-line", points: "0,55 300,55" })); svg.append(svgNode("polyline", { class: "diagnostic-line network-tx-line", points: "0,55 300,55" })); row.append(head, svg); } const history = networkInterfaceSamples.get(name) || { rx: [], tx: [] }; const peak = Math.max(1, ...history.rx, ...history.tx); setChartText(row.querySelector(".network-interface-rate"), `${t("networkInterfaceRate")}: ${fmtBytes(item.rx_bytes_per_second)} / ${fmtBytes(item.tx_bytes_per_second)}`); setChartText(row.querySelector(".network-interface-hide"), t("hideMetric")); row.querySelector(".network-rx-line").setAttribute("points", sparklinePoints(history.rx, 300, 60, 5, peak) || "0,55 300,55"); row.querySelector(".network-tx-line").setAttribute("points", sparklinePoints(history.tx, 300, 60, 5, peak) || "0,55 300,55"); row.querySelector("svg").setAttribute("aria-label", `${name} ${t("networkInterfacesHistory")}`); children.push(row); } const hidden = current.filter(item => item?.name && hiddenNetworkInterfaces.has(item.name)); if (hidden.length) { const shelf = target.querySelector(":scope > .network-interface-hidden") || el("div", undefined, "network-interface-hidden"); const previous = [...shelf.querySelectorAll("button[data-interface-name]")].map(node => node.dataset.interfaceName); const changed = previous.length !== hidden.length || previous.some((name, i) => name !== hidden[i].name); if (changed) { shelf.replaceChildren(el("span", t("hiddenInterfaces"), "fine")); for (const item of hidden) { const show = action(`${t("showMetric")} ${item.name}`, () => { hiddenNetworkInterfaces.delete(item.name); saveHiddenNetworkInterfaces(); renderNetworkInterfaceCharts(latestNetworkInterfaceSnapshot, false); }); show.dataset.interfaceName = item.name; shelf.append(show); } } else { setChartText(shelf.querySelector(".fine"), t("hiddenInterfaces")); for (const button of shelf.querySelectorAll("button[data-interface-name]")) setChartText(button, `${t("showMetric")} ${button.dataset.interfaceName}`); } children.push(shelf); } else if (!visible.length) { const empty = target.querySelector(":scope > .network-interface-empty") || el("p", undefined, "empty network-interface-empty"); empty.textContent = t("networkNoVisibleInterfaces"); children.push(empty); } } if (children.length !== target.children.length || children.some((node, index) => node !== target.children[index])) { target.replaceChildren(...children); } } // Cumulative process counters, not rate-sampler deltas. Never invent OTHER // or force a 100% mix if independently read totals are inconsistent. function cacheOutcomeMix(totals) { if (!Array.isArray(totals) || totals.length !== 4 || totals.some(value => !Number.isSafeInteger(value) || value < 0)) return null; const [requests, hits, misses, bypasses] = totals; const cacheTotal = hits + misses + bypasses; if (requests === 0 || !Number.isSafeInteger(cacheTotal) || cacheTotal > requests) return null; const counts = [hits, misses, bypasses, requests - cacheTotal]; return { counts, percentages: counts.map(value => value / requests * 100), requests }; } function renderCacheMix(mix, totalRequests) { const names = ["hit", "miss", "bypass", "other"]; const unavailable = t(totalRequests === 0 ? "cacheMixWaiting" : "cacheMixInvalid"); names.forEach((name, i) => { const pct = mix ? mix.percentages[i] : 0; $(`cache-mix-${name}`).style.width = `${pct}%`; $(`cache-mix-${name}-count`).textContent = mix ? fmt(mix.counts[i]) : "-"; $(`cache-mix-${name}-percent`).textContent = mix ? `${fmt(pct, 1)}%` : "-"; }); $("cache-mix-headline").textContent = mix ? `${fmt(mix.percentages[0], 1)}% HIT` : "-"; const caption = $("cache-mix-caption"); const validTotal = Number.isSafeInteger(totalRequests) && totalRequests >= 0; const total = validTotal ? fmt(totalRequests) : "-"; const totalLabel = validTotal ? t("cacheMixSample").replace("{count}", total) : t("cacheMixInvalid"); caption.textContent = total; caption.setAttribute("title", totalLabel); caption.setAttribute("aria-label", totalLabel); $("cache-mix-bar").setAttribute("aria-label", mix ? t("cacheMixAria").replace("{hit}", fmt(mix.counts[0])) .replace("{miss}", fmt(mix.counts[1])) .replace("{bypass}", fmt(mix.counts[2])) .replace("{other}", fmt(mix.counts[3])) : unavailable); } function renderDiagnostics(status, appendSample = true, systemSample = appendSample) { const d = status.system || {}, live = status.live_system || d, c = status.cache || {}, m = status.metrics || {}; $("metric-h3").textContent = fmt(m.http3_requests); const cacheHits = c.hits == null ? null : Number(c.hits), cacheMisses = c.misses == null ? null : Number(c.misses); const cacheEvents = cacheHits !== null && cacheMisses !== null ? cacheHits + cacheMisses : 0; $("metric-cache-ratio").textContent = cacheEvents > 0 ? `${fmt(cacheHits / cacheEvents * 100, 1)}%` : "-"; window.rustProxyExtraOverview?.donut?.("cache-ratio", cacheHits, cacheMisses); const counters = [m.requests, c.hits, c.misses, c.bypasses].map(value => value == null ? NaN : Number(value)); renderCacheMix(cacheOutcomeMix(counters), counters[0]); if (systemSample) { if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes); $("process-summary").hidden = d.process_threads == null; if (d.process_threads != null) $("process-summary").textContent = `${fmt(d.process_threads)} ${t("threads")}`; setOptionalMetric("system-cpu", d.cpu_percent != null, `${fmt(d.cpu_percent, 1)}%`); const sourceSupported = !!d.source && d.source !== "unsupported"; $("system-source").hidden = !sourceSupported; if (sourceSupported) $("system-source").textContent = t("realSample"); const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value)); setOptionalMetric("system-memory", systemMemory != null, systemMemory); const ramMeter = $("memory-meter"), ramFill = $("memory-meter-fill"); ramMeter.hidden = systemMemory == null; const ramUsed = Number(d.memory_used_bytes), ramTotal = Number(d.memory_total_bytes); const ramPercent = d.memory_used_bytes != null && d.memory_total_bytes != null && Number.isFinite(ramUsed) && Number.isFinite(ramTotal) && ramTotal > 0 ? Math.max(0, Math.min(100, ramUsed / ramTotal * 100)) : null; ramFill.style.width = `${ramPercent ?? 0}%`; if (ramPercent == null) { ramMeter.removeAttribute("aria-valuenow"); ramMeter.setAttribute("aria-valuetext", "-"); } else { ramMeter.setAttribute("aria-valuenow", String(ramPercent)); ramMeter.setAttribute("aria-valuetext", `${fmt(ramPercent, 1)}%`); } const systemSwap = knownPair(d.system_swap_used_bytes, d.system_swap_total_bytes, value => fmtBytes(value)); setOptionalMetric("system-swap", systemSwap != null, systemSwap); const loadSupported = Array.isArray(d.load_average) && d.load_average.length > 0; setOptionalMetric("system-load", loadSupported, loadSupported ? d.load_average.map(n => fmt(n, 2)).join(" / ") : ""); setOptionalMetric("system-uptime", d.system_uptime_seconds != null, d.system_uptime_seconds == null ? "" : uptime(d.system_uptime_seconds)); } $("system-chart").hidden = live.cpu_percent == null; if (appendSample && live.cpu_percent != null) { systemSamples.push(Number(live.cpu_percent)); systemSamples = systemSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } $("system-sparkline").setAttribute("points", systemSamples.map((n, i) => `${i * 300 / Math.max(1, systemSamples.length - 1)},${95 - Math.max(0, Math.min(100, n)) * .9}`).join(" ")); $("cache-state").textContent = t(envelope?.config.cache?.enabled ? "enabled" : "notEnabled"); $("cache-counters").textContent = [c.hits, c.misses, c.bypasses].map(n => fmt(n)).join(" / "); $("cache-objects").textContent = `${fmt(c.entries)} / ${fmtBytes(c.resident_bytes)}`; $("cache-memory-breakdown").textContent = `${fmtBytes(c.resident_bytes)} / ${fmtBytes(c.transient_bytes)}`; $("cache-transient-breakdown").textContent = `${fmtBytes(c.fill_bytes_active)} / ${fmtBytes(c.cold_queue_bytes)} / ${fmtBytes((Number(c.cold_write_bytes_active) || 0) + (Number(c.cold_read_bytes_active) || 0))}`; const coldStorage = knownPair(c.cold_bytes, c.cold_limit_bytes, value => fmtBytes(value)); const coldObjects = knownPair(c.cold_entries, c.cold_limit_entries, value => fmt(value)); $("cache-cold-storage").textContent = envelope?.config.cache?.cold_enabled === true ? `${coldStorage ?? "-"}${coldObjects == null ? "" : ` · ${coldObjects} ${t("cacheColdStorageObjects")}`}` : t("notEnabled"); $("cache-evictions").textContent = `${fmt(c.evictions)} / ${fmt(c.expired)}`; if (appendSample) { if (c.resident_bytes != null) { cacheMemorySamples.push(Number(c.resident_bytes)); cacheMemorySamples = cacheMemorySamples.slice(-window.rustProxyOverviewSamples.getLimit()); } if (c.transient_bytes != null) { cacheTransientSamples.push(Number(c.transient_bytes)); cacheTransientSamples = cacheTransientSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } if (c.fill_bytes_active != null) { cacheFillSamples.push(Number(c.fill_bytes_active)); cacheFillSamples = cacheFillSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } if (c.cold_queue_bytes != null) { cacheColdQueueSamples.push(Number(c.cold_queue_bytes)); cacheColdQueueSamples = cacheColdQueueSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } const coldIoBytes = (Number(c.cold_write_bytes_active) || 0) + (Number(c.cold_read_bytes_active) || 0); if (c.cold_write_bytes_active != null || c.cold_read_bytes_active != null) { cacheColdIoSamples.push(coldIoBytes); cacheColdIoSamples = cacheColdIoSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } if (c.cold_bytes != null) { cacheColdStorageSamples.push(Number(c.cold_bytes)); cacheColdStorageSamples = cacheColdStorageSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } const currentCounters = [c.hits, c.misses, c.bypasses].map(Number); if (currentCounters.every(Number.isFinite)) { const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds")); const values = delta.deltas || lastCacheOutcomeRates || (!lastCacheCounters ? [0, 0, 0] : null); if (values) { cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]); cacheHitSamples = cacheHitSamples.slice(-window.rustProxyOverviewSamples.getLimit()); cacheMissSamples = cacheMissSamples.slice(-window.rustProxyOverviewSamples.getLimit()); cacheBypassSamples = cacheBypassSamples.slice(-window.rustProxyOverviewSamples.getLimit()); lastCacheOutcomeRates = [...values]; } lastCacheCounters = delta.next; } } if (appendSample) { if (live.process_cpu_percent != null) { processCpuSamples.push(Number(live.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } if (live.process_rss_bytes != null) { processRssSamples.push(Number(live.process_rss_bytes)); processRssSamples = processRssSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } } const cacheOutcomePeak = Math.max(1, ...cacheHitSamples, ...cacheMissSamples, ...cacheBypassSamples); $("cache-hit-sparkline").setAttribute("points", sparklinePoints(cacheHitSamples, 300, 60, 5, cacheOutcomePeak)); $("cache-miss-sparkline").setAttribute("points", sparklinePoints(cacheMissSamples, 300, 60, 5, cacheOutcomePeak)); $("cache-bypass-sparkline").setAttribute("points", sparklinePoints(cacheBypassSamples, 300, 60, 5, cacheOutcomePeak)); const latestCacheDelta = [cacheHitSamples.at(-1), cacheMissSamples.at(-1), cacheBypassSamples.at(-1)]; setChartText($("cache-outcome-chart-value"), latestCacheDelta.every(value => value != null) ? latestCacheDelta.map(value => fmt(value)).join(" / ") : "-"); const hotLimit = Number(c.hot_limit_bytes), hotPeak = Number.isFinite(hotLimit) && hotLimit > 0 ? Math.max(hotLimit, ...cacheMemorySamples) : null; $("cache-memory-sparkline").setAttribute("points", sparklinePoints(cacheMemorySamples, 300, 60, 5, hotPeak)); setChartText($("cache-chart-value"), occupancyText(c.resident_bytes, c.hot_limit_bytes)); const transientLimit = Number(c.transient_limit_bytes); const transientPeak = Number.isFinite(transientLimit) && transientLimit > 0 ? Math.max(transientLimit, ...cacheTransientSamples, ...cacheFillSamples, ...cacheColdQueueSamples, ...cacheColdIoSamples) : Math.max(1, ...cacheTransientSamples, ...cacheFillSamples, ...cacheColdQueueSamples, ...cacheColdIoSamples); $("cache-transient-sparkline").setAttribute("points", sparklinePoints(cacheTransientSamples, 300, 60, 5, transientPeak)); $("cache-fill-sparkline").setAttribute("points", sparklinePoints(cacheFillSamples, 300, 60, 5, transientPeak)); $("cache-queue-sparkline").setAttribute("points", sparklinePoints(cacheColdQueueSamples, 300, 60, 5, transientPeak)); $("cache-io-sparkline").setAttribute("points", sparklinePoints(cacheColdIoSamples, 300, 60, 5, transientPeak)); setChartText($("cache-transient-chart-value"), occupancyText(c.transient_bytes, c.transient_limit_bytes)); const coldEnabled = envelope?.config.cache?.cold_enabled === true; const coldLimit = Number(c.cold_limit_bytes), coldPeak = Number.isFinite(coldLimit) && coldLimit > 0 ? Math.max(coldLimit, ...cacheColdStorageSamples) : null; $("cache-cold-sparkline").setAttribute("points", sparklinePoints(cacheColdStorageSamples, 300, 60, 5, coldPeak)); setChartText($("cache-cold-chart-value"), coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled")); if (systemSample) { const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / "); setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform); setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`); const fds = knownPair(d.open_fds, d.max_fds, value => fmt(value), t("unlimited")); setOptionalMetric("process-fds", fds != null, fds); setOptionalMetric("process-threads", d.process_threads != null, fmt(d.process_threads)); const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited")); setOptionalMetric("process-virtual-memory", virtualMemory != null, virtualMemory); setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000)); setOptionalMetric("process-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds)); setOptionalMetric("process-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes)); const memory = status.memory_accounting || {}; const hotMemory = knownPair(memory.cache_hot_bytes, memory.cache_hot_limit_bytes, value => fmtBytes(value)); const transientMemory = knownPair(memory.cache_transient_bytes, memory.cache_transient_limit_bytes, value => fmtBytes(value)); const transformMemory = knownPair(memory.transform_reserved_bytes, memory.transform_budget_bytes, value => fmtBytes(value)); const transformInflight = knownPair(memory.transform_inflight, memory.transform_max_inflight, value => fmt(value)); setOptionalMetric("memory-cache-hot", hotMemory != null, hotMemory); setOptionalMetric("memory-cache-transient", transientMemory != null, transientMemory); setOptionalMetric("memory-transform", transformMemory != null, transformMemory); setOptionalMetric("memory-transform-inflight", transformInflight != null, transformInflight); setOptionalMetric("memory-non-cache-rss", memory.non_cache_rss_bytes != null, fmtBytes(memory.non_cache_rss_bytes)); setOptionalMetric("process-rss-anon", d.process_rss_anon_bytes != null, fmtBytes(d.process_rss_anon_bytes)); const rssFile = knownPair(d.process_rss_file_bytes, d.process_rss_shmem_bytes, value => fmtBytes(value)); setOptionalMetric("process-rss-file", rssFile != null, rssFile); const dataSwap = knownPair(d.process_data_bytes, d.process_swap_bytes, value => fmtBytes(value)); setOptionalMetric("process-data-swap", dataSwap != null, dataSwap); const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited")); setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory); } $("process-cpu-sparkline").setAttribute("points", sparklinePoints(processCpuSamples)); setChartText($("process-cpu-chart-value"), live.process_cpu_percent == null ? "" : `${fmt(live.process_cpu_percent, 1)}%`); $("process-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples)); setChartText($("process-rss-chart-value"), live.process_rss_bytes == null ? "" : fmtBytes(live.process_rss_bytes)); $("process-cpu-sparkline").closest(".mini-live-chart").hidden = live.process_cpu_percent == null; $("process-rss-sparkline").closest(".mini-live-chart").hidden = live.process_rss_bytes == null; updateCacheMemorySizing(); const networkRateSupported = live.network_rx_bytes_per_second != null && live.network_tx_bytes_per_second != null; if (systemSample) setOptionalMetric("system-network", d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null, `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}`); setOptionalMetric("mean-latency", m.mean_duration_ms != null, m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`); if (appendSample) { if (networkRateSupported) { networkRxSamples.push(Number(live.network_rx_bytes_per_second)); networkTxSamples.push(Number(live.network_tx_bytes_per_second)); networkRxSamples = networkRxSamples.slice(-window.rustProxyOverviewSamples.getLimit()); networkTxSamples = networkTxSamples.slice(-window.rustProxyOverviewSamples.getLimit()); } if (m.mean_duration_ms != null) { responseLatencySamples.push(Number(m.mean_duration_ms)); responseLatencySamples = responseLatencySamples.slice(-window.rustProxyOverviewSamples.getLimit()); } } const networkPeak = Math.max(1, ...networkRxSamples, ...networkTxSamples); $("network-rx-sparkline").setAttribute("points", sparklinePoints(networkRxSamples, 300, 60, 5, networkPeak)); $("network-tx-sparkline").setAttribute("points", sparklinePoints(networkTxSamples, 300, 60, 5, networkPeak)); setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(live.network_rx_bytes_per_second)} / ${fmtBytes(live.network_tx_bytes_per_second)}` : ""); $("network-rx-sparkline").closest(".mini-live-chart").hidden = !networkRateSupported; renderNetworkInterfaceCharts(live.network_interfaces || [], appendSample); $("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples)); setChartText($("latency-chart-value"), m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`); $("latency-sparkline").closest(".mini-live-chart").hidden = m.mean_duration_ms == null; $("compression-counters").textContent = [m.compression_gzip, m.compression_br, m.compression_zstd, m.compression_deflate].map(n => fmt(n)).join(" / "); const age = d.sampled_at_ms ? Math.max(0, Math.floor((Date.now() - d.sampled_at_ms) / 1000)) : null; $("system-sample-age").hidden = age == null; if (age != null) $("system-sample-age").textContent = `${fmt(age)} s`; $("system-sample-age").classList.toggle("status-error", age != null && age > 10); const l = status.listeners || {}; $("listener-pending").hidden = !l.restart_required; $("listener-pending").textContent = t("restartPending"); const activeListeners = $("listener-active"), listenerItems = el("div", undefined, "listener-active-items"); let endpoints = Array.isArray(l.active_endpoints) ? l.active_endpoints : []; if (!endpoints.length) { endpoints = []; if (status.proxy_listen) endpoints.push({ tls: false, address: status.proxy_listen, http3: false }); if (status.tls_listen) endpoints.push({ tls: true, address: status.tls_listen, http3: false }); } for (const endpoint of endpoints) { const item = el("div", undefined, "listener-active-item"); item.append(el("strong", endpoint.tls ? "HTTPS" : "HTTP", "listener-active-protocol"), el("code", endpoint.address || "-", "listener-active-address")); const protocols = ["H1"]; if (endpoint.tls) { if (endpoint.http2) protocols.push("H2"); if (endpoint.http3) protocols.push("H3/UDP"); } else if (endpoint.h2c) { protocols.push("H2C"); } for (const protocol of protocols) item.append(el("span", protocol, "listener-active-feature")); listenerItems.append(item); } if (!endpoints.length) listenerItems.append(el("span", t("notEnabled"), "fine")); activeListeners.replaceChildren(el("strong", t("activeListeners"), "listener-active-title"), listenerItems); } function renderHeaderRules(direction, rules) { const target = $(`${direction}-header-rules`); target.replaceChildren(); for (const rule of rules) appendHeaderRule(direction, rule, false); } function appendHeaderRule(direction, rule = { op: "set", name: "", value: "" }, mark = true) { const target = $(`${direction}-header-rules`); if (target.children.length >= 32) { toast(t("headerLimit"), true); return; } const row = el("div", undefined, "header-rule"); const op = el("select"); op.className = "header-op"; op.setAttribute("aria-label", t("headerOperation")); for (const value of ["set", "append", "remove"]) { const option = el("option", t(`header_${value}`)); option.value = value; op.append(option); } op.value = rule.op; const name = el("input"); name.className = "header-name"; name.value = rule.name; name.required = true; name.maxLength = 128; name.placeholder = t("headerName"); name.setAttribute("aria-label", t("headerName")); name.autocomplete = "off"; name.spellcheck = false; const value = el("input"); value.className = "header-value"; value.value = rule.value ?? ""; value.maxLength = 8192; value.placeholder = t("headerValue"); value.setAttribute("aria-label", t("headerValue")); value.autocomplete = "off"; value.spellcheck = false; function sync() { value.disabled = op.value === "remove"; } sync(); op.addEventListener("change", sync); const remove = action(t("remove"), () => { row.remove(); setSettingsDirty("headers-form"); }); row.append(op, name, value, remove); target.append(row); if (mark) setSettingsDirty("headers-form"); } function collectHeaderRules(direction) { return [...$(`${direction}-header-rules`).children].map(row => { const op = row.querySelector(".header-op").value, name = row.querySelector(".header-name").value.trim(), value = row.querySelector(".header-value").value; return op === "remove" ? { op, name } : { op, name, value }; }); } window.rustProxyOverviewSamples.onChange(() => { for (const history of [systemSamples, cacheMemorySamples, cacheTransientSamples, cacheFillSamples, cacheColdQueueSamples, cacheColdIoSamples, cacheColdStorageSamples, cacheHitSamples, cacheMissSamples, cacheBypassSamples, processCpuSamples, processRssSamples, networkRxSamples, networkTxSamples, responseLatencySamples]) { window.rustProxyOverviewSamples.trim(history); } for (const history of networkInterfaceSamples.values()) { window.rustProxyOverviewSamples.trim(history.rx); window.rustProxyOverviewSamples.trim(history.tx); } if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { renderDiagnostics(latestStatus, false, false); } });