fix: add background keepalive ping to sync frontend idle timer with server
Prevents session timeout for users who stay active on a single page (scrolling, reading) without making requests. Pings every 5 min only when interaction was detected since last sync — no false keep-alive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
const WARNING_LEAD_SECONDS = 120;
|
||||
const TICK_INTERVAL_MS = 1000;
|
||||
const MIN_IDLE_TIMEOUT = 180; // Don't show warning if timeout < 3 min
|
||||
const KEEPALIVE_INTERVAL_MS = 300_000; // Sync with server every 5 min (if active)
|
||||
|
||||
const INTERACTION_EVENTS = ['click', 'keydown', 'scroll', 'mousemove', 'touchstart'];
|
||||
const INTERACTION_THROTTLE_MS = 30_000; // Only reset every 30s to limit overhead
|
||||
@@ -235,6 +236,7 @@ const init = () => {
|
||||
// ── User interaction tracking ──────────────────────────────
|
||||
|
||||
let lastInteractionReset = Date.now();
|
||||
let hasInteractionSinceLastSync = false;
|
||||
|
||||
const onUserInteraction = () => {
|
||||
const now = Date.now();
|
||||
@@ -246,10 +248,47 @@ const init = () => {
|
||||
// Only reset timer when dialog is NOT showing.
|
||||
if (!isDialogOpen) {
|
||||
lastActivity = now;
|
||||
hasInteractionSinceLastSync = true;
|
||||
scheduleWarning();
|
||||
}
|
||||
};
|
||||
|
||||
// ── Background keepalive ────────────────────────────────────
|
||||
// Periodically syncs the server-side idle timer when user interaction
|
||||
// was detected since the last sync. Without this, a user who stays on
|
||||
// a single page (scrolling, reading, moving the mouse) would have
|
||||
// their server-side timer expire even though the frontend shows "active".
|
||||
|
||||
const keepaliveTimerId = setInterval(async () => {
|
||||
if (!hasInteractionSinceLastSync || isDialogOpen) {
|
||||
return;
|
||||
}
|
||||
hasInteractionSinceLastSync = false;
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
[config.csrfKey]: config.csrfToken,
|
||||
});
|
||||
|
||||
const response = await fetch(config.pingUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (typeof data.idle_timeout_seconds === 'number' && data.idle_timeout_seconds > 0) {
|
||||
config.idleSeconds = data.idle_timeout_seconds;
|
||||
}
|
||||
} catch {
|
||||
// Network error — silently ignore, warning dialog will handle expiry.
|
||||
}
|
||||
}, KEEPALIVE_INTERVAL_MS);
|
||||
|
||||
// ── Focus trap (keyboard) ──────────────────────────────────
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
|
||||
Reference in New Issue
Block a user