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"));