PageSourceSearch

https://fdcryptoalert-site.pages.dev/js/inactivity.js

js fdcryptoalert-site.pages.dev collected 2026-10-03 05:39:20 UTC 13,137 bytes, 278 lines download raw bytes

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.