# 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 - [x] 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. - [x] Reconciliação de estados órfãos após restart — `reconciliation.ts` (`reconcileOrphanedAttempts`), rodando a cada 60s via `main.ts`; força `DialAttempt`s 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). - [x] 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. - [x] 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. - [x] 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. - [x] 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. - [x] 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. - [x] 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 - [x] 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` (5–15s conforme a tela), suficiente para o volume atual mas não é WebSocket real. - [x] Logo processada (`b2bcall.png` copiada, original nunca modificada) + tema light/dark com toggle manual + `prefers-color-scheme`. - [x] 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. - [x] 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). - [x] 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) - [ ] 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.