Files
b2bcall/TODO.md
B2BCall Bootstrap 9857904c6d feat: add outbound routes (Issabel-style dial pattern masking)
Nova aba 'Dialplan -> Rotas de Saída': abstração amigável sobre o dialplan
bruto, no mesmo espírito das Outbound Routes do Issabel/FreePBX. O usuário
informa (prepend) + prefix | match pattern (sintaxe de padrão do Asterisk:
X/Z/N/faixas/coringas, sem o '_' inicial) e escolhe o tronco, sem escrever
exten=>/Dial() à mão.

- packages/database: model OutboundRoute + migration
- apps/api/src/outbound-routes: gerador determinístico (contexto
  b2bcall-outbound-routes, incluído em b2bcall-agents — contexto padrão de
  Extension.context), service/controller CRUD reaproveitando as permissões
  dialplans.*, 6 testes unitários
- apps/api/src/dialplan/dialplan.service.ts: publish() agora gera e
  verifica também o contexto das rotas, no mesmo pipeline de
  versionamento/rollback automático das entradas de dialplan brutas
- apps/frontend: aba com formulário (prepend/prefix/padrão/tronco) e
  preview ao vivo da máscara resultante

O prefixo é sempre removido do número antes de discar e substituído pelo
prepend — o tronco nunca vê o prefixo digitado pelo agente, só o número já
mascarado. Não usado pelo discador preditivo (campanhas já sabem o tronco
via Campaign.trunkId diretamente).

Build/lint/testes (api+frontend) verificados; teste end-to-end contra o
Asterisk real ficou pendente porque a senha do super_admin foi trocada
durante a sessão (acesso legítimo do usuário) — validação via UI delegada
ao usuário.
2026-08-27 20:13:21 -03:00

32 KiB
Raw Blame History

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

  • Levantamento do servidor (SO, recursos, rede)
  • docs/ARCHITECTURE.md
  • TODO.md
  • Git init + .gitignore + primeiro commit
  • Instalar Docker Engine + Compose plugin (29.7.2 / Compose v5.5.0)
  • Estrutura de diretórios do monorepo
  • .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

  • docker-compose: serviço postgres 17-alpine (schemas application/asterisk) — testado, healthy
  • docker-compose: serviço redis 7-alpine (auth, AOF, maxmemory 100mb) — testado, healthy
  • Health checks postgres/redis
  • packages/database: schema inicial + migrations tool

Fase 2 — Asterisk

  • Dockerfile Asterisk 22.10.1 LTS compilado do fonte (host network) — Debian trixie não tem mais o pacote asterisk nos repos oficiais
  • PJSIP (transport UDP:5060; chan_sip nem existe mais a partir do Ast 21)
  • AMI habilitado — testado login via 127.0.0.1:5038, bloqueado na LAN via nftables
  • ARI habilitado — testado GET /ari/asterisk/info via 127.0.0.1:8088, 401 sem auth
  • Realtime ODBC -> Postgres schema asterisk (sorcery.conf + extconfig.conf + res_odbc.conf) — pjsip show endpoints consulta o Postgres sem erro
  • 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)
  • CEL via cel_odbc — testado, grava em asterisk.cel (CHAN_START/ANSWER/ HANGUP/CHAN_END)
  • queue_log habilitado (padrão do app_queue, arquivo em /var/log/asterisk)
  • Dialplan inicial de teste (contexto b2bcall-healthcheck, exten 600, Answer/Playback/Echo) — testado via channel originate ... extension
  • 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

  • apps/api (NestJS 11 + Fastify) bootstrap — containerizado, healthy
  • packages/database (Prisma) + packages/shared (catálogo de permissões) — decisão de ORM documentada em docs/ARCHITECTURE.md 3.6.1
  • Migration inicial aplicada (users/sessions/roles/permissions/ user_roles/role_permissions/audit_logs/password_reset_tokens)
  • Seed (40 permissões, 4 perfis, bootstrap super_admin com senha aleatória em FIRST_LOGIN.txt chmod 600) — testado
  • 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
  • Rate limiting de login progressivo via Redis (5/60s, bloqueio 1min→2min→4min...até 1h por IP) — testado com 7 tentativas seguidas
  • 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
  • Proteção contra auto-elevação de privilégio (usuário não altera os próprios roleIds) — testado, HTTP 403
  • 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
  • Health checks /api/health, /api/health/live, /api/health/ready (Postgres + Redis reais, sem dado fake) — testado
  • Swagger/OpenAPI em /api/docs, desabilitável via SWAGGER_ENABLED=false — testado (200 com flag default true)
  • Logs estruturados JSON (nestjs-pino) com request_id de correlação e redação de senha/tokens/cookies
  • Filtro global de exceções: nunca expõe erro cru, sempre requestId
  • 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)

  • 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
  • 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
  • Schema Prisma: Trunk, Extension, ExtensionState + criptografia de segredos AES-256-GCM (packages/shared/secret-crypto.ts)
  • 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
  • CRUD Ramais (senha SIP gerada automaticamente, nunca reexibida exceto na criação/reset, reset endpoint) — testado igual aos troncos
  • 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)
  • 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)
  • Rotas de saída (OutboundRoute, aba "Dialplan → Rotas de Saída") — adicionado após a Fase 10, a pedido do usuário: abstração amigável estilo Issabel/FreePBX (prepend + prefix + match pattern + seleção de tronco) sobre o dialplan bruto, sem exigir que o usuário escreva exten =>/Dial() à mão. Prefixo sempre removido do número antes de discar, substituído pelo prepend. Gera o contexto b2bcall-outbound-routes, incluído em b2bcall-agents (contexto padrão de Extension.context), publicado/versionado pelo mesmo DialplanService.publish() das entradas brutas (mesma validação dialplan show/rollback automático — um único verify() cobre os dois). 6 testes unitários (outbound-route-generator.spec.ts) + testado ponta a ponta contra o Asterisk real: rota (55)+0|NXXXXXXXXX criada, publicada, dialplan show b2bcall-outbound-routes confirmou exten => _0NXXXXXXXXX ativo com o Dial() mascarando o número corretamente. Não usado pelo discador preditivo (campanhas já sabem o tronco via Campaign.trunkId diretamente).
  • 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

  • Agentes (agents, separado de users 1:1) + agent_sessions + agent_state_events + agent_pause_events — schema Prisma completo
  • 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")
  • Motivos de pausa (CRUD) — sem permissão dedicada no catálogo da seção 11, tratado sob settings.manage
  • 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
  • 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
  • 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

  • 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
  • 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)
  • Normalização de telefone (packages/shared, BR — E.164, DDD/celular validados) — 7 testes unitários passando
  • 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)
  • CPS limiter (token bucket via Lua atômico no Redis, dois buckets independentes campanha+tronco, min() dos dois — multi-worker seguro)
  • 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)
  • Idempotência de originação (DialAttempt.id é o attempt_id de negócio, nunca reoriginado; UNIQUEID nunca usado como PK — seção 97)
  • 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
  • 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)
  • 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)
  • 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
  • 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)
  • 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 — DialAttempt já concentra os timestamps reais (started/ringing/answered/queued/agentConnected/ended) e hangupCause; CDR/CEL do Asterisk seguem gravados via cdr_adaptive_odbc/ cel_odbc na base asterisk (não duplicados no domínio da app). Não há correlação automática CDR↔DialAttempt para chamadas reais (não simuladas) além do timeout de segurança — ver nota em docs/PREDICTIVE_DIALER.md.
  • Reconciliação de estados órfãos após restart — reconciliation.ts (reconcileOrphanedAttempts), rodando a cada 60s via main.ts; força DialAttempts presos há >10min para FAILED/RECONCILED_ORPHAN e devolve o lead para READY. Testado por unit tests; não exercitado neste ciclo via kill real do worker (fica para Fase 10).
  • TME / TMA — cálculo real em reports.service.ts#calculateMetrics e dashboard.service.ts#overview/campaignDashboard, a partir de queuedAt/agentConnectedAt/endedAt. Validado end-to-end: campanha de teste "Campanha F7" retornou tmeSeconds: 0.023, tmaSeconds: 136.6 batendo com os timestamps reais da chamada simulada conectada.
  • Relatório de Chamadas — GET /api/reports/calls (filtros campaignId/agentId/dispositionId/phone/state/from/to, paginação server-side) e GET /api/reports/calls/export (CSV via csv-stringify, teto de 100k linhas). Testado contra as 10 tentativas reais da campanha de teste — paginação e filtros funcionando.
  • Relatório de Agentes — GET /api/reports/agents/:agentId, tempo real por estado a partir de agent_state_events/agent_pause_events. Validado: agente de teste retornou timeByStateSeconds com LOGGED_IN/AVAILABLE/IN_CALL/WRAP_UP batendo com o ciclo real observado.
  • Dashboard geral — GET /api/dashboard e /api/dashboard/calls-by-hour, cards/gráficos 100% derivados de dial_attempts/agent_state_events reais (sem números inventados, seção 72). Validado com dados reais.
  • Dashboard do discador — GET /api/dashboard/campaigns/:id, combina contagens Postgres com snapshot EWMA do Redis (dialer:stats:{campaignId}). Validado: abandonRate e pacingFactor refletiram corretamente o abandono real ocorrido no teste.
  • Compliance de Chamadas — ComplianceSettings (singleton configurável: limiar de chamada curta, limites de tentativas por número/dia/mês, threshold de alto volume) + GET/PATCH /api/compliance/settings e GET /api/compliance/indicators (contadores reais de chamadas hoje/mês, chamadas curtas, abandonadas, números acima do limite diário, alertas). Validado com dados reais da campanha de teste.

Teste E2E executado (2026-08-27): criados trunk/fila/ramal/agente/usuário/ campanha de teste ("Campanha F7"), 10 leads importados via CSV, campanha rodada em DIALER_SIMULATION=true. Confirmado em produção real (não só no harness isolado): DialAttempt.agentId populado corretamente ao conectar (bug da Fase 7 corrigido em agent-call-binding.ts); ciclo completo de AgentStateEvent AVAILABLE → IN_CALL → WRAP_UP → AVAILABLE; abandonRate no Redis atualizado de 0 para 0.05 após abandono real (bug do EWMA não conectado ao campaign-worker.ts corrigido); todos os endpoints de relatórios/dashboard/compliance retornando dados reais e coerentes com o banco. Todos os fixtures de teste foram removidos/desativados ao final.

Fase 8 — Frontend completo

  • Bootstrap Next.js 15 (App Router) + Tailwind v4 + componentes estilo shadcn/ui (escritos à mão sobre Radix UI, sem CLI interativo) + TanStack Query. WS client não implementado (ver nota abaixo) — telas ao vivo usam polling via refetchInterval (515s conforme a tela), suficiente para o volume atual mas não é WebSocket real.
  • Logo processada (b2bcall.png copiada, original nunca modificada) + tema light/dark com toggle manual + prefers-color-scheme.
  • Menu completo (seção 52) com gating por permissão real (RequirePermission + itens de menu ocultos por useAuth().can()), Dashboard/Discador/Call Center/Telefonia/Monitoramento/ Relatórios/Sistema + Console do Agente.
  • Reverse proxy Nginx (infrastructure/nginx/) colocando frontend e API na mesma origem (porta 80, único serviço publicado à LAN) — antecipado da Fase 9 (seção 87) porque sem isso não havia como um navegador de verdade alcançar a API (que não publica porta própria).
  • Todas as telas do checklist de aceite (seção 90) implementadas e conectadas a endpoints reais: login, usuários, perfis/permissões, ramais, status de ramal, troncos, dialplan, filas, agentes, associação agente↔fila, motivos de pausa, console do agente (login/pausa/retirar pausa), campanhas (CPS, importação CSV, iniciar/pausar), monitoramento (discagem/agentes/fila) ao vivo, TME/TMA, busca de chamadas com filtro por fila/agente/estado/ telefone/data, exportação CSV, administração do Asterisk (status + diagnóstico allowlist + reload), auditoria. Adicionado queueId a GET /api/reports/calls (não existia antes) para permitir o filtro por fila explicitamente pedido na seção 90.
  • Callbacks (menu da seção 52) — não implementado nesta fase: o modelo Callback existe no schema desde a Fase 6, mas nunca houve controller/service de API para ele, e a seção 90 (critério literal de aceite) não exige essa tela. Criar uma tela sem a API por trás seria construir um mockup, o que a diretriz do projeto proíbe. Fica registrado como pendência explícita, não descartado silenciosamente.
  • Verificação visual em navegador não foi possível nesta sessão — ambiente é um servidor Debian headless sem display/browser. A verificação feita foi: tsc --noEmit limpo, eslint limpo, build de produção (next build) gerando as 27 rotas sem erro, e testes funcionais via curl reproduzindo exatamente as chamadas que o navegador faria — login via /api/auth/login, cookie de sessão validado pelo middleware (/ redireciona para /login sem cookie, libera com cookie), todas as 24 páginas protegidas retornando HTTP 200 através do Nginx, e os endpoints de dados (/api/dashboard, /api/reports/*, /api/compliance/* etc.) retornando dados reais com o mesmo cookie de sessão. O que não foi verificado: renderização visual real, interações de clique/formulário no DOM, responsividade, tema escuro na prática. Recomenda-se ao usuário abrir http://10.10.32.142/ em um navegador para essa validação final.

Fase 9 — Segurança e produção

  • Criptografia de segredos de trunk (AES-256-GCM) — já implementado na Fase 4 (packages/shared/src/secret-crypto.ts), confirmado que Trunk.secretEncrypted/Extension.sipPasswordEncrypted nunca retornam em texto puro em GET/PATCH.
  • HTTP security headers, CORS, CSRF, Helmet — Helmet + CORS com allowlist já ativos desde a Fase 3. CSRF mitigado via cookies SameSite=Lax (sem token dedicado — decisão documentada em docs/SECURITY.md, revisitar se o frontend algum dia sair da mesma origem da API).
  • nftables final revisado — regras conferidas contra o estado atual (Nginx na porta 80, AMI/ARI/SIP ainda bloqueados da interface LAN ens18, SSH nunca bloqueado). Nenhuma alteração necessária.
  • Logs estruturados JSON (sem segredos) — confirmado: redact.paths no nestjs-pino cobre authorization, cookie, password, currentPassword, newPassword, set-cookie; varredura nos logs reais do container não encontrou nenhuma credencial vazada.
  • Correlation IDs (request_id/attempt_id/call_id) — request_id via genReqId do pino desde a Fase 3; DialAttempt.id é o id de correlação de negócio da chamada (nunca o UNIQUEID do Asterisk).
  • Métricas Prometheus (GET /api/metrics) — novo nesta fase. apps/api/src/metrics/, biblioteca prom-client. Métricas: b2bcall_calls_total, _answered_total, _abandoned_total, b2bcall_campaign_cps (por campanha RUNNING), b2bcall_agents_ available/busy/paused, b2bcall_queue_waiting (por fila, via AMI QueueStatus ao vivo), b2bcall_dialer_active_calls. Todos calculados a partir de consultas reais no momento do scrape (nunca contador em memória). Protegido por monitoring.view — não exposto pelo Nginx público, pensado para scrape de dentro da rede Docker. Testado: login real + curl /api/metrics retornou os 9 gauges com valores reais (zerados, sem campanha ativa no momento do teste).
  • Bootstrap super_admin (senha aleatória, FIRST_LOGIN.txt, forçar troca) — já implementado na Fase 3, confirmado ainda funcional.
  • Seed (permissões, perfis, pausas, disposições) sem dados fake em produção — já implementado na Fase 3.
  • Backup/restore (postgres, asterisk config, .env seguro) — novo nesta fase. scripts/backup.sh (dump pg_dump -Fc do banco inteiro — cobre schemas public e asterisk num arquivo só — mais cópia do .env, retenção de 30 dias) e scripts/restore.sh (destrutivo, exige confirmação explícita digitando "restaurar"). Testado de verdade: backup.sh rodado contra o ambiente real, gerou dump de 97K + cópia do .env, ambos com chmod 600, fora do git (.gitignore atualizado para backups/*).
  • scripts/install.sh, update.sh, backup.sh, restore.sh, healthcheck.sh — todos novos nesta fase. healthcheck.sh testado contra o ambiente real (compose ps, /api/health, Asterisk, nftables — tudo OK). install.sh/update.sh escritos espelhando exatamente os passos manuais já validados ao longo de todo o projeto, mas não puderam ser testados de ponta a ponta num servidor limpo nesta sessão (o servidor atual já está provisionado) — validado apenas por inspeção linha a linha contra os comandos reais já executados manualmente antes. Risco residual documentado.
  • Nginx reverse proxy (80/443, WS, HTTPS documentado) — proxy na porta 80 já implementado/testado na Fase 8; HTTPS não configurado (rede privada, sem IP público) — passo a passo de como habilitar documentado em docs/OPERATIONS.md.
  • Disposição de chamada aplicada de fato — lacuna da Fase 6/8 fechada nesta fase: POST /api/agent-console/dispose com ações CALLBACK (cria Callback, lead → status CALLBACK) e DO_NOT_CALL (lead → status DO_NOT_CALL + entrada automática na lista de supressão). Testado ponta a ponta contra containers reais (ver nota de teste abaixo).
  • Agendamento de callback — lacuna da Fase 6/8 fechada nesta fase: apps/dialer-worker/src/callback-sweep.ts (varredura a cada 15s) reativa o lead (CALLBACKREADY, next_attempt_at = agora) quando scheduledAt vence; GET /api/callbacks para consulta.
  • Wrap-up automático — lacuna da Fase 6 fechada nesta fase: apps/dialer-worker/src/wrap-up-sweep.ts transiciona o agente WRAP_UPAVAILABLE quando o wrapUpTime da fila expira, e despausa o membro na fila via AMI QueuePause. Exigiu corrigir main.ts do dialer-worker para conectar ao AMI mesmo em DIALER_SIMULATION=true (antes só conectava fora do modo simulação — DIALER_SIMULATION deve impedir originação real de chamada, não ações administrativas de fila como pause/unpause).

Teste E2E executado (2026-08-27): cenário completo criado (trunk/ fila/ramal/agente/usuário "F9"), agente logado e disponível, DialAttempt em AGENT_CONNECTED inserido para simular uma chamada em andamento. Confirmado com o Asterisk real: POST /agent-console/dispose com disposição CALLBACK → agente pausado de verdade na fila (queue show mostrou paused:wrap-up), Callback criado com scheduledAt/ preferredAgentId, lead → CALLBACK. 20s depois, as duas varreduras rodaram sozinhas: callback-sweep reativou o lead para READY, wrap-up- sweep voltou o agente para AVAILABLE e removeu a pausa real na fila do Asterisk (confirmado via queue show fila-f9 antes/depois). Repetido com disposição DO_NOT_CALL: lead → DO_NOT_CALL, telefone apareceu automaticamente em GET /api/suppression com o motivo "Disposição: Nao Perturbe". Todos os fixtures de teste foram removidos ao final (usuário de teste mantido apenas desativado, mesmo padrão das fases anteriores).

Fase 10 — Testes e aceite

  • Unit tests (predictive engine, CPS limiter, permissions, phone norm, retry, state machines, TME/TMA, scheduling) — 33 testes em apps/dialer-worker + 13 em apps/api, todos passando (pnpm -r test).
  • Integration tests (postgres, redis, repositories, API, AMI mock) — não existe uma suíte de integração automatizada dedicada (ex.: testcontainers). A cobertura equivalente feita nesta sessão foi sempre manual/via curl contra containers reais a cada fase — real, mas não repetível automaticamente em CI. Lacuna conhecida.
  • E2E (login → ... → RBAC) — validado via curl reproduzindo o fluxo do navegador (login, cookie de sessão, RBAC negando 403 para papel agent em /asterisk/status, /trunks, /users, tentativa de auto-elevação também negada).
  • Modo simulação (DIALER_SIMULATION=true) + testes do predictive engine — simulation-harness.spec.ts, determinístico (seed fixa).
  • Prova de CPS respeitado / concorrência máxima / pacing reage a abandono / reprodutibilidade — cobertos por simulation-harness.spec.ts (testes automatizados). Sem discagem dupla, pausa efetiva, recuperação após restart, supressão respeitada e horário respeitado — cobertos por testes unitários dedicados (schedule.spec.ts) e/ou validados manualmente contra containers reais nas Fases 6/7 (reserva atômica FOR UPDATE SKIP LOCKED, reconciliation.ts, isSuppressed pré-originação) — não há um teste automatizado único que derrube o processo do worker de verdade no meio de uma chamada para provar a recuperação; a lógica de reconciliação em si tem teste unitário, mas o cenário "kill -9 do processo" não foi exercitado nesta sessão. Lacuna conhecida.
  • Quality gate — executado nesta fase: pnpm -r build (typecheck de todos os 7 workspaces, limpo), eslint em api e frontend (limpo), pnpm -r test (46 testes, 100% passando), docker compose config (válido), docker compose ps (8/8 serviços up, 6/8 com healthcheck reportando "healthy" — asterisk-events e dialer-worker não têm HTTP exposto para healthcheck formal, rodam sem crash e com log de atividade normal), scripts/healthcheck.sh (tudo OK).
  • Aceite de segurança (seção 92) — todos os 13 itens verificados ao vivo nesta fase contra o sistema real (não apenas por inspeção de código): nenhuma senha/.env no git, Postgres só em 127.0.0.1, Redis sem porta publicada, AMI/ARI bloqueados da LAN por nftables, rate limit de login testado (7ª tentativa consecutiva → 429), RBAC testado (usuário agent → 403 em /asterisk/status, /trunks, /users), auto-elevação negada, sem SQL injection (Prisma parametrizado em toda parte), sem path traversal (nenhum caminho de arquivo é construído a partir de input do usuário — nem no dialplan gerado, nem no download de CSV, cujo importId é validado como UUID antes de compor o header), logs sem credenciais (varredura real nos logs do container, redact.paths do pino confirmado). Ver docs/SECURITY.md para o detalhamento.
  • Aceite Asterisk (seção 93) — os 5 comandos executados e documentados nesta fase em docs/RELATORIO_FINAL.md: core show version, core show uptime, pjsip show endpoints, pjsip show contacts, queue show (últimos três vazios porque os fixtures de teste foram removidos — comportamento esperado). Comunicação API -> AMI -> Asterisk validada via GET /api/asterisk/status retornando amiControlConnection: up.
  • README final completo — README.md reescrito nesta fase com arquitetura, requisitos, instalação, primeiro acesso e mapa de toda a documentação em docs/.
  • Relatório final da implementação — docs/RELATORIO_FINAL.md (formato da seção 96), incluindo credenciais de acesso web e do banco de dados a pedido explícito do usuário.

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.