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

496 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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] 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).
- [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` (515s 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
- [x] 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`.
- [x] 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).
- [x] 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.
- [x] 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.
- [x] 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).
- [x] 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).
- [x] Bootstrap super_admin (senha aleatória, FIRST_LOGIN.txt, forçar
troca) — já implementado na Fase 3, confirmado ainda funcional.
- [x] Seed (permissões, perfis, pausas, disposições) sem dados fake em
produção — já implementado na Fase 3.
- [x] 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/*`).
- [x] `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.
- [x] 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`.
- [x] 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).
- [x] 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 (`CALLBACK``READY`, `next_attempt_at = agora`)
quando `scheduledAt` vence; `GET /api/callbacks` para consulta.
- [x] Wrap-up automático — **lacuna da Fase 6 fechada nesta fase**:
`apps/dialer-worker/src/wrap-up-sweep.ts` transiciona o agente
`WRAP_UP``AVAILABLE` 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
- [x] 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.
- [x] 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).
- [x] Modo simulação (`DIALER_SIMULATION=true`) + testes do predictive
engine — `simulation-harness.spec.ts`, determinístico (seed fixa).
- [x] 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.
- [x] 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).
- [x] 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.
- [x] 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`.
- [x] README final completo — `README.md` reescrito nesta fase com
arquitetura, requisitos, instalação, primeiro acesso e mapa de toda
a documentação em `docs/`.
- [x] 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.