Timeout de Inatividade — Documentação Técnica
1. Visão Geral
O sistema implementa logout automático por inatividade em dois contextos com thresholds diferentes:
| Contexto | Timeout | Warning | Motivo |
|---|---|---|---|
Sistema Admin (olp_auth) | 3 horas | 2h55min | Estações compartilhadas em escolas |
Mural Olímpico (olp_mural) | 1 hora | 59min | Sessões efêmeras, dispositivos compartilhados (celulares de família) |
2. Constantes e Thresholds
Sistema Admin
typescript
const INACTIVITY_LIMIT_MS = 3 * 60 * 60 * 1000; // 3 horas → logout
const WARNING_BEFORE_MS = 5 * 60 * 1000; // 5 min antes → aviso (2h55m)
const THROTTLE_MS = 60 * 1000; // 60s entre gravações no localStorage
const CHECK_INTERVAL_MS = 60 * 1000; // 60s entre verificações do timer
const STORAGE_KEY = 'olp_last_activity'; // Chave no localStoragePortal Aluno/Responsável
typescript
const TIMEOUT_MS = 3_600_000; // 1 hora → logout
const WARNING_MS = 3_540_000; // 59min → aviso
const THROTTLE_MS = 30_000; // 30s entre gravações (sessões curtas)
const CHECK_INTERVAL_MS = 30_000; // 30s entre verificações
const LS_KEY = 'olp_mural_last_activity';Por que valores diferentes?
| Aspecto | Admin | Portal |
|---|---|---|
| Timeout | 3h — reuniões, intervalos longos | 1h — sessões rápidas de consulta |
| Throttle | 60s — sessões longas | 30s — sessões curtas, resolução maior |
| Check | 60s | 30s — logout mais preciso |
3. Fluxo Técnico
┌─────────────────────────────────────────────────────────────┐
│ NAVEGADOR (aba OLP) │
│ │
│ Eventos DOM capturados: │
│ mousemove, click, keydown, scroll, touchstart │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Throttle (Ns) │ ← Ignora eventos se <N segundos │
│ │ useRef(lastSave)│ desde a última gravação │
│ └────────┬─────────┘ │
│ │ (passou N segundos) │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ localStorage.setItem( │ ← Apenas um número (epoch) │
│ │ 'olp_[portal_]last_...'│ Nenhum dado sensível │
│ │ Date.now().toString() │ │
│ │ ) │ │
│ └──────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ │
│ │ setInterval (Ns) │ ← Lê localStorage │
│ │ │ │
│ │ elapsed = now - lastAct │ │
│ │ │ │
│ │ if elapsed ≥ TIMEOUT: │──→ LOGOUT (invoca logout) │
│ │ elif elapsed ≥ WARNING: │──→ showWarning = true │
│ │ else: │──→ (nada) │
│ └──────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ │
│ │ Dialog de aviso │ ← Aparece quando │
│ │ │ showWarning = true │
│ │ [Estou aqui!] │──→ Reseta timer (setItem now) │
│ │ │ showWarning = false │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘Detalhes do Throttle
O throttle é implementado com useRef (não setTimeout/debounce):
typescript
const lastSaveRef = useRef(0);
const handleActivity = useCallback(() => {
const now = Date.now();
if (now - lastSaveRef.current < THROTTLE_MS) return; // Ignora
lastSaveRef.current = now;
localStorage.setItem(STORAGE_KEY, now.toString());
}, []);Impacto em performance:
- Zero chamadas HTTP (apenas localStorage)
- Zero re-renders React (useRef, não useState)
- CPU: ~0.001% (4-5 listeners passivos + 1 interval)
4. Segurança do localStorage
Dado Armazenado
Admin: olp_last_activity = "1709312400000"
Mural: olp_mural_last_activity = "1709312400000"Não há dado sensível. O valor é apenas um timestamp.
Persistência Proposital
O localStorage (e não sessionStorage) é usado para que o timer sobreviva a reloads (F5).
Cleanup
O localStorage é limpo em:
- Logout voluntário:
logoutPortal('logout')/handleLogoutremovem a chave - Logout por inatividade:
logoutPortal('inatividade')remove a chave
5. UX dos Avisos
Admin — "Tem alguém aí?" 👀
| Aspecto | Implementação |
|---|---|
| Emoji | 👀 |
| Título | "Tem alguém aí?" |
| Mensagem | "Você está ausente há quase 3 horas..." |
| Botão | "Estou aqui! Continuar 🙋" |
| Componente | src/components/inactivity-warning-dialog.tsx |
Portal — "Sessão expirando..." ⏳
| Aspecto | Implementação |
|---|---|
| Emoji | ⏳ |
| Título | "Sessão expirando..." |
| Mensagem | "Você está inativo há quase 1 hora..." |
| Botão | "Estou aqui! Continuar 🙋" |
| Componente | src/components/portal-inactivity-warning-dialog.tsx |
Ambos: ESC bloqueado, não fecha clicando fora, obriga clique no botão.
6. Backend: Motivo do Logout
O logout_portal aceita parâmetro motivo:
typescript
await logoutPortal('inatividade'); // ou 'logout'O backend registra no log:
json
{
"acao": "portal.logout",
"detalhes": {
"tipo": "logout",
"motivo": "inatividade",
"origem": "portal_publico"
}
}7. Arquivos Envolvidos
Sistema Admin
| Arquivo | Responsabilidade |
|---|---|
src/hooks/useInactivityTimeout.ts | Hook principal (3h) |
src/components/inactivity-warning-dialog.tsx | Dialog "Tem alguém aí?" |
src/App.tsx | Integra hook + dialog |
src/contexts/auth-context.tsx | Cleanup do localStorage |
Portal Aluno/Responsável
| Arquivo | Responsabilidade |
|---|---|
src/hooks/usePortalInactivityTimeout.ts | Hook principal (1h) |
src/components/portal-inactivity-warning-dialog.tsx | Dialog "Sessão expirando..." |
src/pages/portal/PortalEscolaPage.tsx | Integra hook + dialog |
src/hooks/usePortalEscola.ts | logoutPortal(motivo) + cleanup LS |
supabase/functions/portal-escola/index.ts | Aceita motivo no logout_portal |
8. Cross-Tab — Esclarecimento Arquitetural
O sistema NÃO implementa sincronização cross-tab dedicada. O localStorage compartilhado entre abas do mesmo domínio é um efeito colateral inerente da API.
9. Cenários de Teste Manual
Admin
| Cenário | Resultado Esperado |
|---|---|
| Usuário fica 2h55m sem mexer | Dialog "Tem alguém aí?" aparece |
| Usuário clica "Estou aqui!" | Timer reseta, dialog fecha |
| Usuário ignora o dialog por 5min | Logout automático, redirect para / |
Portal
| Cenário | Resultado Esperado |
|---|---|
| Aluno/responsável fica 59min sem mexer | Dialog "Sessão expirando..." aparece |
| Clica "Estou aqui!" | Timer reseta, dialog fecha |
| Ignora dialog por 1min | Logout automático, toast "Sessão encerrada por inatividade" |
| F5 após 50min | Timer não reseta (persiste via localStorage) |
| Logout voluntário | olp_mural_last_activity removido do localStorage |
| Log no backend | acao: "portal.logout", detalhes.motivo: "inatividade" |