This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+381
View File
@@ -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 };
});
}