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

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

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

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

175
TODO.md
View File

@@ -321,33 +321,158 @@ banco. Todos os fixtures de teste foram removidos/desativados ao final.
`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)
- [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
- [ ] 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
- [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