Files
b2bcall/docs/TROUBLESHOOTING.md
B2BCall Bootstrap 675806314d docs: registrar liberação de SIP/RTP no firewall e confusão de senhas
Atualiza TODO.md/SECURITY.md para refletir a regra de nftables real (SIP/
RTP liberados para RFC1918, AMI/ARI seguem bloqueados incondicionalmente)
e documenta em TROUBLESHOOTING.md o caso completo: timeout de registro de
softphone causado pelo firewall + confusão entre senha de login web e
senha SIP do ramal (duas credenciais diferentes).
2026-08-27 21:33:32 -03:00

8.2 KiB

B2BCall — Troubleshooting

Problemas reais encontrados durante o desenvolvimento deste projeto e como foram diagnosticados/resolvidos — mantido como referência para o futuro.

Softphone não registra ("timeout")

Sintoma: usuário criou um ramal, colocou usuário/senha num softphone real e o registro dava timeout (sem resposta nenhuma do servidor).

Causa raiz nº 1 — firewall: nftables bloqueava incondicionalmente SIP (5060/udp) e RTP (10000-20000) vindos da interface LAN (ens18) — regra criada na Fase 2 como proteção, mas que também bloqueia qualquer uso real, já que o OpenSIPS que ficaria na frente (docs/OPENSIPS.md) nunca foi implantado. Diagnosticado direto pelo contador da regra: nft list table inet b2bcall_fw mostrando udp dport 5060 ... drop com contador subindo a cada tentativa. Fix: liberar SIP/RTP para origem RFC1918 (rede privada), mantendo AMI/ARI sempre bloqueados — ver docs/SECURITY.md.

Causa raiz nº 2 — depois de liberar o firewall, "Failed to authenticate": o usuário estava digitando a senha de login web (a do admin@b2bcall. local) no campo de senha do softphone, em vez da senha SIP do próprio ramal (que é uma credencial completamente diferente, gerada por ramal). Confirmado ativando pjsip set logger on e vendo o REGISTER chegando e falhando na autenticação. Lição: deixar claro na interface (ou pelo menos na documentação) que são dois sistemas de credenciais distintos — fácil de confundir porque os dois são "a senha que o sistema gerou".

Melhoria feita por causa disso: antes, a única forma de recuperar uma senha SIP esquecida era resetá-la (invalidando a anterior). Agora dá para ver e editar a senha SIP diretamente no formulário de edição do ramal (GET /extensions/:id/password, decifra sob demanda) — diferente da senha de login (hash argon2id, irreversível por design), a senha SIP é cifrada (reversível) porque o próprio Asterisk precisa dela em texto puro para autenticar.

"Body cannot be empty when content-type is set to 'application/json'" em quase todo botão de ação

Sintoma: usuário real testando no navegador (não curl) relatou erro "body is empty" ao clicar em redefinir senha de um ramal. Investigação mostrou que o mesmo bug afetava todo botão de ação sem payload: disponibilizar agente, pausar/despausar, publicar dialplan, iniciar/ pausar/parar campanha, etc.

Causa: o cliente HTTP central do frontend (apps/frontend/src/lib/api-client.ts) sempre enviava Content-Type: application/json mesmo em requisições sem corpo. O Fastify rejeita isso com 400 antes de a requisição chegar ao controller — o @Body() do NestJS nem entra em cena, o parser de body do Fastify já barra antes.

Por que passou despercebido por tanto tempo: todo teste de API deste projeto, em toda fase anterior, foi feito via curl — e curl -X POST sem -H 'Content-Type: ...' e sem -d não define esse header. O fetch() do navegador (usado pelo cliente HTTP real do frontend) sempre define o header por padrão. Ou seja: testar só via curl não teria pegado nunca esse bug — só apareceu quando um usuário de verdade clicou em botões reais no navegador. Lição: curl prova que o backend funciona: não prova que o frontend consegue falar com ele.

Fix: só incluir o header Content-Type quando body !== undefined, na única função request() usada por get/post/patch/delete.

Bug relacionado, achado na mesma revisão: DELETE /api/suppression/:id exige removalReason no corpo, mas api.delete() nem aceitava um argumento de corpo — o botão "Desbloquear" da lista de bloqueio sempre falhava com 403. Mesma causa raiz (função de ação sem forma de levar payload), mesmo tipo de bug que só um clique real revela.

"Argument calledNumber is missing" ao originar chamada

Sintoma: PrismaClientValidationError ao reservar um lead, mesmo a query SQL parecendo correta.

Causa: $queryRaw do Prisma não aplica o mapeamento camelCase automático que findMany/update aplicam — colunas retornadas por SQL cru mantêm o nome exato da coluna (phone_normalized, não phoneNormalized).

Fix: sempre usar alias explícito no SQL: phone_normalized AS "phoneNormalized". Ver apps/dialer-worker/src/lead-repository.ts.

Pacing do discador oscilando/nunca parando de subir mesmo sem atividade

Sintoma: no cenário de teste da seção 66 (20 agentes/10 CPS/30% taxa de atendimento), o pacingFactor continuava subindo mesmo em períodos ociosos, gerando concorrência muito acima do esperado.

Causas (três, compostas):

  1. O cálculo de concorrência total não incluía chamadas já conectadas ao agente (agentConnectedCalls), subestimando quanto já estava "em uso".
  2. O ajuste de pacing subia mesmo sem nenhuma atividade de discagem no ciclo (nada "deu errado", então ele interpretava como "pode subir mais").
  3. Uma única iteração fechava 100% da diferença entre o alvo e o atual, causando overshoot.

Fix: agentConnectedCalls entra no total de concorrência; pacing nunca sobe durante ociosidade (hasActivity gate); fechamento de gap limitado a 50% por ciclo (RAMP_FRACTION); e dampening por raiz quadrada do próprio pacingFactor. Ver apps/dialer-worker/src/predictive-engine.ts e docs/PREDICTIVE_DIALER.md.

AMI: comando Command não retorna o output esperado

Sintoma: parsing manual do protocolo AMI para a action Command (usada por diagnóstico) não batia com a documentação legada (Response: Follows / --END COMMAND--).

Causa: o Asterisk 22 usa um formato diferente para Command: múltiplos headers Output: repetidos, um por linha de saída — não o bloco Follows/END COMMAND de versões antigas.

Fix: reverse-engineering via socket TCP cru (script de debug dedicado) para confirmar o formato real antes de implementar o parser em packages/telephony.

Erro de sintaxe no schema.prisma: @default(1_000_000)

Sintoma: prisma generate falha com erro de parsing.

Causa: separador de milhar com underscore (1_000_000) não é suportado na sintaxe do .prisma — é um recurso de JS/TS, não do DSL do Prisma.

Fix: escrever o número sem separadores: 1000000.

Prisma.InputJsonValue rejeita instância de DTO

Sintoma: Index signature for type 'string' is missing ao passar um objeto DTO diretamente para um campo Json do Prisma (ex.: AuditLog.after).

Causa: uma instância de classe (mesmo com os campos certos) não satisfaz a assinatura de índice que InputJsonValue exige — só um objeto literal simples satisfaz.

Fix: espalhar em um objeto literal: after: { ...dto } em vez de after: dto.

Redis: agentes travados em IN_CALL para sempre

Sintoma: depois de uma chamada, o agente nunca voltava a AVAILABLE.

Causa: o "claim" de agente disponível só marcava IN_CALL na conexão — não havia nenhum mecanismo simétrico de liberação após o fim da chamada.

Fix: agent-call-binding.ts#releaseAgentAfterCall, chamado explicitamente quando a chamada termina, com suporte a WRAP_UP intermediário antes de AVAILABLE.

Corrida entre duas campanhas reivindicando o mesmo agente

Sintoma (encontrado em revisão de código, não em produção): implementação inicial de claimAvailableAgent usava findFirst + update separado — janela de corrida entre duas campanhas concorrentes.

Fix: reescrito com UPDATE ... WHERE id = (SELECT ... FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING ..., mesmo padrão atômico já usado para reserva de leads.

Diagnóstico geral

Para qualquer problema não listado acima:

  1. bash scripts/healthcheck.sh — descarta problema de infraestrutura básica primeiro.
  2. docker compose logs -f <serviço> — logs estruturados JSON, procurar por "level":50 (error) ou "level":40 (warn).
  3. docker exec b2bcall-asterisk asterisk -rx "..." — ver docs/ASTERISK.md para comandos úteis.
  4. Consultar AuditLog (GET /api/audit) se o problema envolve uma ação específica de um usuário.
  5. Verificar docker compose ps — um container "unhealthy" quase sempre aponta a causa raiz de problemas em cascata nos serviços que dependem dele.