184 lines
11 KiB
JavaScript
184 lines
11 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
let loading = false, generation = 0, lastData = null;
|
|
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null;
|
|
const liveChartSamples = { applied: [], cache_hits: [], skipped: [], errors: [] };
|
|
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
|
|
const LIVE_CHART_STORAGE_KEY = "rustproxy.transform.liveChartSamples";
|
|
let liveChartLimit = 60;
|
|
const scope = () => $("transform-scope")?.value || "*";
|
|
const number = value => fmt(Number(value) || 0);
|
|
const percent = counters => {
|
|
const original = Number(counters?.original_bytes) || 0;
|
|
const saved = Number(counters?.saved_bytes) || 0;
|
|
return original > 0 ? `${fmt(saved / original * 100, 1)}%` : "0.0%";
|
|
};
|
|
const averageMs = counters => {
|
|
const examined = Number(counters?.examined) || 0;
|
|
const duration = Number(counters?.duration_us) || 0;
|
|
return examined ? `${fmt(duration / examined / 1000, 2)} ms` : "0.00 ms";
|
|
};
|
|
const emptyCounters = () => ({ examined: 0, applied: 0, skipped: 0, errors: 0, cache_hits: 0, original_bytes: 0, transformed_bytes: 0, saved_bytes: 0, duration_us: 0, by_kind: {}, reasons: {} });
|
|
function resetLiveChart(selected = scope()) {
|
|
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null;
|
|
for (const values of Object.values(liveChartSamples)) values.length = 0;
|
|
for (const id of ["transform-live-applied-line", "transform-live-cache-line", "transform-live-skipped-line", "transform-live-error-line"]) $(id)?.setAttribute("points", "0,170 900,170");
|
|
for (const id of ["transform-live-applied-rate", "transform-live-cache-rate", "transform-live-skipped-rate", "transform-live-error-rate"]) if ($(id)) $(id).textContent = "0.0/s";
|
|
if ($("transform-live-chart-value")) $("transform-live-chart-value").textContent = "-";
|
|
}
|
|
function redrawLiveChart() {
|
|
const peak = Math.max(1, ...liveChartSamples.applied, ...liveChartSamples.cache_hits, ...liveChartSamples.skipped, ...liveChartSamples.errors);
|
|
$("transform-live-applied-line")?.setAttribute("points", sparklinePoints(liveChartSamples.applied, 900, 180, 10, peak));
|
|
$("transform-live-cache-line")?.setAttribute("points", sparklinePoints(liveChartSamples.cache_hits, 900, 180, 10, peak));
|
|
$("transform-live-skipped-line")?.setAttribute("points", sparklinePoints(liveChartSamples.skipped, 900, 180, 10, peak));
|
|
$("transform-live-error-line")?.setAttribute("points", sparklinePoints(liveChartSamples.errors, 900, 180, 10, peak));
|
|
}
|
|
function setLiveChartLimit(value, persist = true) {
|
|
const parsed = Number(value);
|
|
liveChartLimit = LIVE_CHART_LIMITS.has(parsed) ? parsed : 60;
|
|
for (const key of Object.keys(liveChartSamples)) liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit);
|
|
if ($("transform-live-samples")) $("transform-live-samples").value = String(liveChartLimit);
|
|
if (persist) { try { localStorage.setItem(LIVE_CHART_STORAGE_KEY, String(liveChartLimit)); } catch { } }
|
|
redrawLiveChart();
|
|
}
|
|
function initializeLiveChartLimit() {
|
|
let saved = 60;
|
|
try { saved = Number(localStorage.getItem(LIVE_CHART_STORAGE_KEY) || 60); } catch { }
|
|
setLiveChartLimit(saved, false);
|
|
}
|
|
function renderLiveChart(counters) {
|
|
const selected = scope();
|
|
if (liveChartScope !== selected) resetLiveChart(selected);
|
|
const now = performance.now(), current = {
|
|
applied: Math.max(0, Number(counters.applied) || 0),
|
|
cache_hits: Math.max(0, Number(counters.cache_hits) || 0),
|
|
skipped: Math.max(0, Number(counters.skipped) || 0),
|
|
errors: Math.max(0, Number(counters.errors) || 0)
|
|
}, rates = { applied: 0, cache_hits: 0, skipped: 0, errors: 0 };
|
|
if (liveChartPrevious && liveChartPreviousAt != null) {
|
|
const elapsed = Math.max(.001, (now - liveChartPreviousAt) / 1000);
|
|
for (const key of Object.keys(rates)) if (current[key] >= liveChartPrevious[key]) rates[key] = (current[key] - liveChartPrevious[key]) / elapsed;
|
|
}
|
|
liveChartPrevious = current; liveChartPreviousAt = now;
|
|
for (const key of Object.keys(rates)) { liveChartSamples[key].push(rates[key]); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); }
|
|
redrawLiveChart();
|
|
$("transform-live-applied-rate").textContent = `${rates.applied.toFixed(1)}/s`;
|
|
$("transform-live-cache-rate").textContent = `${rates.cache_hits.toFixed(1)}/s`;
|
|
$("transform-live-skipped-rate").textContent = `${rates.skipped.toFixed(1)}/s`;
|
|
$("transform-live-error-rate").textContent = `${rates.errors.toFixed(1)}/s`;
|
|
$("transform-live-chart-value").textContent = `Σ ${(rates.applied + rates.cache_hits + rates.skipped + rates.errors).toFixed(1)}/s`;
|
|
}
|
|
function countersFor(data) {
|
|
if (scope() === "*") return data?.metrics?.total || emptyCounters();
|
|
return data?.metrics?.scopes?.find(row => row.vhost === scope()) || emptyCounters();
|
|
}
|
|
function configure(data) {
|
|
const selected = scope(), labels = new Map();
|
|
for (const vhost of data?.vhosts || []) {
|
|
const hosts = Array.isArray(vhost.hosts) ? vhost.hosts.join(", ") : "";
|
|
labels.set(vhost.id, hosts ? `${vhost.id} / ${hosts}` : vhost.id);
|
|
}
|
|
for (const row of data?.metrics?.scopes || []) if (!labels.has(row.vhost)) labels.set(row.vhost, row.vhost);
|
|
const items = [el("option", t("logAllVhosts"))]; items[0].value = "*";
|
|
for (const [id, label] of [...labels].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
const option = el("option", label); option.value = id; items.push(option);
|
|
}
|
|
$("transform-scope").replaceChildren(...items);
|
|
$("transform-scope").value = items.some(item => item.value === selected) ? selected : "*";
|
|
}
|
|
function renderTypes(counters) {
|
|
const kinds = [
|
|
["javascript", "JavaScript"], ["css", "CSS"], ["html", "HTML"], ["svg", "SVG"], ["json", "JSON"]
|
|
];
|
|
$("transform-type-rows").replaceChildren(...kinds.map(([key, label]) => {
|
|
const tr = el("tr"); tr.append(el("td", label), el("td", number(counters?.by_kind?.[key] || 0))); return tr;
|
|
}));
|
|
}
|
|
function renderReasons(counters) {
|
|
const entries = Object.entries(counters?.reasons || {}).sort((a, b) => b[1] - a[1]);
|
|
const target = $("transform-reason-list"); target.replaceChildren(...entries.map(([key, count]) => {
|
|
const tag = el("span", `${key}: ${number(count)}`, "cache-reason"); tag.title = key; return tag;
|
|
}));
|
|
$("transform-reasons-empty").hidden = entries.length > 0;
|
|
$("tp-skip-top").textContent = entries.length ? entries.slice(0, 2).map(([key, count]) => `${key}: ${number(count)}`).join(" · ") : t("transformNoSkipsShort");
|
|
}
|
|
function renderVhosts(data) {
|
|
const wanted = scope();
|
|
const rows = (data?.metrics?.scopes || []).filter(row => wanted === "*" || row.vhost === wanted);
|
|
$("transform-vhost-rows").replaceChildren(...rows.map(row => {
|
|
const tr = el("tr");
|
|
for (const value of [
|
|
row.vhost,
|
|
number(row.applied),
|
|
number(row.cache_hits),
|
|
number(row.skipped),
|
|
number(row.errors),
|
|
fmtBytes(row.saved_bytes || 0),
|
|
percent(row),
|
|
averageMs(row)
|
|
]) tr.append(el("td", value));
|
|
return tr;
|
|
}));
|
|
$("transform-vhosts-empty").hidden = rows.length > 0;
|
|
}
|
|
function render(data) {
|
|
lastData = data; configure(data);
|
|
const counters = countersFor(data);
|
|
renderLiveChart(counters);
|
|
$("tp-applied").textContent = number(counters.applied);
|
|
$("tp-examined").textContent = `${t("transformExamined")}: ${number(counters.examined)}`;
|
|
$("tp-cache-hits").textContent = number(counters.cache_hits);
|
|
$("tp-saved").textContent = fmtBytes(counters.saved_bytes || 0);
|
|
$("tp-byte-flow").textContent = `${fmtBytes(counters.original_bytes || 0)} → ${fmtBytes(counters.transformed_bytes || 0)}`;
|
|
$("tp-reduction").textContent = percent(counters);
|
|
$("tp-skipped").textContent = number(counters.skipped);
|
|
$("tp-errors").textContent = number(counters.errors);
|
|
$("tp-duration").textContent = `${t("transformAvgTime")}: ${averageMs(counters)}`;
|
|
const resources = data?.resources || {};
|
|
const reserved = Number(resources.memory_reserved_bytes), budget = Number(resources.memory_budget_bytes);
|
|
const inflight = Number(resources.inflight), maxInflight = Number(resources.max_inflight);
|
|
$("tp-memory").textContent = Number.isFinite(reserved) ? fmtBytes(reserved) : "-";
|
|
$("tp-memory-detail").textContent = Number.isFinite(budget) ? `${t("transformBudgetShort")}: ${fmtBytes(budget)}${budget > 0 && Number.isFinite(reserved) ? ` · ${fmt(reserved / budget * 100, 1)}%` : ""}` : "-";
|
|
$("tp-inflight").textContent = Number.isFinite(inflight) ? number(inflight) : "-";
|
|
$("tp-inflight-detail").textContent = Number.isFinite(maxInflight) ? `${t("cacheLimitShort")}: ${number(maxInflight)}` : "-";
|
|
renderTypes(counters); renderReasons(counters); renderVhosts(data);
|
|
$("transform-panel-state").textContent = `${t("cacheUpdated")}: ${formatTime(new Date())}`;
|
|
}
|
|
async function loadTransform({ signal, isCurrent }) {
|
|
if (!authenticated || currentPanel !== "transform" || loading) return;
|
|
loading = true;
|
|
const epoch = authEpoch, token = generation;
|
|
try {
|
|
const data = await api("/api/transform", { signal });
|
|
if (isCurrent() && token === generation) render(data);
|
|
} catch (error) {
|
|
if (error?.name !== "AbortError" && epoch === authEpoch && isCurrent()) {
|
|
$("transform-panel-state").textContent = error.message;
|
|
if (error.status === 401) lock();
|
|
}
|
|
} finally { loading = false; }
|
|
}
|
|
function refresh() { return rustProxyRefresh.manual("transform"); }
|
|
initializeLiveChartLimit();
|
|
$("transform-live-samples")?.addEventListener("change", event => setLiveChartLimit(event.currentTarget.value));
|
|
$("transform-scope")?.addEventListener("change", () => { resetLiveChart(); if (lastData) render(lastData); });
|
|
$("transform-refresh")?.addEventListener("click", () => { generation++; void refresh(); });
|
|
$("transform-panel-settings")?.addEventListener("click", () => { setPanel("settings"); window.rustProxySettings?.select("transform"); $("transform-form")?.scrollIntoView({ block: "start" }); });
|
|
rustProxyRefresh.register("transform", {
|
|
interval: () => 3,
|
|
active: () => currentPanel === "transform" && !!$("transform-live")?.checked,
|
|
run: loadTransform
|
|
});
|
|
$("transform-live")?.addEventListener("change", () => rustProxyRefresh.sync());
|
|
window.rustProxyTransformPanel = {
|
|
refresh,
|
|
reset() {
|
|
generation++; lastData = null; resetLiveChart();
|
|
$("transform-type-rows")?.replaceChildren();
|
|
$("transform-vhost-rows")?.replaceChildren();
|
|
$("transform-reason-list")?.replaceChildren();
|
|
if ($("transform-panel-state")) $("transform-panel-state").textContent = "";
|
|
}
|
|
};
|
|
})();
|