Files
b2bcall/TODO.md
B2BCall Bootstrap 66cc2058fd feat: implement predictive dialing engine
- apps/dialer-worker: motor do discador preditivo completo
  - predictive-engine.ts: EWMA de answerProbability/avgTalkTimeSeconds/
    abandonRate, previsao de liberacao de agentes, calculo de quantas
    chamadas originar. Logica pura, 14 testes unitarios
  - cps-limiter.ts: token bucket via script Lua atomico no Redis (dois
    buckets independentes campanha+tronco, min() dos dois, seguro com
    multiplos workers)
  - lead-repository.ts: reserva atomica via FOR UPDATE SKIP LOCKED,
    recuperacao de reservas orfas apos queda de worker
  - campaign-lock.ts: lock distribuido por campanha (Redis SET NX PX +
    token de posse, renovacao/liberacao seguras via Lua)
  - schedule.ts: janela de horario da campanha (timezone real via
    Intl.DateTimeFormat, dias da semana), 6 testes unitarios
  - retry-rules.ts: motor de retentativa por causa de encerramento,
    configuravel por campanha, nunca infinito
  - simulation.ts + campaign-worker.ts (modo DIALER_SIMULATION): permite
    testar o motor inteiro sem tronco de operadora real
  - simulation-harness.ts: reproduz em tempo discreto e deterministico o
    cenario exato de aceite da secao 66 (20 agentes/10 CPS/30% atendimento/
    TMA 180s) — 6 testes validando CPS nunca excedido, concorrencia nunca
    excedida, pacing nao diverge
  - campaign-worker.ts: orquestra tudo contra Postgres/Redis/Asterisk reais

- docs/PREDICTIVE_DIALER.md: algoritmo documentado, incluindo dois bugs
  reais encontrados e corrigidos durante o teste do cenario de aceite
  (concorrencia nao contava chamadas em atendimento; pacing subia sem
  limite durante periodos ociosos, causando rajada maxima assim que um
  agente ficava livre) e limitacoes conhecidas (AMD e wrap-up automatico
  via eventos reais ainda pendentes, documentados sem esconder)

Testado ponta a ponta contra containers reais (Postgres/Redis/Asterisk):
campanha completa criada -> agente disponivel via API -> leads importados
-> campanha iniciada -> reserva atomica -> CPS respeitado -> simulacao de
NO_ANSWER (retry agendado) e ANSWERED (AGENT_CONNECTED, EWMA atualizada ao
vivo) -> parada sem derrubar chamadas em andamento.
2026-08-27 15:24:31 -03:00

270 lines
17 KiB
Markdown

# B2BCall — TODO Operacional
Checklist vivo. Marcar `[x]` somente após testar. Não deixar item concluído sem
validação. Ver critérios de aceite completos em `docs/ARCHITECTURE.md` e no prompt
mestre original (`agente.md`, seções 90-93).
## Fase 0 — Infraestrutura base
- [x] Levantamento do servidor (SO, recursos, rede)
- [x] docs/ARCHITECTURE.md
- [x] TODO.md
- [x] Git init + .gitignore + primeiro commit
- [x] Instalar Docker Engine + Compose plugin (29.7.2 / Compose v5.5.0)
- [x] Estrutura de diretórios do monorepo
- [x] .env.example (+ scripts/generate-secrets.sh, .env real gerado localmente)
- [ ] nftables (firewall base, default-deny administrativo, sem bloquear SSH) —
adiado para a Fase 9, quando houver serviços/portas reais a proteger
## Fase 1 — Dados e cache
- [x] docker-compose: serviço postgres 17-alpine (schemas application/asterisk) — testado, healthy
- [x] docker-compose: serviço redis 7-alpine (auth, AOF, maxmemory 100mb) — testado, healthy
- [x] Health checks postgres/redis
- [ ] packages/database: schema inicial + migrations tool
## Fase 2 — Asterisk
- [x] Dockerfile Asterisk 22.10.1 LTS compilado do fonte (host network) —
Debian trixie não tem mais o pacote `asterisk` nos repos oficiais
- [x] PJSIP (transport UDP:5060; chan_sip nem existe mais a partir do Ast 21)
- [x] AMI habilitado — testado login via 127.0.0.1:5038, bloqueado na LAN via nftables
- [x] ARI habilitado — testado GET /ari/asterisk/info via 127.0.0.1:8088, 401 sem auth
- [x] Realtime ODBC -> Postgres schema `asterisk` (sorcery.conf + extconfig.conf
+ res_odbc.conf) — `pjsip show endpoints` consulta o Postgres sem erro
- [x] CDR via cdr_adaptive_odbc — testado, grava em asterisk.cdr (ANSWERED,
duration/billsec corretos). Nota: coluna `end` renomeada para
`end_time` (palavra reservada no Postgres, cdr_adaptive_odbc não
quota identificadores)
- [x] CEL via cel_odbc — testado, grava em asterisk.cel (CHAN_START/ANSWER/
HANGUP/CHAN_END)
- [x] queue_log habilitado (padrão do app_queue, arquivo em /var/log/asterisk)
- [x] Dialplan inicial de teste (contexto `b2bcall-healthcheck`, exten 600,
Answer/Playback/Echo) — testado via `channel originate ... extension`
- [x] nftables protegendo AMI/ARI/SIP na interface da LAN (ens18), trazido da
Fase 9 para já — não testado a partir de outra máquina na LAN (só
localmente, onde tráfego para o próprio IP vai por `lo` e não
exercita a regra; revisar quando OpenSIPS/outro host estiver disponível)
### Pendências conhecidas da Fase 2 (não bloqueiam, revisitar depois)
- [ ] `res_pjsip_outbound_registration` recusa carregar no boot (mensagem
"no existing documentation" para o tipo 'registration' do sorcery) —
carregamento manual via `module load` funciona mas é desfeito por uma
flag interna de "declined at boot". Só afeta troncos com registration
outbound; endpoints/aors funcionam normalmente. Investigar na Fase 4
ao implementar cadastro de troncos com registration.
- [ ] `res_config_ldap` gera ERROR de log no boot (sem LDAP configurado,
não usamos). Cosmético — considerar `noload` em modules.conf.
- [ ] `cdr_pgsql`/`cel_pgsql` não compilaram (falta libpq-dev na imagem);
usamos `cdr_adaptive_odbc`/`cel_odbc` como alternativa funcional via
ODBC — decisão adequada, não precisa dos módulos pgsql nativos.
## Fase 3 — Backend base
- [x] apps/api (NestJS 11 + Fastify) bootstrap — containerizado, healthy
- [x] packages/database (Prisma) + packages/shared (catálogo de permissões)
— decisão de ORM documentada em docs/ARCHITECTURE.md 3.6.1
- [x] Migration inicial aplicada (users/sessions/roles/permissions/
user_roles/role_permissions/audit_logs/password_reset_tokens)
- [x] Seed (40 permissões, 4 perfis, bootstrap super_admin com senha
aleatória em FIRST_LOGIN.txt chmod 600) — testado
- [x] Autenticação (Argon2id, access JWT + refresh opaco com rotação,
cookies HttpOnly/SameSite=Lax, refresh_token restrito a /api/auth) —
testado: login, /me, refresh, change-password, revogação de sessão
- [x] Rate limiting de login progressivo via Redis (5/60s, bloqueio
1min→2min→4min...até 1h por IP) — testado com 7 tentativas seguidas
- [x] RBAC completo (users/roles/permissions/user_roles/role_permissions)
reforçado no backend via PermissionsGuard — testado (nega sem
permissão, permite com todas as permissões) + unit tests
- [x] Proteção contra auto-elevação de privilégio (usuário não altera os
próprios roleIds) — testado, HTTP 403
- [x] Auditoria (audit_logs) chamada explicitamente em cada ação sensível
(login/login_failed/logout/password_changed/user_created/
user_updated/role_*) com redação de campos sensíveis — testado via
GET /api/audit (roles.manage/audit.view), paginação server-side
- [x] Health checks /api/health, /api/health/live, /api/health/ready
(Postgres + Redis reais, sem dado fake) — testado
- [x] Swagger/OpenAPI em /api/docs, desabilitável via SWAGGER_ENABLED=false
— testado (200 com flag default true)
- [x] Logs estruturados JSON (nestjs-pino) com request_id de correlação e
redação de senha/tokens/cookies
- [x] Filtro global de exceções: nunca expõe erro cru, sempre requestId
- [x] Lint (eslint --fix) e testes unitários (PermissionsGuard) passando
- [ ] Recuperação de senha por e-mail: lógica completa (token com
expiração, hash, uso único), mas envio real via SMTP é um stub que
só loga — falta credencial SMTP real (.env SMTP_*), meramente
externo — trocar MailerService por nodemailer quando houver
## Fase 4 — Telefonia (camada de aplicação)
- [x] packages/telephony: cliente AMI próprio (TCP raw, sem dependência de
terceiros) + TelephonyProvider + AsteriskTelephonyProvider — testado
contra o Asterisk real: login, PJSIPShowEndpoints/QueueStatus (lista
vazia tratada corretamente), Command (parsing do formato real do
Asterisk 22: múltiplos headers "Output:", não mais "Follows"/"--END
COMMAND--"), Originate, stream de eventos
- [x] apps/asterisk-events (worker dedicado, containerizado) — conecta via
host.docker.internal (rede bridge -> Asterisk em host network),
normaliza eventos relevantes, persiste ExtensionState (Postgres),
publica em Redis pub/sub (b2bcall:events:asterisk e :extensions),
heartbeat para health check — testado ponta a ponta com chamada real
- [x] Schema Prisma: Trunk, Extension, ExtensionState + criptografia de
segredos AES-256-GCM (packages/shared/secret-crypto.ts)
- [x] CRUD Troncos (IP/AUTH/REGISTRATION, CPS máximo, ACL, secret cifrado
AES-256-GCM, teste de status) — testado: criado via API, verificado
que o Asterisk enxerga o endpoint via realtime SEM reload, status
consultado via PJSIPShowContacts, exclusão em cascata confirmada
- [x] CRUD Ramais (senha SIP gerada automaticamente, nunca reexibida exceto
na criação/reset, reset endpoint) — testado igual aos troncos
- [x] GET /api/monitoring/extensions — status com prioridade de cor
(offline/available/busy; pausa e agente logado chegam na Fase 5)
- [ ] Painel visual de Ramais (WebSocket) — backend (gateway WS) ainda
pendente; ExtensionState/pub-sub e o endpoint de monitoramento já
prontos como base (frontend consumirá via WS na Fase 8)
- [x] Dialplan estruturado e versionado (dialplan_entries + dialplan_versions)
— testado ponta a ponta: criar entradas -> publicar -> gera
b2bcall-dialplan.conf (volume compartilhado api<->asterisk) -> AMI
"dialplan reload" -> "dialplan show" confirma ativo, SEM restart.
Validação estrita de caracteres em cada campo (nunca aceita entrada
capaz de injetar linhas de config). Rollback automático se a
verificação pós-reload detectar erro; rollback manual para qualquer
versão anterior via endpoint dedicado. Limitação conhecida: detecta
falhas estruturais/sintáticas, não erros semânticos como nome de
aplicação Asterisk inexistente (só se manifesta em tempo de chamada)
- [x] Administração do Asterisk: status (AMI + heartbeat asterisk-events),
module show, e diagnóstico com ALLOWLIST ESTRITA de comandos exatos
(core show uptime/channels/version, pjsip show *, queue show, module
show, dialplan show) — testado: comando permitido executa, comando
arbitrário rejeitado com 400. Abas de configuração fina (pjsip/rtp/
cdr/cel/logs como formulário editável) ficam para quando o fluxo de
"editor avançado com backup/validação" for generalizado além do
dialplan — no momento config geral do Asterisk é só leitura via
status/diagnóstico, não tem tela de edição de asterisk.conf/rtp.conf
## Fase 5 — Call Center
- [x] Agentes (agents, separado de users 1:1) + agent_sessions +
agent_state_events + agent_pause_events — schema Prisma completo
- [x] Máquina de estados do agente (LOGGED_IN→AVAILABLE→PAUSED→AVAILABLE,
logout) com histórico início/fim por estado — testado ponta a ponta
contra o Asterisk real: login cria sessão, "disponível" faz AMI
QueueAdd (confirmado via `queue show`: membro dinâmico aparece),
"pausar" faz QueuePause com motivo (confirmado: "paused:Almoco"
aparece ao vivo), "despausar" reverte, "logout" faz QueueRemove
(confirmado: fila volta a "No Members")
- [x] Motivos de pausa (CRUD) — sem permissão dedicada no catálogo da
seção 11, tratado sob settings.manage
- [x] Filas (CRUD + 8 estratégias do enum QueueStrategy) — gera
queues.conf via o mesmo padrão do dialplan (arquivo compartilhado +
reload), mas SEM membros estáticos: membership é 100% dinâmica via
AMI (login/logout do agente), evitando dessincronia entre duas
fontes de verdade
- [x] Tela do agente (backend completo: login/available/pause/unpause/
logout via /api/agent-console/*) — disposição de chamada fica para
quando existir uma chamada de verdade para dispor (Fase 6)
- [ ] Disposições de chamada (CRUD + ações: callback, DNC) — adiado para
Fase 6 junto com Campanhas/Leads, já que disposição só faz sentido
quando há chamadas de campanha reais para classificar
- [ ] Callback (agendamento + scheduler) — idem, depende de leads/campanhas
- [x] Monitoramento de Filas (tempo real): GET /api/monitoring/queues
(chamadas aguardando, maior espera, agentes logados/pausados/
disponíveis) via AMI QueueStatus ao vivo. TME/TMA/SLA/taxa de
abandono HISTÓRICOS ficam para a Fase 7 (dependem de consolidação
de CDR/CEL/queue_log) — não inventamos número aqui
## Fase 6 — Campanhas e discador preditivo
- [x] CRUD Campanhas (todos os campos da seção 24) — testado: criação com
defaults corretos, máquina de estados (DRAFT/READY/RUNNING/PAUSED/
DRAINING/STOPPED/COMPLETED) rejeitando transições inválidas (ex.:
PAUSED->PAUSED = 400), edição bloqueada com campanha RUNNING
- [x] Import CSV streaming (detecção automática de delimitador — vírgula/
ponto-e-vírgula/tab —, mapeamento de coluna por nome de header,
validação+normalização, dedupe dentro do arquivo E contra leads já
existentes na campanha, CSV de rejeitados com motivo, modo dryRun
para preview sem persistir) — testado com CSV real (5 linhas: 3
válidas, 1 inválida, 1 duplicada — todos os contadores bateram)
- [x] Normalização de telefone (packages/shared, BR — E.164, DDD/celular
validados) — 7 testes unitários passando
- [x] Lista de supressão (DNC): CRUD + import CSV + remoção sempre exige
motivo e é auditada — checagem obrigatória pré-originação
(isSuppressed) implementada e usada pelo dialer-worker (ver abaixo)
- [x] CPS limiter (token bucket via Lua atômico no Redis, dois buckets
independentes campanha+tronco, min() dos dois — multi-worker seguro)
- [x] Reserva concorrente de leads (SQL único com FOR UPDATE SKIP LOCKED —
atômico; timeout de 90s libera reservas órfãs após queda de worker)
- [x] Idempotência de originação (DialAttempt.id é o attempt_id de negócio,
nunca reoriginado; UNIQUEID nunca usado como PK — seção 97)
- [x] PredictiveDialerEngine (EWMA de answerProbability/avgTalkTimeSeconds/
abandonRate, previsão de liberação de agentes via TMA histórico +
tempo decorrido, ajuste de pacing) — apps/dialer-worker/src/
predictive-engine.ts, 100% lógica pura testável, 14 testes unitários
- [x] Controle de abandono (pacing cai imediatamente acima da meta; nunca
sobe "porque nada de ruim aconteceu" durante período ocioso — bug
real encontrado e corrigido durante o teste do cenário da seção 66)
- [ ] AMD opcional por campanha — campo existe no schema/DTO, detecção real
(app AMD do Asterisk) ainda não integrada à originação (ver
docs/PREDICTIVE_DIALER.md seção 7)
- [ ] Wrap-up time — campo existe, mas transição automática do agente para
WRAP_UP via eventos reais (AgentComplete) não implementada; hoje só
funciona no modo simulação e nas transições manuais da Fase 5 (ver
docs/PREDICTIVE_DIALER.md seção 7)
- [x] Retry engine (regras por causa configuráveis por campanha via JSON,
default seção 79, nunca rediscagem infinita via maxAttempts) —
testado ponta a ponta (NO_ANSWER agendou retry corretamente)
- [x] Horário de campanha (timezone via Intl.DateTimeFormat, dias da
semana, HH:MM, WAITING_SCHEDULE computado) — 6 testes unitários
cobrindo timezone, dias, startDate/endDate
- [x] Lock distribuído por campanha (Redis SET NX PX + token de posse +
renovação/liberação seguras via Lua — nunca libera lock de outro dono)
- [x] docs/PREDICTIVE_DIALER.md — algoritmo, decisões, bugs encontrados e
corrigidos durante o teste, limitações conhecidas documentadas
**Testado ponta a ponta contra containers reais** (Postgres/Redis/Asterisk):
campanha completa criada → agente logado/disponível via API real → leads
importados → campanha iniciada → reserva atômica → CPS respeitado →
simulação de NO_ANSWER (retry agendado) e ANSWERED (AGENT_CONNECTED, EWMA
atualizada ao vivo no Redis) → parada sem derrubar chamadas em andamento.
## Fase 7 — CDR, métricas e relatórios
- [ ] Modelo consolidado de chamadas (CDR+CEL+AMI+queue_log)
- [ ] Reconciliação de estados órfãos após restart
- [ ] TME / TMA (definições documentadas, cálculo correto)
- [ ] Relatório de Chamadas (filtros, paginação server-side, export CSV)
- [ ] Relatório de Agentes (tempos, pausas detalhadas)
- [ ] Dashboard geral (cards + gráficos reais)
- [ ] Dashboard do discador (por campanha, tempo real)
- [ ] Compliance de Chamadas (parâmetros configuráveis, alertas, contadores)
## Fase 8 — Frontend completo
- [ ] Bootstrap Next.js + Tailwind + shadcn/ui + TanStack Query + WS client
- [ ] Logo processada (b2bcall.png) + tema light/dark
- [ ] Menu completo (seção 52)
- [ ] Todas as telas do checklist de aceite (seção 90)
## Fase 9 — Segurança e produção
- [ ] Criptografia de segredos de trunk (AES-256-GCM)
- [ ] HTTP security headers, CORS, CSRF, Helmet
- [ ] nftables final revisado
- [ ] Logs estruturados JSON (sem segredos)
- [ ] Correlation IDs (request_id/attempt_id/call_id)
- [ ] Métricas Prometheus (/metrics)
- [ ] Bootstrap super_admin (senha aleatória, FIRST_LOGIN.txt, forçar troca)
- [ ] Seed (permissões, perfis, pausas, disposições) sem dados fake em produção
- [ ] Backup/restore (postgres, asterisk config, .env seguro)
- [ ] scripts/install.sh, update.sh, backup.sh, restore.sh, healthcheck.sh
- [ ] Nginx reverse proxy (80/443, WS, HTTPS documentado)
## Fase 10 — Testes e aceite
- [ ] Unit tests (predictive engine, CPS limiter, permissions, phone norm, retry,
state machines, TME/TMA, scheduling)
- [ ] Integration tests (postgres, redis, repositories, API, AMI mock)
- [ ] E2E (login → ... → RBAC, conforme seção 64)
- [ ] Modo simulação (DIALER_SIMULATION=true) + testes do predictive engine
- [ ] Prova de CPS respeitado / concorrência máxima / sem discagem dupla /
pausa efetiva / recuperação após restart / pacing reage a abandono /
supressão respeitada / horário respeitado
- [ ] Quality gate (lint, typecheck, unit, integration, e2e, compose config,
compose ps, health checks) — tudo verde
- [ ] Aceite de segurança (seção 92)
- [ ] Aceite Asterisk (seção 93, comandos documentados)
- [ ] README final completo
- [ ] Relatório final da implementação
---
**Nota de ambiente:** VM atual com 1.9 GiB RAM / 2 vCPU — adequada para dev e
simulação, não para carga real de produção. Ver docs/ARCHITECTURE.md seção 1.