# Monitoramento em tempo real (WebSocket) Agente.md secao 54-55 (filas/ramais) e 161 (WebSocket multi-tenant). Esta fase entrega a infraestrutura de push em tempo real — o consumo visual ("Monitoramento → Filas/Ramais", cards coloridos) fica pra fase Frontend. ## Requisito central: tenant-scoped no servidor Secao 161: "Não transmitir tudo e filtrar somente no browser." O gateway (`apps/api/src/realtime/realtime.gateway.ts`) só tem **um** ponto de emissão, `broadcastToTenant()`, e ele sempre usa `server.to(room)` — nunca `server.emit()` global. Cada socket só entra na room do próprio tenant (`tenant:`) na conexão, nunca escolhe a room ele mesmo. ## Autenticação na conexão Socket.io não usa headers HTTP como uma request REST — o client manda o access token em `socket.handshake.auth.token` (não em `Authorization`). `RealtimeGateway.handleConnection`: 1. Rejeita (`disconnect(true)`) se não tiver token. 2. Valida o JWT (`verifyAccessToken`, mesmo helper usado no `JwtAuthGuard`). 3. Exige `tenantId` no token (mesmo princípio do `PermissionGuard`: nunca um tenant vindo do client, só do JWT já emitido por `/auth/select-tenant`). 4. Exige a permission `monitoring.view` (`userHasPermission`, já existia desde a fase RBAC — só não tinha nenhum consumidor ainda). 5. Só então entra na room do tenant. Testado: token ausente/inválido → desconectado na hora, nenhum evento vaza. Token válido → entra na room certa, recebe eventos do próprio tenant. ## Origem dos eventos Um único canal Redis, `b2bcall:events` (o mesmo que já existia desde a fase Event Socket) — o `RealtimeRedisBridge` (`apps/api`) assina esse canal com uma conexão dedicada (pub/sub exige conexão própria, não pode compartilhar com a usada pra comandos) e reencaminha pra `broadcastToTenant()` quando o evento já tem `tenantId` resolvido; sem tenant resolvido, o evento é descartado (nenhuma room saberia receber). Dois produtores publicam nesse canal: 1. **`b2bcall-fs-events`** (eventos do FreeSWITCH, normalizados — ver `packages/telephony/src/normalize-event.ts`). A maioria desses eventos (`callcenter::info`, `sofia::gateway_state`) não carrega `b2bcall_tenant_id` como channel variable (só existe a partir do Predictive Engine) — o tenant é resolvido pelo id do agente/fila embutido no nome FreeSWITCH (`@dominio`), com fan-out sobre os tenants ativos (mesmo padrão de `trunk-status.ts`), cacheado por id em `tenant-resolve.ts` (agente/fila nunca troca de tenant, cache nunca fica desatualizado). Isso só passou a funcionar depois da correção do bug de subscrição de eventos CUSTOM — ver docs/AGENTS.md e docs/EVENT_SOCKET.md. 2. **`apps/api`** (mudanças no nosso próprio `Agent.state`, via `realtime-publish.helper.ts`, chamado de dentro de `agents-me.controller.ts` em login/logout/pause/resume). Aqui o `tenantId` já vem direto do JWT da requisição HTTP — sem fan-out. ## Tipos de evento emitidos - `AGENT_STATE_CHANGED`: nosso enum próprio (`OFFLINE`/`AVAILABLE`/ `PAUSED`/...), só muda via login/logout/pause/resume. - `AGENT_STATUS_CHANGED`: `CC-Agent-State` bruto do mod_callcenter (`Waiting`/`Receiving`/...) — vocabulário diferente do de cima, não dá pra misturar (mesmo agente pode estar "AVAILABLE" no nosso enum e "Receiving" no mod_callcenter simultaneamente, description de momentos diferentes do ciclo de uma chamada). - `QUEUE_MEMBER_COUNT`: contagem ao vivo de chamadas esperando por fila (`CC-Count`) — a peça central da secao 54 ("Chamadas esperando"). - `QUEUE_MEMBER_LEFT`: uma chamada saiu da fila, com `cause`/`cancelReason` e timestamps de entrada/saída — atendida vs. abandonada, base pro cálculo futuro de Service Level/Abandon Rate. - `AGENT_OFFERED_CALL` / `AGENT_BRIDGE_FAILED`: uma chamada foi ofertada a um agente / falhou ao bridgear (ex.: `USER_NOT_REGISTERED`). - `CALL_CREATED`/`CALL_ANSWERED`/`CALL_ENDED`: já existiam desde a fase Event Socket, mas só carregam `tenantId` quando o `b2bcall_tenant_id` channel variable existir (chamadas puramente sintéticas de teste, como as usadas pra verificar esta fase, não têm — não chegam no WebSocket). ## Verificado ponta a ponta Client de teste com `socket.io-client`, autenticado com o JWT de um tenant de teste, na room `tenant:`: ``` login do agente → AGENT_STATE_CHANGED {state: "AVAILABLE"} pause → AGENT_STATE_CHANGED {state: "PAUSED"} resume → AGENT_STATE_CHANGED {state: "AVAILABLE"} logout → AGENT_STATE_CHANGED {state: "OFFLINE"} originate null/dummy &callcenter(fila@dominio), com agente logado numa fila: → QUEUE_MEMBER_COUNT {count: 3} → AGENT_STATUS_CHANGED {state: "Receiving"} → AGENT_OFFERED_CALL → AGENT_BRIDGE_FAILED {hangupCause: "USER_NOT_REGISTERED"} (ramal sem SIP real registrado) → QUEUE_MEMBER_LEFT {cause: "Cancel", cancelReason: "TIMEOUT"} → QUEUE_MEMBER_COUNT {count: 2} token ausente/inválido → socket desconectado na hora, nenhum evento recebido. ``` Todos chegaram só na room do tenant certo, com o `tenantId` batendo. ## O que falta - **Ramais/extensões (secao 55 completa)**: as cores dependem de "ocupado" (busy — CHANNEL_ANSWER/HANGUP tied a um ramal específico) e "offline" (registro SIP — `sofia::register`/`unregister`, cujo mecanismo de entrega agora funciona, mas nunca foi exercitado com um client SIP real nesta sessão). Além disso, resolver **qual ramal** um evento de canal pertence exige extrair o número de discagem dos headers (`Caller-Destination-Number`/ `Channel-Name`), não implementado ainda — e o mesmo limite de domínio compartilhado entre tenants já documentado em `docs/EXTENSIONS.md` afeta esse mapeamento. Não implementado; fica pra quando houver um client SIP de verdade pra testar. - **Persistir `AGENT_STATUS_CHANGED`/`QUEUE_MEMBER_COUNT` em `Agent.state` ou numa tabela de snapshot de fila**: hoje só passam pelo WebSocket, não gravam nada — suficiente pro requisito "tempo real", mas sem histórico consultável fora da tabela `agent_state_events` (que só reflete login/logout/pause/resume, não os estados derivados de chamada). - TME/TMA/Service Level/Abandon Rate (secao 54): dependem de CDR (fase futura, depois do Predictive Engine na ordem do agente.md secao 232) — `QUEUE_MEMBER_LEFT` já traz os dados brutos (`cause`, `cancelReason`, timestamps) que vão alimentar esse cálculo quando CDR existir. - Dashboard Tenant/Platform (secao 162-163) — fase Frontend. - Reconexão/reconciliação de estado ao reconectar o WebSocket (perder um evento por queda de rede momentânea não é recuperável hoje — o client precisa buscar o snapshot atual via REST depois de reconectar; não existe endpoint de snapshot ainda).