Files
RustProxy/src/admin/web/app/stream-dashboard.js
T
2026-10-25 09:58:39 +01:00

116 lines
7.1 KiB
JavaScript

window.rustProxyStreamDashboard = (() => {
const ids = ["stream_overview", "stream_traffic", "stream_sessions", "stream_backend_health"];
const history = { last: null, rx: [], tx: [] };
let wasVisible = false;
const card = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
const set = (id, value) => { const node = $(id); if (node) node.textContent = value; };
const trafficRates = (rx, tx) => {
set("stream-dashboard-rx-rate", rx);
set("stream-dashboard-tx-rate", tx);
};
function clearTraffic() {
history.last = null; history.rx.length = 0; history.tx.length = 0;
$("stream-dashboard-rx")?.setAttribute("points", "0,55 300,55");
$("stream-dashboard-tx")?.setAttribute("points", "0,55 300,55");
}
function syncVisibility(hiddenOverride = null) {
const prefs = window.rustProxyPreferences?.current?.();
const hidden = new Set(hiddenOverride ?? window.rustProxyInlineLayout?.currentDraft?.()?.hidden
?? prefs?.dashboard_hidden_widgets ?? ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets);
for (const id of ids) if (card(id)) card(id).hidden = hidden.has(id);
if (card("stream_traffic")?.hidden && wasVisible) clearTraffic();
wasVisible = !card("stream_traffic")?.hidden;
}
function line(points) {
const peak = Math.max(1, ...history.rx, ...history.tx);
return points.map((value, i) => `${i * 300 / Math.max(1, points.length - 1)},${55 - value / peak * 50}`).join(" ");
}
function render(status, hiddenOverride = null, recordTrafficSample = true) {
syncVisibility(hiddenOverride);
if (!status || currentPanel !== "overview" || ids.every(id => card(id)?.hidden)) return;
const configured = envelope?.config?.streams || status?.streams?.desired || [];
const active = (status?.streams?.active || []).filter(item => item.enabled !== false);
const metrics = status?.streams?.metrics || {};
if (!configured.some(item => item.enabled !== false) && !active.length) {
for (const name of ["overview", "sessions", "health", "traffic"]) {
if (!card(({ overview: "stream_overview", sessions: "stream_sessions", health: "stream_backend_health", traffic: "stream_traffic" })[name])?.hidden) {
set(`stream-dashboard-${name}`, "-");
if (name === "traffic") trafficRates("-", "-");
else set(`stream-dashboard-${name}-detail`, t("streamWidgetUnavailable"));
}
}
if (!card("stream_traffic")?.hidden) clearTraffic();
return;
}
if (!card("stream_overview")?.hidden) {
const desired = configured.filter(item => item.enabled !== false);
const attention = desired.filter(item => !active.some(a => a.id === item.id)).length +
active.filter(item => (metrics[item.id]?.backends || []).length &&
!(metrics[item.id].backends || []).some(b => b.enabled && !b.drain && b.status !== "unhealthy")).length;
set("stream-dashboard-overview", `${active.length} / ${desired.length}`);
set("stream-dashboard-overview-detail", `${t("streamWidgetActiveConfigured")} · ${attention} ${t("streamWidgetAttention")}`);
}
if (!card("stream_sessions")?.hidden) {
const sum = protocol => active.filter(item => item.protocol === protocol)
.reduce((count, item) => count + (Number(metrics[item.id]?.active) || 0), 0);
const tcp = sum("tcp"), udp = sum("udp");
set("stream-dashboard-sessions", fmt(tcp + udp));
set("stream-dashboard-sessions-detail", `TCP ${fmt(tcp)} · UDP ${fmt(udp)}`);
}
if (!card("stream_backend_health")?.hidden) {
const backends = active.flatMap(item => metrics[item.id]?.backends || []);
const healthy = backends.filter(b => b.status === "healthy").length;
const unhealthy = backends.filter(b => b.status === "unhealthy").length;
const unverified = backends.filter(b => b.status === "unverified" || b.status === "checking").length;
const emptyPools = active.filter(item => (metrics[item.id]?.backends || []).length > 0 &&
!(metrics[item.id].backends || []).some(b => b.enabled && !b.drain && b.status !== "unhealthy")).length;
set("stream-dashboard-health", `${healthy} / ${unhealthy}`);
set("stream-dashboard-health-detail", `${t("streamWidgetHealthyUnhealthy")} · ${unverified} ${t("streamWidgetUnverified")} · ${emptyPools} ${t("streamWidgetEmptyPools")}`);
}
if (card("stream_traffic")?.hidden) return;
const views = active.map(item => metrics[item.id]).filter(Boolean);
if (!views.length) {
set("stream-dashboard-traffic", t("streamWidgetUnavailable"));
trafficRates("-", "-");
return;
}
if (!recordTrafficSample) {
if (history.rx.length) {
const rx = history.rx.at(-1), tx = history.tx.at(-1);
set("stream-dashboard-traffic", `${bytes(Math.round(rx))}/s`);
trafficRates(`${bytes(Math.round(rx))}/s`, `${bytes(Math.round(tx))}/s`);
} else {
set("stream-dashboard-traffic", "-");
trafficRates("-", "-");
}
return;
}
const rxTotal = views.reduce((sum, view) => sum + (Number(view.bytes_in) || 0), 0);
const txTotal = views.reduce((sum, view) => sum + (Number(view.bytes_out) || 0), 0);
const now = performance.now();
const sample = rustProxyRateSampling.difference(history.last, [rxTotal, txTotal], now, rustProxyRateSampling.cadence("overview_seconds"));
const pausedRate = history.last !== null && sample.deltas === null;
history.last = sample.next;
const rx = sample.deltas ? sample.deltas[0] / sample.seconds : 0;
const tx = sample.deltas ? sample.deltas[1] / sample.seconds : 0;
if (!pausedRate) {
history.rx.push(rx); history.tx.push(tx);
if (history.rx.length > window.rustProxyOverviewSamples.getLimit()) { history.rx.shift(); history.tx.shift(); }
}
set("stream-dashboard-traffic", pausedRate ? "-" : `${bytes(Math.round(rx))}/s`);
trafficRates(pausedRate ? "-" : `${bytes(Math.round(rx))}/s`, pausedRate ? "-" : `${bytes(Math.round(tx))}/s`);
$("stream-dashboard-rx")?.setAttribute("points", line(history.rx));
$("stream-dashboard-tx")?.setAttribute("points", line(history.tx));
window.rustProxyChartScale.axes("stream-dashboard-rx", Math.max(1, ...history.rx, ...history.tx),
"bytes/s", history.rx.length, { top: 5 / 60, bottom: 55 / 60 });
}
function reset() { clearTraffic(); wasVisible = false; }
window.rustProxyOverviewSamples.onChange(() => {
window.rustProxyOverviewSamples.trim(history.rx);
window.rustProxyOverviewSamples.trim(history.tx);
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") render(latestStatus, null, false);
});
return { syncVisibility, render, reset };
})();