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,381 @@
|
||||
(async () => {
|
||||
try {
|
||||
// Revalidate the session first. The anonymous document has its own login.js;
|
||||
// app.js belongs to the authenticated shell and must not fall back to its
|
||||
// legacy embedded sign-in form when another startup step fails.
|
||||
const info = await api("/api/auth/session");
|
||||
await loadLanguages();
|
||||
setupRequired = false;
|
||||
translate();
|
||||
syncAuthForm();
|
||||
await activate(info);
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
location.replace(`${location.pathname}${location.search}`);
|
||||
return;
|
||||
}
|
||||
showStartupFailure(e);
|
||||
}
|
||||
})();
|
||||
|
||||
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"));
|
||||
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) ? new Date(Number(value) * 1000).toLocaleString(locale()) : "-";
|
||||
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");
|
||||
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").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);
|
||||
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(); 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());
|
||||
|
||||
// Dashboard metrics are actual server samples. Null means unavailable, not zero.
|
||||
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = 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 renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
const current = Array.isArray(interfaces) ? interfaces.slice().sort((a, b) => String(a.name || "").localeCompare(String(b.name || ""))) : [];
|
||||
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(-60); }
|
||||
if (Number.isFinite(tx)) { history.tx.push(tx); history.tx = history.tx.slice(-60); }
|
||||
networkInterfaceSamples.set(item.name, history);
|
||||
}
|
||||
}
|
||||
const target = $("network-interface-charts");
|
||||
if (!target) return;
|
||||
target.replaceChildren();
|
||||
const visible = current.filter(item => item?.name && !hiddenNetworkInterfaces.has(item.name));
|
||||
$("network-interface-chart-value").textContent = t("networkInterfacesVisibleCount").replace("{visible}", visible.length).replace("{total}", current.length);
|
||||
if (!current.length) { target.append(el("p", t("networkNoInterfaces"), "empty network-interface-empty")); return; }
|
||||
for (const item of visible) {
|
||||
const history = networkInterfaceSamples.get(item.name) || { rx: [], tx: [] };
|
||||
const peak = Math.max(1, ...history.rx, ...history.tx);
|
||||
const row = el("div", undefined, "network-interface-row");
|
||||
const head = el("div", undefined, "mini-live-chart-head network-interface-row-head");
|
||||
const rate = el("span", `${t("networkInterfaceRate")}: ${fmtBytes(item.rx_bytes_per_second)} / ${fmtBytes(item.tx_bytes_per_second)}`, "fine");
|
||||
const hide = action(t("hideMetric"), () => { hiddenNetworkInterfaces.add(item.name); saveHiddenNetworkInterfaces(); renderNetworkInterfaceCharts(current, false); });
|
||||
hide.classList.add("network-interface-hide");
|
||||
const actions = el("span", undefined, "network-interface-head-actions"); actions.append(rate, hide);
|
||||
head.append(el("b", item.name || "-", "mono"), actions);
|
||||
const svg = svgNode("svg", { viewBox: "0 0 300 60", role: "img", "aria-label": `${item.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: sparklinePoints(history.rx, 300, 60, 5, peak) || "0,55 300,55" }));
|
||||
svg.append(svgNode("polyline", { class: "diagnostic-line network-tx-line", points: sparklinePoints(history.tx, 300, 60, 5, peak) || "0,55 300,55" }));
|
||||
row.append(head, svg); target.append(row);
|
||||
}
|
||||
const hidden = current.filter(item => item?.name && hiddenNetworkInterfaces.has(item.name));
|
||||
if (hidden.length) {
|
||||
const hiddenRow = el("div", undefined, "network-interface-hidden");
|
||||
hiddenRow.append(el("span", t("hiddenInterfaces"), "fine"));
|
||||
for (const item of hidden) {
|
||||
const show = action(`${t("showMetric")} ${item.name}`, () => { hiddenNetworkInterfaces.delete(item.name); saveHiddenNetworkInterfaces(); renderNetworkInterfaceCharts(current, false); });
|
||||
hiddenRow.append(show);
|
||||
}
|
||||
target.append(hiddenRow);
|
||||
} else if (!visible.length) {
|
||||
target.append(el("p", t("networkNoVisibleInterfaces"), "empty network-interface-empty"));
|
||||
}
|
||||
}
|
||||
function renderDiagnostics(status, appendSample = true) {
|
||||
const d = status.system || {}, c = status.cache || {}, m = status.metrics || {};
|
||||
$("metric-h3").textContent = fmt(m.http3_requests);
|
||||
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
|
||||
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
|
||||
$("metric-rss").closest("article.stat").hidden = d.process_rss_bytes == null;
|
||||
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")}`;
|
||||
const systemCpuSupported = d.cpu_percent != null;
|
||||
setOptionalMetric("system-cpu", systemCpuSupported, systemCpuSupported ? `${fmt(d.cpu_percent, 1)}%` : "");
|
||||
$("system-chart").hidden = !systemCpuSupported;
|
||||
const sourceSupported = !!d.source && d.source !== "unsupported";
|
||||
$("system-source").hidden = !sourceSupported;
|
||||
if (sourceSupported) $("system-source").textContent = t("realSample");
|
||||
if (appendSample && d.cpu_percent != null) { systemSamples.push(d.cpu_percent); systemSamples = systemSamples.slice(-30); }
|
||||
$("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(" "));
|
||||
const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("system-memory", systemMemory != null, systemMemory);
|
||||
$("memory-meter").hidden = systemMemory == null;
|
||||
if (d.memory_used_bytes != null && d.memory_total_bytes > 0) $("memory-meter").value = d.memory_used_bytes / d.memory_total_bytes * 100; else $("memory-meter").removeAttribute("value");
|
||||
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));
|
||||
$("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(-60); }
|
||||
if (c.transient_bytes != null) { cacheTransientSamples.push(Number(c.transient_bytes)); cacheTransientSamples = cacheTransientSamples.slice(-60); }
|
||||
if (c.fill_bytes_active != null) { cacheFillSamples.push(Number(c.fill_bytes_active)); cacheFillSamples = cacheFillSamples.slice(-60); }
|
||||
if (c.cold_queue_bytes != null) { cacheColdQueueSamples.push(Number(c.cold_queue_bytes)); cacheColdQueueSamples = cacheColdQueueSamples.slice(-60); }
|
||||
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(-60); }
|
||||
if (c.cold_bytes != null) { cacheColdStorageSamples.push(Number(c.cold_bytes)); cacheColdStorageSamples = cacheColdStorageSamples.slice(-60); }
|
||||
const currentCounters = [c.hits, c.misses, c.bypasses].map(Number);
|
||||
if (currentCounters.every(Number.isFinite)) {
|
||||
const delta = lastCacheCounters ? currentCounters.map((value, index) => value >= lastCacheCounters[index] ? value - lastCacheCounters[index] : 0) : [0, 0, 0];
|
||||
cacheHitSamples.push(delta[0]); cacheMissSamples.push(delta[1]); cacheBypassSamples.push(delta[2]);
|
||||
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
|
||||
lastCacheCounters = currentCounters;
|
||||
}
|
||||
if (d.process_cpu_percent != null) { processCpuSamples.push(Number(d.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-60); }
|
||||
if (d.process_rss_bytes != null) { processRssSamples.push(Number(d.process_rss_bytes)); processRssSamples = processRssSamples.slice(-60); }
|
||||
}
|
||||
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)];
|
||||
$("cache-outcome-chart-value").textContent = 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));
|
||||
$("cache-chart-value").textContent = 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));
|
||||
$("cache-transient-chart-value").textContent = 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));
|
||||
$("cache-cold-chart-value").textContent = coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled");
|
||||
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", d.platform || "");
|
||||
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 ? "" : new Date(Number(d.process_start_time_seconds) * 1000).toLocaleString(locale()));
|
||||
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));
|
||||
$("process-cpu-chart-value").textContent = d.process_cpu_percent == null ? "" : `${fmt(d.process_cpu_percent, 1)}%`;
|
||||
$("process-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples));
|
||||
$("process-rss-chart-value").textContent = d.process_rss_bytes == null ? "" : fmtBytes(d.process_rss_bytes);
|
||||
$("process-cpu-sparkline").closest(".mini-live-chart").hidden = d.process_cpu_percent == null;
|
||||
$("process-rss-sparkline").closest(".mini-live-chart").hidden = d.process_rss_bytes == null;
|
||||
updateCacheMemorySizing();
|
||||
const networkRateSupported = d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null;
|
||||
setOptionalMetric("system-network", networkRateSupported, networkRateSupported ? `${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 (d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null) {
|
||||
networkRxSamples.push(Number(d.network_rx_bytes_per_second)); networkTxSamples.push(Number(d.network_tx_bytes_per_second));
|
||||
networkRxSamples = networkRxSamples.slice(-60); networkTxSamples = networkTxSamples.slice(-60);
|
||||
}
|
||||
if (m.mean_duration_ms != null) { responseLatencySamples.push(Number(m.mean_duration_ms)); responseLatencySamples = responseLatencySamples.slice(-60); }
|
||||
}
|
||||
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));
|
||||
$("network-chart-value").textContent = networkRateSupported ? `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}` : "";
|
||||
$("network-rx-sparkline").closest(".mini-live-chart").hidden = !networkRateSupported;
|
||||
renderNetworkInterfaceCharts(d.network_interfaces || [], appendSample);
|
||||
$("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples));
|
||||
$("latency-chart-value").textContent = 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");
|
||||
$("listener-active").textContent = `${t("activeListeners")}: ${l.active_endpoints?.map(e => `${e.tls ? "HTTPS" : "HTTP"} ${e.address}${e.http3 ? " + H3/UDP" : ""}`).join(" | ") || `HTTP ${status.proxy_listen} / HTTPS ${status.tls_listen || t("notEnabled")}`}`;
|
||||
}
|
||||
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 };
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user