(() => { "use strict"; const SEND_INTERVAL = 60_000; const RETRY_INTERVAL = 30_000; let lastSentAt = -Infinity; let pendingActivityAt = null; let activityTimer = null; let expiryTimer = null; let activityInFlight = false; let statusInFlight = false; let stopped = false; let idleLimit = 2 * 60 * 60 * 1000; let lastStatusAt = 0; async function sessionRequest(path, options = {}) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 15_000); try { const response = await fetch(path, { credentials: "same-origin", cache: "no-store", ...options, signal: controller.signal }); const payload = response.ok ? await response.json() : null; return { status: response.status, ok: response.ok, payload }; } finally { clearTimeout(timer); } } function expired() { if (stopped) return; stopped = true; clearTimeout(activityTimer); clearTimeout(expiryTimer); window.location.replace("/login?reason=inactive"); } function acceptStatus(payload) { if (stopped) return; const seconds = Number(payload.expires_in_seconds); if (!Number.isFinite(seconds)) throw new Error("Invalid session deadline"); idleLimit = Math.max(1, Number(payload.idle_timeout_seconds) || 7200) * 1000; lastStatusAt = Date.now(); clearTimeout(expiryTimer); // Check the shared server session before redirecting: another tab may have // renewed it. Passive polling here must not extend its deadline. expiryTimer = setTimeout(checkSession, Math.max(250, seconds * 1000)); } async function checkSession() { if (stopped || statusInFlight) return; statusInFlight = true; try { const response = await sessionRequest("/api/hockey/auth/session"); if (response.status === 401) return expired(); if (!response.ok) throw new Error("Session status unavailable"); acceptStatus(response.payload); } catch (_) { // A temporary network/WFL failure must not log an operator out. if (!stopped) { clearTimeout(expiryTimer); expiryTimer = setTimeout(checkSession, RETRY_INTERVAL); } } finally { statusInFlight = false; } } function scheduleActivity(delay = 0) { if (stopped || activityInFlight || activityTimer !== null || pendingActivityAt === null) return; activityTimer = setTimeout(sendActivity, Math.max(delay, SEND_INTERVAL - (Date.now() - lastSentAt), 0)); } async function sendActivity() { activityTimer = null; if (stopped || activityInFlight || pendingActivityAt === null) return; const activityAt = pendingActivityAt; const idleMs = Math.max(0, Date.now() - activityAt); pendingActivityAt = null; if (idleMs >= idleLimit) { void checkSession(); return; } activityInFlight = true; lastSentAt = Date.now(); let retry = 0; try { const response = await sessionRequest("/api/hockey/auth/activity", { method: "POST", credentials: "same-origin", cache: "no-store", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ idle_ms: idleMs }), }); if (response.status === 401) return expired(); if (!response.ok) throw new Error("Session renewal unavailable"); acceptStatus(response.payload); } catch (_) { pendingActivityAt = Math.max(pendingActivityAt ?? activityAt, activityAt); retry = RETRY_INTERVAL; } finally { activityInFlight = false; scheduleActivity(retry); } } function recordActivity(event) { if (stopped || !event.isTrusted || document.visibilityState === "hidden") return; pendingActivityAt = Date.now(); scheduleActivity(); } ["pointerdown", "pointermove", "keydown", "wheel", "touchstart", "input"].forEach((type) => { window.addEventListener(type, recordActivity, { capture: true, passive: true }); }); const checkOnReturn = () => { if (document.visibilityState !== "hidden" && Date.now() - lastStatusAt >= SEND_INTERVAL) void checkSession(); }; window.addEventListener("focus", checkOnReturn); window.addEventListener("pageshow", checkOnReturn); window.addEventListener("online", () => { void checkSession(); scheduleActivity(); }); document.addEventListener("visibilitychange", checkOnReturn); void checkSession(); })();