feat(frontend): Monitoramento em tempo real (WebSocket via proxy SSE)

O RealtimeGateway (backend, desde a PHASE 13) autentica a conexão
socket.io via auth.token no handshake — um JWT bruto que um
EventSource/WebSocket do browser não tem como mandar sem passar por JS
legível no client, quebrando o princípio seguido em todo o resto do
frontend (token só existe no cookie httpOnly). Resolvido com um proxy:
apps/frontend/src/app/api/monitoring/stream/route.ts roda no servidor,
conecta no socket.io real com o access token do lado do servidor, e
reencaminha cada evento pro browser como Server-Sent Events — o
EventSource do client só precisa do cookie de sessão, nunca do token.

/app/monitoramento: badge de conexão, contadores de sessão (chamadas
criadas/atendidas/encerradas), filas ao vivo (QUEUE_MEMBER_COUNT), estado
de agentes ao vivo (AGENT_STATE_CHANGED, semeado do GET /agents inicial),
feed dos últimos 50 eventos. Menu "Monitoramento" vira 1 link direto em
vez de 5 sub-itens placeholder — o painel novo já cobre tudo numa página
só.

Testado ponta a ponta contra o pipeline real (não simulado): cliente
socket.io cru confirmou a API entregando o evento, curl -N confirmou o
proxy reencaminhando, e com a página aberta de verdade num browser
(Puppeteer) disparei POST /agents/me/login e /logout por fora — o badge
do agente mudou ao vivo e os eventos apareceram no feed sem recarregar a
página. Smoke test de regressão nas 19 telas anteriores, todas 200.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-29 19:17:39 -03:00
parent 7f56c96454
commit 3479dbd007
11 changed files with 481 additions and 16 deletions

63
TODO.md
View File

@@ -1113,6 +1113,69 @@ Troncos, Discador > Lista de Bloqueio (agente.md secao 50-51, 41-42, 71,
decisão de escopo pra manter o formulário enxuto nesta primeira
versão
## PHASE 32 — Frontend: Monitoramento em tempo real
(agente.md secao 54-55, 161)
- [x] **Achado de arquitetura, resolvido antes de codar**: o
`RealtimeGateway` (backend, já existia desde a PHASE 13) autentica a
conexão socket.io via `auth.token` no handshake — um JWT bruto. Um
`EventSource`/`WebSocket` do browser não tem como mandar esse token
sem ele passar por JS legível no client, quebrando o mesmo princípio
seguido em todo o resto do frontend (o access token só existe no
cookie httpOnly). Resolvido com um proxy: `apps/frontend/src/app/
api/monitoring/stream/route.ts` roda no servidor (runtime Node.js),
conecta no socket.io real com o access token do lado do servidor
(`socket.io-client`, dependência nova só usada aqui), e reencaminha
cada evento pro browser como Server-Sent Events — o `EventSource` do
client só precisa do cookie de sessão (same-origin, automático),
nunca do token. Como bônus, elimina qualquer necessidade de mexer em
`CORS_ORIGIN` (a conexão socket.io real é servidor-servidor, nunca
passa pelo browser).
- [x] `/app/monitoramento`: badge de conexão (Conectando/Ao vivo/
Reconectando), 3 contadores de sessão (chamadas criadas/atendidas/
encerradas, zeram a cada reload — não são um total histórico),
grade de filas ao vivo (contagem de espera via `QUEUE_MEMBER_COUNT`,
traço fantasma até o primeiro evento — sem endpoint de "contagem
atual" pra semear um valor inicial), lista de agentes com estado ao
vivo (semeada do `GET /agents` inicial, atualizada via
`AGENT_STATE_CHANGED`), e feed dos últimos 50 eventos com descrição
legível por tipo.
- [x] Menu Tenant: "Monitoramento" deixa de ter 5 sub-itens placeholder
(Campanhas/Filas/Agentes/Ramais/Troncos) e vira 1 link direto — o
painel novo já cobre filas+agentes+eventos gerais numa página só;
abrir em 5 rotas separadas duplicaria a conexão SSE sem necessidade
real (mesma decisão já tomada nas Relatórios: 1 `href` por item de
menu, nunca vários apontando pro mesmo lugar).
- [x] `agent`/`queue` do mod_callcenter chegam como `"<id>@<domain>"`
(secao 45, 50) — `stripDomain()` novo em `lib/realtime-types.ts`
separa o id antes de resolver nome; `AGENT_STATE_CHANGED` (publicado
pela própria API, não pelo FreeSWITCH) já vem com `agentId` cru, sem
sufixo.
- [x] Testado ponta a ponta contra o pipeline real (não simulado): um
cliente socket.io cru confirmou primeiro que a API já entrega o
evento certo (`AGENT_STATE_CHANGED`) ao vivo; a mesma verificação
via `curl -N` direto no proxy SSE confirmou o reencaminhamento;
depois, com a página `/app/monitoramento` aberta de verdade num
browser (Puppeteer), disparado `POST /agents/me/login` e
`/logout` por fora — o badge do agente mudou de Offline pra
"Disponível" e voltou, e os dois eventos apareceram no feed ao
vivo, tudo em tempo real sem recarregar a página. Smoke test de
regressão nas 19 telas anteriores do tenant + platform, todas 200
(a rota de monitoramento mantém uma conexão aberta de propósito,
então usa `domcontentloaded` em vez de `networkidle0` no teste).
- [ ] Sem reconciliação/snapshot ao conectar (secao 55: "estado inicial
completo, não só eventos a partir de agora") — filas começam sem
dado até o primeiro `QUEUE_MEMBER_COUNT`; agentes começam certos
porque semeiam do `GET /agents` inicial, mas filas não têm
equivalente (nenhum endpoint retorna "contagem atual" fora do
próprio stream de eventos)
- [ ] Sem monitoramento de ramais (registro/busy, secao 55 completa) nem
de campanhas/troncos em tempo real — cobertos só pelos relatórios
de período, não por esta tela
- [ ] Reconexão do `EventSource` é o comportamento padrão do browser
(tenta de novo sozinho); o lado do servidor limita a 5 tentativas de
reconexão do socket.io real antes de fechar o stream, mas não há
backoff exponencial nem um teste de queda de rede prolongada
---
## Riscos conhecidos