1/** 2 * inactivity.js â Detector de inatividade para user-dashboard 3 * 4 * Exibe um aviso após INACTIVITY_MS de inatividade. 5 * Se o usuário não responder em WARNING_MS, encerra a sessão 6 * no backend (ended_reason: 'inactivity_timeout') e redireciona 7 * para a home no idioma configurado. 8 * 9 * Depende de: 10 * - API_URL (definido em user-dashboard.js) 11 * - SessionTracker.end() (definido em user-dashboard.js) 12 * - localStorage 'authToken', 'userId', 'userLanguage' 13 */ 14 15(function () { 16 'use strict'; 17 18 // ââ Configuração ââââââââââââââââââââââââââââââââââââââââââââ 19 const INACTIVITY_MS = 10 * 60 * 1000; // 10 minutos sem interação 20 const WARNING_MS = 30 * 1000; // 30 segundos para responder 21 22 // Mapa de idioma â URL da home 23 const HOME_URLS = { 24 'pt': 'pt-br.html', 25 'pt-br': 'pt-br.html', 26 'pt-BR': 'pt-br.html', 27 'en': 'en.html', 28 'es': 'es.html', 29 }; 30 31 // ââ Estado interno âââââââââââââââââââââââââââââââââââââââââââ 32 let _inactivityTimer = null; 33 let _warningTimer = null; 34 let _countdownInterval = null; 35 let _warningVisible = false; 36 let _destroyed = false; 37 38 // ââ Helpers ââââââââââââââââââââââââââââââââââââââââââââââââââ 39 function _getHomeUrl() { 40 const raw = (function () { 41 try { return localStorage.getItem('userLanguage') || navigator.language || 'pt-BR'; } 42 catch (_) { return 'pt-BR'; } 43 })(); 44 const key = raw.toLowerCase().startsWith('pt') ? 'pt-br' 45 : raw.toLowerCase().startsWith('en') ? 'en' 46 : raw.toLowerCase().startsWith('es') ? 'es' 47 : 'pt-br'; 48 return HOME_URLS[key] || 'pt-br.html'; 49 } 50 51 function _getToken() { 52 try { return localStorage.getItem('authToken') || ''; } catch (_) { return ''; } 53 } 54 55 function _getApiUrl() { 56 return (typeof API_URL !== 'undefined' ? API_URL : null) 57 || 'https://api.fdcryptoalert.com'; 58 } 59 60 // ââ Encerramento âââââââââââââââââââââââââââââââââââââââââââââ 61 async function _terminateAndRedirect() { 62 _clearAll(); 63 _hideModal(); 64 65 // Encerrar sessão no backend com reason 'inactivity_timeout' 66 try { 67 if (typeof SessionTracker !== 'undefined') { 68 await SessionTracker.end('inactivity_timeout'); 69 } else { 70 // Fallback manual caso SessionTracker não esteja disponÃvel 71 const sid = sessionStorage.getItem('dashSessionId'); 72 if (sid && _getToken()) { 73 await fetch(`${_getApiUrl()}/session/end`, { 74 method: 'POST', 75 headers: { 76 'Content-Type': 'application/json', 77 'Authorization': `Bearer ${_getToken()}` 78 }, 79 body: JSON.stringify({ session_id: sid, reason: 'inactivity_timeout' }), 80 keepalive: true 81 }); 82 } 83 } 84 } catch (_) {} 85 86 // Limpar credenciais 87 try { 88 localStorage.removeItem('authToken'); 89 localStorage.removeItem('userId'); 90 sessionStorage.clear(); 91 } catch (_) {} 92 93 // Mostrar tela de desconexão antes de redirecionar 94 _showDisconnectedScreen('inactivity_timeout'); 95 } 96 97 // ââ Timers âââââââââââââââââââââââââââââââââââââââââââââââââââ 98 function _clearAll() { 99 clearTimeout(_inactivityTimer); 100 clearTimeout(_warningTimer); 101 clearInterval(_countdownInterval); 102 _inactivityTimer = _warningTimer = _countdownInterval = null; 103 } 104 105 function _resetInactivityTimer() { 106 if (_destroyed || _warningVisible) return; 107 clearTimeout(_inactivityTimer); 108 _inactivityTimer = setTimeout(_onInactive, INACTIVITY_MS); 109 } 110 111 function _onInactive() { 112 if (_destroyed) return; 113 _warningVisible = true; 114 _showWarningModal(); 115 _warningTimer = setTimeout(_terminateAndRedirect, WARNING_MS); 116 } 117 118 // ââ Modal de aviso âââââââââââââââââââââââââââââââââââââââââââ 119 function _showWarningModal() { 120 if (document.getElementById('__inactivity-overlay')) return; 121
122 const overlay = document.createElement('div'); 123 overlay.id = '__inactivity-overlay'; 124 overlay.style.cssText = ` 125 position:fixed;inset:0;z-index:99999; 126 display:flex;align-items:center;justify-content:center; 127 background:rgba(0,0,0,.65);backdrop-filter:blur(4px); 128 animation:__ia-fadein .25s ease; 129 `; 130 131 overlay.innerHTML = ` 132 <style> 133 @keyframes __ia-fadein { from{opacity:0;transform:scale(.95)} to{opacity:1;transform:scale(1)} } 134 @keyframes __ia-countdown { 135 from { stroke-dashoffset: 0; } 136 to { stroke-dashoffset: 157; } 137 } 138 #__ia-box { 139 background:#1e2130;border:1px solid rgba(99,102,241,.35); 140 border-radius:16px;padding:36px 32px;max-width:360px;width:90%; 141 text-align:center;box-shadow:0 24px 60px rgba(0,0,0,.5); 142 position:relative; 143 } 144 #__ia-icon { font-size:2.8rem; margin-bottom:12px; } 145 #__ia-title { font-size:1.15rem;font-weight:700;color:#e2e8f0;margin:0 0 8px; } 146 #__ia-sub { font-size:.88rem;color:#94a3b8;margin:0 0 24px;line-height:1.5; } 147 #__ia-ring-wrap { position:relative;width:72px;height:72px;margin:0 auto 20px; } 148 #__ia-ring-bg { fill:none;stroke:rgba(99,102,241,.15);stroke-width:5; } 149 #__ia-ring-arc { 150 fill:none;stroke:#6366f1;stroke-width:5; 151 stroke-linecap:round;stroke-dasharray:157;stroke-dashoffset:0; 152 transform:rotate(-90deg);transform-origin:50% 50%; 153 animation:__ia-countdown ${WARNING_MS / 1000}s linear forwards; 154 } 155 #__ia-countdown-num { 156 position:absolute;top:50%;left:50%;transform:translate(-50%,-50%); 157 font-size:1.3rem;font-weight:700;color:#e2e8f0; 158 } 159 #__ia-btn { 160 display:inline-block;padding:11px 28px;border-radius:9px;border:none; 161 background:linear-gradient(135deg,#6366f1,#4f46e5);color:#fff; 162 font-size:.95rem;font-weight:600;cursor:pointer;transition:opacity .15s; 163 } 164 #__ia-btn:hover { opacity:.88; } 165 </style> 166 <div id="__ia-box"> 167 <div id="__ia-icon">â°</div> 168 <div id="__ia-title">Ainda está aÃ?</div> 169 <div id="__ia-sub">Você ficou inativo por um tempo.<br>Sua sessão será encerrada em:</div> 170 <div id="__ia-ring-wrap"> 171 <svg viewBox="0 0 54 54" width="72" height="72"> 172 <circle id="__ia-ring-bg" cx="27" cy="27" r="25"/> 173 <circle id="__ia-ring-arc" cx="27" cy="27" r="25"/> 174 </svg> 175 <div id="__ia-countdown-num">30</div> 176 </div> 177 <button id="__ia-btn">Continuar sessão</button> 178 </div> 179 `; 180 181 document.body.appendChild(overlay); 182 183 // Countdown numérico 184 let remaining = Math.ceil(WARNING_MS / 1000); 185 const numEl = overlay.querySelector('#__ia-countdown-num'); 186 _countdownInterval = setInterval(() => { 187 remaining -= 1; 188 if (numEl) numEl.textContent = remaining > 0 ? remaining : 0; 189 if (remaining <= 0) clearInterval(_countdownInterval); 190 }, 1000); 191 192 // Botão "Continuar" 193 overlay.querySelector('#__ia-btn').addEventListener('click', _onUserContinue); 194 } 195 196 function _hideModal() { 197 const el = document.getElementById('__inactivity-overlay'); 198 if (el) el.remove(); 199 } 200 201 // ââ Tela de desconexão (após timeout) ââââââââââââââââââââââââ 202 function _showDisconnectedScreen(reason) { 203 const _i = (typeof I18n !== 'undefined') ? I18n : null; 204 const _t = (k) => _i ? _i.t(k) : ({ session_ended_title: 'Sessão encerrada', session_ended_sub: 'Sua sessão foi encerrada. Faça login novamente para continuar.', session_ended_btn: 'Voltar à página inicial' }[k] || k); 205 const homeUrl = _i ? _i.homeUrl() : _getHomeUrl();
206 const isInactivity = reason === 'inactivity_timeout'; 207 const icon = isInactivity ? 'â°' : 'ð'; 208 const title = _t('session_ended_title'); 209 const sub = isInactivity 210 ? (_i ? _i.t('session_inactivity_sub') || _t('session_ended_sub') : 'Você foi desconectado por inatividade. Faça login novamente para continuar.') 211 : _t('session_ended_sub'); 212 const btn = _t('session_ended_btn'); 213 214 document.body.innerHTML = ` 215 <style> 216 * { margin:0;padding:0;box-sizing:border-box; } 217 body { 218 background:#0d1117;display:flex;align-items:center; 219 justify-content:center;min-height:100vh; 220 font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; 221 } 222 .__dc-card { 223 background:#1e2130;border:1px solid rgba(99,102,241,.25); 224 border-radius:20px;padding:48px 40px;max-width:400px;width:90%; 225 text-align:center;box-shadow:0 32px 80px rgba(0,0,0,.5); 226 animation:__dc-in .35s ease; 227 } 228 @keyframes __dc-in { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} } 229 .__dc-icon { font-size:3.5rem;margin-bottom:16px; } 230 .__dc-title { font-size:1.3rem;font-weight:700;color:#e2e8f0;margin-bottom:10px; } 231 .__dc-sub { font-size:.9rem;color:#6b7280;line-height:1.6;margin-bottom:28px; } 232 .__dc-btn { 233 display:inline-block;padding:13px 32px;border-radius:10px;border:none; 234 background:linear-gradient(135deg,#6366f1,#4f46e5);color:#fff; 235 font-size:1rem;font-weight:600;cursor:pointer; 236 text-decoration:none;transition:opacity .15s; 237 } 238 .__dc-btn:hover { opacity:.85; } 239 </style> 240 <div class="__dc-card"> 241 <div class="__dc-icon">${icon}</div> 242 <div class="__dc-title">${title}</div> 243 <div class="__dc-sub">${sub}</div> 244 <a href="${homeUrl}" class="__dc-btn">${btn}</a> 245 </div> 246 `; 247 } 248 249 // Expor para que user-dashboard.js possa acionar a mesma tela ao detectar 250 // sessão encerrada externamente via heartbeat 251 window.__showDisconnectedScreen = _showDisconnectedScreen; 252 253 // ââ Retomar sessão âââââââââââââââââââââââââââââââââââââââââââ 254 function _onUserContinue() { 255 clearTimeout(_warningTimer); 256 clearInterval(_countdownInterval); 257 _warningTimer = _countdownInterval = null; 258 _warningVisible = false; 259 _hideModal(); 260 _resetInactivityTimer(); 261 } 262 263 // ââ Inicialização ââââââââââââââââââââââââââââââââââââââââââââ 264 function init() { 265 const EVENTS = ['mousemove', 'keydown', 'mousedown', 'touchstart', 'scroll', 'click']; 266 EVENTS.forEach(e => document.addEventListener(e, _resetInactivityTimer, { passive: true })); 267 _resetInactivityTimer(); 268 console.log('[Inactivity] Monitor iniciado â inatividade:', INACTIVITY_MS / 60000, 'min | aviso:', WARNING_MS / 1000, 's'); 269 } 270 271 // Inicializa após o DOM estar pronto 272 if (document.readyState === 'loading') { 273 document.addEventListener('DOMContentLoaded', init); 274 } else { 275 init(); 276 } 277 278})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.