Files
RustProxy/src/admin/web/app/refresh.js
T
2026-10-14 11:51:37 +02:00

205 lines
9.1 KiB
JavaScript

const REFRESH_DEFAULTS = Object.freeze({
live_enabled: true, overview_seconds: 3, traffic_seconds: 5,
cache_seconds: 5, system_info_seconds: 60
});
const REFRESH_CHOICES = Object.freeze({
overview_seconds: [1, 2, 3, 5, 10], traffic_seconds: [0, 3, 5, 10, 30],
cache_seconds: [0, 3, 5, 10, 30], system_info_seconds: [0, 15, 30, 60, 300]
});
function normalizeRefreshPreferences(input = {}) {
const value = { ...REFRESH_DEFAULTS };
if (typeof input.live_enabled === "boolean") value.live_enabled = input.live_enabled;
for (const [key, allowed] of Object.entries(REFRESH_CHOICES)) {
if (allowed.includes(input[key])) value[key] = input[key];
}
return value;
}
const rustProxyRefresh = (() => {
const tasks = new Map();
let ready = false, options = { ...REFRESH_DEFAULTS }, panel = currentPanel;
let settingsTab = "listeners", watchingVisibility = false;
let sessionReady = true, visibleResumeHandled = false;
const visible = () => !document.hidden;
function allowed(task) {
return ready && authenticated && sessionReady && visible() && options.live_enabled &&
task.interval() > 0 && (!task.active || task.active());
}
function clear(task, abort = false) {
if (task.timer !== null) clearTimeout(task.timer);
task.timer = null;
if (abort) {
++task.generation;
task.controller?.abort();
}
}
function schedule(task, delay) {
if (task.timer !== null || task.inFlight || !allowed(task)) return;
task.timer = setTimeout(() => { task.timer = null; void execute(task, false); }, delay);
}
function execute(task, manual) {
if (!authenticated || !sessionReady || (!manual && !allowed(task))) return Promise.resolve();
if (task.inFlight) {
if (manual) task.queued = true;
return task.inFlight;
}
clear(task);
const controller = new AbortController(), generation = task.generation, epoch = authEpoch;
const panelAtStart = panel, tabAtStart = settingsTab;
task.controller = controller;
const isCurrent = () => authenticated && epoch === authEpoch && !controller.signal.aborted &&
generation === task.generation && (manual ? panel === panelAtStart && settingsTab === tabAtStart : allowed(task));
const promise = Promise.resolve().then(() => task.run({ signal: controller.signal, isCurrent }));
task.inFlight = promise.catch(error => {
if (error?.name !== "AbortError" && isCurrent()) {
if (task.onError) task.onError(error);
else console.warn(`Refresh ${task.id} failed:`, error);
}
}).finally(() => {
task.inFlight = null;
if (task.controller === controller) task.controller = null;
if (task.queued && authenticated) {
task.queued = false;
void execute(task, true);
} else {
task.queued = false;
schedule(task, task.interval() * 1000);
}
});
return task.inFlight;
}
function register(id, spec) {
if (tasks.has(id)) throw new Error(`Duplicate refresh task: ${id}`);
if (typeof spec.run !== "function" || typeof spec.interval !== "function") throw new Error(`Invalid refresh task: ${id}`);
const task = { ...spec, id, timer: null, inFlight: null, controller: null, generation: 0, queued: false, enabled: false, seconds: null };
tasks.set(id, task);
sync();
return () => { clear(task, true); tasks.delete(id); };
}
function sync() {
for (const task of tasks.values()) {
const shouldRun = allowed(task), seconds = task.interval();
if (!shouldRun) {
if (task.enabled) clear(task, true);
task.enabled = false;
} else if (!task.enabled || task.seconds !== seconds) {
clear(task);
task.enabled = true;
schedule(task, 0);
}
task.seconds = seconds;
}
if (ready && authenticated && sessionReady && visible() && options.live_enabled) startLive();
else stopLive();
}
function suspendForSessionValidation() {
sessionReady = false;
for (const task of tasks.values()) {
clear(task, true); task.queued = false; task.enabled = false;
}
sync();
}
function completeSessionValidation(success) {
sessionReady = !!success;
sync();
}
function requestSessionResume() {
if (!ready || !authenticated || !visible() || visibleResumeHandled) return;
visibleResumeHandled = true;
void revalidateSession();
}
function onVisibility() {
if (visible()) requestSessionResume();
else { visibleResumeHandled = false; sync(); }
}
function onPageHide() {
visibleResumeHandled = false;
suspendForSessionValidation();
}
function onPageShow(event) { if (event.persisted) requestSessionResume(); }
function start() {
ready = true; sessionReady = true; visibleResumeHandled = false; panel = currentPanel;
settingsTab = window.rustProxySettings?.current?.() || "listeners";
if (!watchingVisibility) {
document.addEventListener("visibilitychange", onVisibility);
window.addEventListener("pagehide", onPageHide);
window.addEventListener("pageshow", onPageShow);
watchingVisibility = true;
}
sync();
}
function stop() {
ready = false; sessionReady = false; visibleResumeHandled = false;
for (const task of tasks.values()) {
clear(task, true); task.queued = false; task.enabled = false;
}
if (watchingVisibility) {
document.removeEventListener("visibilitychange", onVisibility);
window.removeEventListener("pagehide", onPageHide);
window.removeEventListener("pageshow", onPageShow);
watchingVisibility = false;
}
stopLive();
}
function update(value) { options = normalizeRefreshPreferences(value); sync(); }
function setPanel(value) {
panel = value; settingsTab = window.rustProxySettings?.current?.() || settingsTab;
for (const task of tasks.values()) if (task.active && !task.active()) task.queued = false;
sync();
}
function setSettingsTab(value) {
settingsTab = value;
for (const task of tasks.values()) if (task.active && !task.active()) task.queued = false;
sync();
}
function manual(id) {
const task = tasks.get(id);
if (!task) return Promise.reject(new Error(`Unknown refresh task: ${id}`));
return execute(task, true);
}
register("status-full", {
interval: () => 0, run: async ({ signal, isCurrent }) => {
const value = await api("/api/status", { signal });
if (isCurrent()) renderStatus(value, true);
}
});
register("status-fallback", {
interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
const value = await api("/api/status?view=live", { signal });
if (isCurrent()) renderLiveStatus(value);
}, onError: error => { if (error?.status !== 401) $("connection-label").textContent = t("disconnected"); }
});
register("logs-fallback", {
interval: () => options.overview_seconds, active: () => !liveConnected && (liveFallback || typeof EventSource === "undefined"), run: async ({ signal, isCurrent }) => {
const value = await api("/api/logs", { signal });
if (isCurrent()) renderLogs(value);
}
});
register("system-info", {
interval: () => options.system_info_seconds,
active: () => panel === "overview" || (panel === "settings" && ["system-info", "cache"].includes(settingsTab)),
run: async ({ signal, isCurrent }) => {
const value = await api("/api/system-info", { signal });
if (!isCurrent()) return;
latestStatus = { ...(latestStatus || {}), ...value };
if (latestStatus.metrics) renderDiagnostics(latestStatus, false, true);
window.rustProxySettings?.render();
}
});
register("tunnels", {
interval: () => 2, active: () => !!$("tunnels-dialog")?.open,
run: async context => { await loadTunnels(context); }
});
return {
register, manual, start, stop, sync, update, setPanel, setSettingsTab,
suspendForSessionValidation, completeSessionValidation,
current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null }))
};
})();
window.rustProxyRefresh = rustProxyRefresh;
$("overview-refresh")?.addEventListener("click", () => {
void Promise.all([rustProxyRefresh.manual("status-full"), rustProxyRefresh.manual("logs-fallback"), rustProxyRefresh.manual("system-info")]);
});
$("system-info-refresh")?.addEventListener("click", () => void rustProxyRefresh.manual("system-info"));