feat: Fase 9/10 — métricas, scripts operacionais, callback/wrap-up e aceite final

Fase 9 (segurança e produção):
- GET /api/metrics: endpoint Prometheus com métricas reais (chamadas,
  agentes, filas, CPS por campanha), protegido por permissão
- scripts/backup.sh, restore.sh, healthcheck.sh, install.sh, update.sh —
  testados contra o ambiente real (backup.sh e healthcheck.sh rodados de
  verdade; install.sh/update.sh validados por inspeção, ambiente atual já
  provisionado)
- POST /api/agent-console/dispose: aplica disposição de chamada de verdade
  (lacuna deixada aberta desde a Fase 6), com ações CALLBACK (agenda
  retorno) e DO_NOT_CALL (suprime automaticamente)
- apps/dialer-worker/src/callback-sweep.ts: reativa leads com callback
  vencido; GET /api/callbacks para consulta
- apps/dialer-worker/src/wrap-up-sweep.ts: transição automática
  WRAP_UP -> AVAILABLE + despausa real na fila do Asterisk. Exigiu corrigir
  main.ts para conectar ao AMI mesmo em DIALER_SIMULATION=true
  (DIALER_SIMULATION deve impedir só originação de chamada, não ações
  administrativas de fila)
- nftables revisado (sem alterações necessárias)
- Documentação completa: INSTALL, OPERATIONS, BACKUP_RESTORE, SECURITY,
  DATABASE, API, ASTERISK, OPENSIPS (não implementado, motivo
  documentado), TROUBLESHOOTING
- README.md e CHANGELOG.md reescritos

Fase 10 (testes e aceite):
- Quality gate completo executado: build/typecheck (7 workspaces), lint,
  46 testes unitários, docker compose config/ps, healthcheck — tudo verde
- Aceite de segurança (seção 92): 13 itens verificados ao vivo contra o
  sistema real, não só por inspeção de código
- Aceite Asterisk (seção 93): os 5 comandos executados e documentados,
  comunicação API->AMI->Asterisk validada
- docs/RELATORIO_FINAL.md: relatório final no formato da seção 96

Todos os fixtures de teste desta fase foram removidos/desativados ao
final. Credenciais de acesso entregues separadamente em CREDENCIAIS.txt
(fora do git, nunca versionado).
This commit is contained in:
2026-08-27 19:16:14 -03:00
parent 6273b32214
commit 80e72881b2
33 changed files with 2024 additions and 39 deletions

108
docs/TROUBLESHOOTING.md Normal file
View File

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