Achado real reportado pelo usuário: "não achei como deixar o agente
online, tem algum perfil ou tipo de usuario especifico?" — não era
permissão nenhuma. POST /agents/me/login|pause|resume|logout sempre
funcionaram sem exigir permissão (só JwtAuthGuard, resolvem "meu
agente" pelo userId do próprio token). O problema: nenhuma tela chamava
esses endpoints — só existia o CRUD admin de Agentes (criar o vínculo
usuário+ramal), nunca um controle pro próprio usuário logado.
Adicionado GET /agents/me (404 = usuário sem Agent neste tenant, não um
erro — é assim que o widget decide se aparece) e GET /agents/me/pause-
reasons (motivos de pausa sem exigir agents.view, que listaria TODOS os
agentes do tenant — permissão que o role "agent" nunca teve e não devia
ganhar só pra isso).
AgentStatusWidget na topbar (visível em qualquer página do app do
tenant) só aparece pra quem tem Agent vinculado: Offline -> Entrar ->
Disponível -> Pausar (escolhe motivo) -> Em pausa -> Retomar/Sair.
Achado real construindo o widget: as Server Actions de login/logout/
pause/resume exportadas como arrow function que só repassavam
argumentos quebravam em runtime ("Server Actions must be async
functions") — o mesmo bug já documentado antes nesta sessão (wizard de
campanhas). Corrigido declarando como async function de verdade.
Testado ponta a ponta com Playwright, logado como um agente de teste
real (usuário novo, role "agent", Agent vinculado a um ramal real): o
fluxo completo (entrar/pausar/retomar/sair) funciona pela tela, sem
nenhuma chamada de API manual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
180 lines
9.3 KiB
Markdown
180 lines
9.3 KiB
Markdown
# Agentes, Tiers e Pausas
|
|
|
|
Agente.md secao 45-49 e 52. Completa o Call Center: depois desta fase, um
|
|
usuário autenticado consegue logar como agente, entrar numa fila real, se
|
|
pausar e voltar — tudo refletido de verdade no FreeSWITCH.
|
|
|
|
## Modelo
|
|
|
|
- `agents` (tenant-scoped, RLS): liga `User` (login) → `Extension` (ramal
|
|
SIP) → identidade de agente (`User`/`Agent`/`Extension` separados,
|
|
agente.md secao 45). `state` espelha o estado real (secao 46), nunca
|
|
escrito direto pela API — só via login/logout/pause/resume.
|
|
- `tiers`: Queue↔Agent (secao 52) — level/position espelham 1:1 o
|
|
mod_callcenter.
|
|
- `agent_sessions`: uma linha por ciclo login→logout.
|
|
- `agent_state_events`: histórico de transições de estado (auditoria).
|
|
- `pause_reasons`/`agent_pause_events` (secao 48).
|
|
|
|
## Mecanismo: 100% dinâmico, sem arquivo
|
|
|
|
Confirmado na fase anterior (Queues) via `help callcenter_config`: agentes e
|
|
tiers não têm representação em XML — só comandos ESL diretos
|
|
(`agent add/del/set status/set contact`, `tier add/del`). Isso muda o padrão
|
|
de sincronização: em vez de "regenerar todos os arquivos" (Trunks/Queues),
|
|
`apps/api` publica uma mensagem **por ação, com payload** (`{tenantId,
|
|
agentId, action}` em `b2bcall:agents:sync`; `{tenantId, queueId, agentId,
|
|
level, position, action}` em `b2bcall:tiers:sync`) — `b2bcall-fs-config`
|
|
recebe e aplica o comando correspondente.
|
|
|
|
## Achados reais confirmados manualmente antes de codar
|
|
|
|
Rodei cada comando contra o FreeSWITCH real antes de escrever qualquer
|
|
lógica de sync (mesmo método usado nas fases anteriores):
|
|
|
|
- `agent add` em cima de um agente que já existe: **erro**
|
|
("Agent already exist!") — não é idempotente. `tier add` duplicado: mesmo
|
|
erro ("Tier already exist!"). Ambos capturados e ignorados no sync
|
|
(`.catch(() => undefined)`), já que essa condição é esperada em qualquer
|
|
resync.
|
|
- `tier del`/`agent del` em algo que não existe: **não dá erro** (+OK) —
|
|
seguro chamar sem checar existência antes.
|
|
- `agent set status` só aceita 3 valores exatos: `Available`, `On Break`,
|
|
`Logged Out` — qualquer outro string dá `-ERR Invalid Agent Status!`
|
|
(testado deliberadamente). Os estados derivados de chamada do nosso enum
|
|
(RESERVED/RINGING/IN_CALL/WRAP_UP) não têm status próprio no
|
|
mod_callcenter — mapeiam pra "Available" (só o `state`, campo separado,
|
|
muda sozinho conforme a chamada progride — não escrevemos isso).
|
|
- **Achado de corrida real**, visto ao testar o fluxo completo: como um
|
|
agente só passa a existir no FreeSWITCH no login (`agent add` roda ali,
|
|
não na criação do registro `Agent`), atribuir um tier (`POST
|
|
/queues/:id/agents`) **antes** do primeiro login do agente falha
|
|
silenciosamente do lado do FreeSWITCH (`-ERR Agent not found!`, logado
|
|
mas não propagado como erro HTTP). Por isso o login **sempre
|
|
re-sincroniza todos os tiers do agente** depois de garantir que ele
|
|
existe — a auto-correção aconteceu exatamente assim no teste real.
|
|
|
|
## Fluxo de login (agente.md secao 47)
|
|
|
|
```
|
|
POST /agents/me/login
|
|
→ valida usuário (JWT) e ramal (Agent.extensionId precisa existir)
|
|
→ cria agent_session
|
|
→ agent add (idempotente via catch) + set contact (dial-string do ramal)
|
|
→ set status Available
|
|
→ re-sincroniza todos os tiers do agente
|
|
→ Agent.state = AVAILABLE
|
|
```
|
|
|
|
`POST /agents/me/logout|pause|resume` seguem o mesmo padrão — sempre sobre
|
|
o agente do **próprio usuário autenticado** (nunca um `agentId` arbitrário
|
|
do client, mesmo princípio de nunca confiar em tenant_id/ids sensíveis vindo
|
|
do frontend sem checar contra o JWT).
|
|
|
|
## Verificado ponta a ponta
|
|
|
|
```
|
|
POST /extensions {"number":"2000",...}
|
|
POST /agents {userId, extensionId, name}
|
|
POST /queues {"name":"Suporte"}
|
|
POST /queues/:queueId/agents {agentId} → tier add falha (agente ainda nao existe)
|
|
POST /agents/me/login → agent add + contact + status Available
|
|
→ tier re-sync (dessa vez funciona)
|
|
|
|
callcenter_config agent list
|
|
→ status=Available, contact={ignore_early_media=true}user/2000@b2bcall.local ✓
|
|
callcenter_config tier list
|
|
→ queue|agent com state=Ready ✓
|
|
|
|
POST /agents/me/pause {pauseReasonId} → status=On Break ✓
|
|
POST /agents/me/resume → status=Available ✓
|
|
POST /agents/me/logout → status=Logged Out ✓
|
|
```
|
|
|
|
Todos os 4 estados confirmados batendo entre o banco (`Agent.state`) e o
|
|
FreeSWITCH (`agent list`).
|
|
|
|
## O que falta
|
|
|
|
- Estados derivados de chamada (RINGING, IN_CALL, WRAP_UP, RESERVED) — o
|
|
mecanismo de entrega do `callcenter::info` foi corrigido e confirmado
|
|
funcionando (ver seção abaixo), mas gravar esses estados de volta em
|
|
`Agent.state` ainda não foi implementado — hoje o campo só muda via
|
|
login/logout/pause/resume. O estado real do mod_callcenter (`CC-Agent-State`:
|
|
`Waiting`/`Receiving`/...) já está disponível em tempo real via WebSocket
|
|
(docs/REALTIME.md); persistir isso em `Agent.state` fica pra quando o
|
|
Predictive Engine/CDR precisarem consultar esse histórico.
|
|
- Tela do agente (secao 49) — fase Frontend.
|
|
- ~~Quota de agentes~~ — implementada na fase Plans/Entitlements (ver
|
|
docs/ENTITLEMENTS.md), `assertQuota` chamado antes de criar.
|
|
- `PauseReason.maxDuration` existe no modelo mas não é aplicado
|
|
automaticamente ainda (ninguém força o fim da pausa ao expirar).
|
|
- **Achado ao testar a fase Realtime Monitoring, sistêmico (não é só
|
|
Agent)**: `@@unique([tenantId, userId])` em `Agent` não exclui
|
|
`deletedAt` — apagar um agente (soft delete) e tentar criar outro pro
|
|
mesmo usuário no mesmo tenant falha com `Unique constraint failed`,
|
|
porque a linha apagada continua ocupando o slot único indefinidamente.
|
|
Confirmado ao vivo durante o teste desta fase. O mesmo padrão
|
|
(`@@unique` combinado com soft delete, sem excluir `deletedAt`) existe
|
|
em pelo menos mais 4 models: `Extension` (`tenantId, number`), `Trunk`
|
|
(`tenantId, name`), `Queue` (`tenantId, name`), `PauseReason`
|
|
(`tenantId, code`) — todos vão ter o mesmo problema (não dá pra reusar
|
|
um número/nome/código depois de apagar). Precisa de um índice único
|
|
parcial (`WHERE deleted_at IS NULL`) em cada um — não corrigido nesta
|
|
fase (é uma migration própria tocando 5 tabelas, fora do escopo de
|
|
Realtime Monitoring); fica registrado aqui pra não se perder.
|
|
|
|
## Correção: eventos CUSTOM (`callcenter::info`) nunca chegavam
|
|
|
|
Achado ao testar esta fase ponta a ponta com uma chamada real de teste
|
|
(`originate null/dummy &callcenter(fila@dominio)`): nenhum evento CUSTOM
|
|
aparecia em `b2bcall-fs-events`, apesar do agente ser ofertado a chamada de
|
|
verdade (`agent list` mostrava `last_offered_call` mudando). Causa raiz:
|
|
`event_json(...SUBSCRIBED_EVENTS)` mandava `"CUSTOM"` como último argumento
|
|
do comando `event json`, sem nenhum subclass depois — o mod_event_socket do
|
|
FreeSWITCH exige que os subclasses (`callcenter::info`, `sofia::register`,
|
|
`sofia::gateway_state`, ...) venham logo depois do token `CUSTOM` no mesmo
|
|
comando, senão zero eventos CUSTOM são entregues (de qualquer subclass).
|
|
Afetava também `sofia::gateway_state` (ver correção equivalente em
|
|
docs/TRUNKS.md).
|
|
|
|
Corrigido separando `PLAIN_EVENTS` (nomes normais, cada um vira um listener
|
|
`.on()`) de `CUSTOM_SUBCLASSES` (só compõem o comando `event json`, nunca
|
|
viram listener — o client ESL sempre emite `"CUSTOM"` como nome de evento,
|
|
com o subclass real no header `Event-Subclass`).
|
|
|
|
De quebra, corrigido também o nome de campo errado em
|
|
`normalizeCustomEvent`: o código lia `CC-Agent-Status`, que não existe — o
|
|
campo real é `CC-Agent-State`. Os `CC-Action` reais observados no teste
|
|
(úteis pro monitoramento de filas, agente.md secao 54): `agent-offering`,
|
|
`agent-state-change`, `bridge-agent-fail`, `member-queue-end` (com
|
|
`CC-Cause`/`CC-Cancel-Reason` e timestamps de entrada/saída — abandono vs.
|
|
atendida), `members-count` (contagem ao vivo de chamadas esperando por
|
|
fila). Ver `packages/telephony/src/normalize-event.ts`.
|
|
|
|
## Widget de status na topbar (PHASE 62)
|
|
|
|
Achado real reportado pelo usuário: "não achei como deixar o agente
|
|
online, tem algum perfil ou tipo de usuário específico?" — não era
|
|
permissão nenhuma: `POST /agents/me/login|pause|resume|logout`
|
|
(`apps/api/src/agents/agents-me.controller.ts`) sempre funcionaram sem
|
|
exigir permissão nenhuma (só `JwtAuthGuard`, resolvem "meu agente" via
|
|
`userId` do próprio token — nunca aceitam um `agentId` do client). O
|
|
problema era que NENHUMA tela chamava esses endpoints — só existia o CRUD
|
|
admin de Agentes (criar o vínculo usuário+ramal em Call Center >
|
|
Agentes).
|
|
|
|
Adicionado `GET /agents/me` (404 = usuário sem Agent neste tenant, não um
|
|
erro) e `GET /agents/me/pause-reasons` (motivos de pausa sem exigir
|
|
`agents.view`, que listaria TODOS os agentes do tenant — permissão que o
|
|
role "agent" nunca teve e não devia ganhar só pra isso). `AgentStatusWidget`
|
|
na topbar (visível em qualquer página do app do tenant, `apps/app/layout.tsx`
|
|
busca `/agents/me` uma vez por request) só aparece pra quem tem Agent
|
|
vinculado: Offline → "Entrar" → Disponível → "Pausar" (escolhe motivo) →
|
|
Em pausa → "Retomar"/"Sair".
|
|
|
|
Testado ponta a ponta com Playwright, logado como um agente de teste
|
|
real (usuário novo, role "agent", Agent vinculado a um ramal real): o
|
|
fluxo completo (entrar/pausar/retomar/sair) funciona pela tela, sem
|
|
nenhuma chamada de API manual.
|