Files
b2bcall/docs/TROUBLESHOOTING.md
B2BCall Bootstrap a10a40a489 docs: registrar bugs encontrados na verificação real em navegador
Usuário testou em produção e confirmou que os dois bugs do commit
anterior (Content-Type em requisições sem corpo, DELETE de supressão sem
motivo) estão corrigidos. Atualiza TODO.md/RELATORIO_FINAL.md para
refletir a verificação visual como concluída, e documenta a lição em
TROUBLESHOOTING.md: teste via curl prova que o backend funciona, não que
o frontend consegue falar com ele.
2026-08-27 20:52:48 -03:00

142 lines
6.4 KiB
Markdown

# B2BCall — Troubleshooting
Problemas reais encontrados durante o desenvolvimento deste projeto e como
foram diagnosticados/resolvidos — mantido como referência para o futuro.
## "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.