feat: edição de rotas de entrada, fix de 2 bugs reais no ESL, diagnóstico de NAT/áudio e softphone WebRTC (PHASE 65/66)

Três achados reportados pelo usuário numa mensagem só: (1) Rotas de Entrada
não tinha edição depois de criada — implementada no mesmo padrão de Filas;
(2) telas de Platform > Infraestrutura sempre davam "Timeout no ESL" nesta
VM — não era limitação permanente como o comentário antigo dizia, e sim
ESL_HOST=freeswitch (nome DNS que só existe dentro da rede do Docker) mais
um segundo bug independente (`show gateways as json` não é comando válido
nesta versão do FreeSWITCH); (3) ramal externo registrava mas sem áudio —
diagnosticado com contadores de pacote do iptables: a VM está atrás de um
roteador sem port-forward pra faixa de RTP, achado de infraestrutura de
rede, não bug de código.

Também integra o softphone WebRTC (handphone.js/OpenSIPS, já em produção):
código-fonte encontrado em git.falehandix.com.br/Handix/handphone-2.0,
patch mínimo pra aceitar o endereço do proxy em runtime (era build-time),
nova config global (Platform > Infraestrutura > Softphone WebRTC) e widget
na topbar do tenant que pega usuário/domínio/senha do ramal vinculado ao
agente logado.

Adiciona docs/QA_SETUP.md — runbook completo pra subir o ambiente do zero
numa máquina nova (Docker, migrations, seed, systemd), e completa o
.env.example que estava faltando a maioria das variáveis reais.

Testado ponta a ponta com Playwright: edição de rota (criar/editar/F5),
as 3 telas de Infraestrutura com dado real, e um tenant/ramal/agente de
teste criados na hora confirmando que o script do softphone recebe as
credenciais certas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 22:14:39 -03:00
parent a779a7e51f
commit 97ef8a6ba8
25 changed files with 1047 additions and 58 deletions

View File

@@ -65,11 +65,48 @@ imagem, só injetada via variável de ambiente do container. O entrypoint falha
alto (`set -eu` + `${ESL_PASSWORD:?...}`) se a variável não estiver definida —
nunca sobe com a senha padrão por engano.
Porta 8021 **não é publicada no host** (`ports:` ausente no compose) — só
alcançável por outros containers na rede interna do Docker Compose
(`b2bcall_default`), pelo nome de serviço `freeswitch`. Isso satisfaz
"NÃO pública... acesso somente pela aplicação autorizada" sem precisar de ACL
adicional por enquanto (ACL fica como hardening futuro, ver TODO).
Porta 8021 **nunca publicada pra rede** — só em loopback do próprio host
(`"127.0.0.1:8021:8021"` no `docker-compose.yml`, desde a PHASE 65, ver seção
abaixo). Isso satisfaz "NÃO pública... acesso somente pela aplicação
autorizada" (agente.md secao 22) sem abrir a porta pra ninguém além do próprio
host.
## Achados na sessão de PHASE 65 (Platform > Infraestrutura)
Duas telas de `platform/infraestrutura` (FreeSWITCH, SIP Profiles, Nodes)
sempre mostravam "Timeout conectando no ESL do FreeSWITCH" nesta VM. A
suposição registrada até então (agora corrigida) era que isso seria uma
limitação permanente: "`apps/api` roda fora do Docker, a porta nunca é
publicada, então este endpoint sempre falha aqui". Isso estava incompleto.
**Causa real**: `ESL_HOST=freeswitch` no `.env` — um nome DNS que só resolve
dentro da rede interna do Docker Compose (embedded DNS), nunca a partir do
resolver do próprio host (confirmado: `getent hosts freeswitch` falha no host
com exit code 2). Não era uma questão de porta publicada — testado e
confirmado que o **host sempre alcança o IP de qualquer container na rede
bridge do Docker diretamente** (`docker inspect` + `/dev/tcp` bateram), mesmo
sem nenhuma porta publicada. Só outras *máquinas* são bloqueadas sem
`ports:` — o host nunca foi.
**Fix**: publicar 8021 só em loopback (`127.0.0.1:8021:8021`) e trocar
`ESL_HOST` pra `127.0.0.1` no `.env` — resolve o DNS sem abrir a porta pra
rede nenhuma (confirmado via `ss -tlnp`: `docker-proxy` escutando em
`127.0.0.1`, não `0.0.0.0`). Os serviços que rodam DENTRO do Docker
(`fs-events`, `fs-config`, `predictive-dialer`) continuam com
`ESL_HOST=freeswitch` fixo no próprio `docker-compose.yml` — não dependem do
`.env` pra isso, então a troca não afeta eles.
**Segundo bug, achado na mesma investigação**: a tela "Nodes" continuava
mostrando `error: ""` (string vazia) mesmo depois do fix acima.
`getGateways()` (`packages/telephony/src/freeswitch-provider.ts`) rodava
`show gateways as json`**não é um sub-comando válido** de `show` nesta
versão do FreeSWITCH (`fs_cli -x "show gateways as json"` devolve
`-USAGE: codec|endpoint|application|...`). O pacote `esl` trata qualquer
reply começando com `-` como erro de protocolo e rejeita a promise com um
`FreeSwitchError` cujo `.message` pode vir vazio — daí o `""` na tela em vez
de uma mensagem útil. Fix: `getGateways()` agora roda `sofia status` (mesmo
comando já usado pela tela de SIP Profiles) — gateways aparecem como linhas
`type=gateway` no texto puro, sem variante JSON dedicada nesta versão.
## O que NÃO foi feito nesta fase (fica para as próximas, por design)

View File

@@ -27,6 +27,34 @@ topologia esperada é `Internet → OpenSIPS → rede SIP privada → FreeSWITCH
então o FreeSWITCH em si tende a ficar em rede privada mesmo, o que favorece
manter bridge/macvlan em vez de host).
## Achado real (PHASE 65): VM atrás de roteador, ramal externo sem áudio
Esta VM de laboratório está atrás de um roteador (NAT) — o IP da interface de
rede (`ens18`) é privado (`10.10.32.x`), o roteador é quem faz NAT pro IP
público real. Reportado pelo usuário: um softphone externo registrava com
sucesso, mas sem áudio.
Diagnóstico (contadores de pacotes do `iptables`, não suposição): 10 pacotes
chegaram em `5060/udp` (SIP), **zero** pacotes chegaram em qualquer porta da
faixa de RTP (`16384-16584/udp`). O REGISTER funciona porque a resposta
trafega de volta pelo mesmo "buraco" NAT que o próprio pacote do cliente abriu
— mas RTP usa portas completamente diferentes, como um fluxo NOVO. Sem uma
regra de **port forward no roteador** pra essa faixa (apontando pro IP
privado desta VM), esses pacotes nunca chegam até o Docker/FreeSWITCH.
Do lado desta VM estava tudo certo: `docker-compose.yml` publica a faixa de
RTP pra rede (não só loopback), `Ext-RTP-IP`/`Ext-SIP-IP` resolvem certo via
STUN (`sofia status profile internal` mostra o IP público real), e a
detecção de NAT do próprio FreeSWITCH (`apply-nat-acl value="nat.auto"`,
`nat.auto` é uma ACL auto-gerada pelo core do FreeSWITCH — RFC1918 exceto a
própria rede local do container, não precisa existir em `acl.conf.xml`)
funciona corretamente (testado direto via `fs_cli -x "acl <ip> nat.auto"`).
**Não tem fix de código pra isso** — é infraestrutura de rede fora do
controle desta aplicação. Checklist pra quem for expor telefonia real atrás
de um roteador: encaminhar UDP `5060` e toda a faixa RTP (`16384-16584`,
Dockerfile do FreeSWITCH) pro IP privado da VM, sem tradução de porta.
## Quando `apps/api` virar container
Hoje ela roda no host por conveniência de desenvolvimento. Quando virar o

227
docs/QA_SETUP.md Normal file
View File

@@ -0,0 +1,227 @@
# Subindo o ambiente do zero numa máquina nova (QA)
Runbook pra clonar este repositório numa máquina nova e deixar tudo rodando
— Postgres/Redis/FreeSWITCH em Docker, `apps/api`/`apps/frontend` como
services systemd, banco migrado e semeado. Escrito pra outra sessão do
Claude Code seguir sozinha, na ordem, verificando cada passo antes do
próximo. Nada aqui precisa de intervenção manual além do que está marcado
explicitamente como "precisa de um humano".
Este documento descreve como levantar um ambiente **igual ao de
desenvolvimento/laboratório** (`ACESSO_TESTE.md`), não produção: sem HTTPS,
sem domínio próprio, `DIALER_SIMULATION=true` (nenhuma chamada PSTN real
sai). Rodar em produção de verdade é fora de escopo deste guia.
## 0. Pré-requisitos da máquina
- Linux com `systemd` (as units em `infrastructure/systemd/` assumem isso)
- Docker + Docker Compose plugin (`docker compose version` funcionando)
- Node.js >= 22 (`node -v`) e `pnpm` (`corepack enable` costuma bastar —
o `packageManager` no `package.json` da raiz fixa a versão exata)
- Pelo menos ~2GB de RAM livres pro Postgres+Redis+FreeSWITCH juntos, mais o
que o Next.js dev server e o `tsc` do `apps/api` consumirem
- Este repositório **precisa ficar em `/opt/b2bcall`** — as units systemd em
`infrastructure/systemd/*.service` têm esse caminho fixo
(`WorkingDirectory=`, `EnvironmentFile=`). Cloná-lo em outro lugar exige
editar essas duas linhas nas duas units antes de instalar.
Verifique antes de prosseguir:
```bash
docker compose version
node -v # >= 22
corepack enable && corepack prepare pnpm@$(node -e "console.log(require('/opt/b2bcall/package.json').packageManager.split('@')[1])") --activate
pnpm -v
```
## 1. Clonar e instalar dependências
```bash
cd /opt
git clone <url-do-repositorio> b2bcall # ou `git pull` se já existe
cd /opt/b2bcall
pnpm install
```
## 2. Criar o `.env`
`.env` nunca é commitado (segredos reais). Copie o template e preencha:
```bash
cp .env.example .env
```
Abra `.env.example` pra ver o comentário de cada variável — ele já explica
o que cada uma faz e como gerar as que são segredos aleatórios
(`openssl rand -hex 32` / `openssl rand -base64 24`). Resumo do que precisa
de valor manual:
| Variável | Como preencher |
|---|---|
| `FREESWITCH_PAT` | **Precisa de um humano** — token de acesso ao repositório de pacotes do FreeSWITCH (`freeswitch.signalwire.com`), não é gerável localmente. Peça ao dono do projeto. |
| `POSTGRES_PASSWORD`, `REDIS_PASSWORD`, `POSTGRES_APP_PASSWORD` | `openssl rand -base64 24` (gerar um valor diferente pra cada) |
| `JWT_SECRET`, `JWT_REFRESH_SECRET`, `ENCRYPTION_KEY` | `openssl rand -hex 32` (gerar um valor diferente pra cada — `ENCRYPTION_KEY` cifra segredos reversíveis como senha SIP de ramal, nunca reaproveitar entre ambientes) |
| `ESL_PASSWORD` | `openssl rand -base64 24` |
| `FS_CONFIG_PASSWORD` | `openssl rand -base64 24` |
| `DATABASE_URL` | `postgresql://b2bcall:<POSTGRES_PASSWORD>@localhost:5432/b2bcall?schema=public` |
| `REDIS_URL` | `redis://:<REDIS_PASSWORD>@localhost:6379` |
| `APP_DATABASE_URL` | `postgresql://b2bcall_app:<POSTGRES_APP_PASSWORD>@localhost:5432/b2bcall?schema=public` |
| `FS_CONFIG_USER` | Qualquer string curta sem espaço (ex.: `fsconfig`) |
Todas as outras variáveis do `.env.example` já têm um valor padrão
sensato pra este tipo de ambiente (deixe como está).
Crie também `apps/frontend/.env.local` (não existe ainda num clone novo,
também gitignored):
```bash
echo "B2BCALL_API_URL=http://localhost:3000" > apps/frontend/.env.local
```
## 3. Subir os containers Docker
```bash
cd /opt/b2bcall
docker compose up -d --build
```
O `docker compose``FREESWITCH_PAT` automaticamente do `.env` na raiz
pro build da imagem do FreeSWITCH (BuildKit secret — não fica em nenhuma
camada da imagem final). O build do FreeSWITCH baixa pacotes `.deb`
externos, pode levar alguns minutos na primeira vez.
Verifique que todos os 7 containers subiram saudáveis:
```bash
docker compose ps
```
Espera-se `b2bcall-postgres`, `b2bcall-redis`, `b2bcall-freeswitch`,
`b2bcall-fs-config`, `b2bcall-fs-events`, `b2bcall-predictive-dialer`,
`b2bcall-ai-worker` todos `Up` (os 3 primeiros com healthcheck `healthy`).
Se algum não subir, `docker compose logs <serviço>` quase sempre mostra a
causa na hora (comum: `.env` com uma variável faltando/typo).
## 4. Migrar e semear o banco
```bash
cd /opt/b2bcall/packages/database
set -a && source /opt/b2bcall/.env && set +a
npx prisma migrate deploy
npx prisma generate
```
Isso cria todas as tabelas, incluindo a role restrita `b2bcall_app` (não
tem senha ainda — a migration não pode embutir um segredo). Defina a senha
dela agora:
```bash
cd /opt/b2bcall
bash scripts/db-setup-app-role.sh
```
Depois, rode o seed (idempotente — cria o catálogo de permissions/roles do
sistema e o usuário Platform Super Admin):
```bash
pnpm --filter @b2bcall/auth run seed
```
A senha temporária do Platform Super Admin (`admin@b2bcall.local`) é
gravada em `/opt/b2bcall/FIRST_LOGIN.txt` (permissão 600, gitignored) —
**leia esse arquivo pra saber a senha do primeiro login**, ele não aparece
no terminal.
## 5. Instalar os services systemd
```bash
cd /opt/b2bcall
sudo cp infrastructure/systemd/b2bcall-api.service /etc/systemd/system/
sudo cp infrastructure/systemd/b2bcall-frontend.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now b2bcall-api.service
sudo systemctl enable --now b2bcall-frontend.service
```
Verifique:
```bash
systemctl status b2bcall-api.service b2bcall-frontend.service
journalctl -u b2bcall-api.service -n 50 # esperado: "Nest application successfully started" (ou similar)
```
`apps/api` recompila (`tsc`) toda vez que a unit sobe — leva uns 15-20s.
Detalhes/troubleshooting em `infrastructure/systemd/README.md`.
## 6. Verificação de ponta a ponta
```bash
curl -s http://localhost:3000/health/ready # apps/api + Postgres + Redis, tudo respondendo
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3001/login # apps/frontend respondendo (espera 200)
```
Depois, pelo navegador (ou um túnel SSH se a máquina não for acessível
direto: `ssh -L 3001:localhost:3001 <usuario>@<ip-da-maquina>`):
1. Acesse `http://<ip-da-maquina>:3001/login`
2. Entre com `admin@b2bcall.local` e a senha de `FIRST_LOGIN.txt`
3. A tela "Trocar senha" aparece automaticamente (senha temporária,
`mustChangePassword=true`) — troque por uma senha sua
4. Depois de trocar, você cai em `/platform` (Platform Super Admin)
5. Crie um tenant de teste em **Clientes > Tenants** pra ter algo pra
testar (a senha do admin desse tenant também é mostrada uma única vez
na tela — anote ou copie na hora)
## 7. Se for testar telefonia real (SIP/RTP de fora da rede Docker)
Só necessário se for registrar um softphone/telefone físico de verdade
contra este ambiente — não é preciso pra testar só a aplicação web.
`docker-compose.yml` já publica `5060/udp`, `5060/tcp` e a faixa de RTP
`16384-16584/udp` pra rede (não só loopback). **Se esta máquina estiver
atrás de um roteador/NAT** (IP da interface de rede é privado, tipo
`10.x.x.x`/`192.168.x.x`), é preciso configurar **port forward no
roteador** pra essas mesmas portas apontando pro IP privado desta máquina
— sem isso, o registro SIP até funciona (a resposta volta pelo mesmo
"buraco" que o cliente abriu), mas o **áudio não passa** (RTP é um fluxo
novo, em portas diferentes, e chega bloqueado). Isto foi um achado real
diagnosticado numa sessão anterior — detalhes completos e como confirmar
com contadores de pacote do `iptables` em `docs/NETWORK_ARCHITECTURE.md`
("Achado real (PHASE 65)").
## 8. Softphone WebRTC (widget embutido, PHASE 66)
O app do tenant tem um softphone WebRTC embutido (`apps/frontend/public/
handphone.js`) que conecta via WebRTC num proxy OpenSIPS externo (já em
produção, fora deste repositório) — o FreeSWITCH desta instalação nunca
fala WebRTC diretamente, cada ramal continua um registro SIP puro.
Pra funcionar, um Platform Super Admin precisa configurar o endereço WSS
desse proxy em **Platform > Infraestrutura > Softphone WebRTC**
(`PUT /platform/webrtc-proxy`, guardado na tabela `platform_settings`
não tem nada a configurar via `.env` ou variável de ambiente pra isso).
Sem essa configuração, o widget simplesmente não conecta (fica sem
aparecer/logar um aviso no console do navegador) — não é um erro de
deploy, só falta esse passo manual pós-subida.
## Referência rápida — o que roda onde
| Componente | Como sobe | Restart depois de mudar código |
|---|---|---|
| Postgres, Redis, FreeSWITCH, fs-config, fs-events, predictive-dialer, ai-worker | Docker (`restart: unless-stopped`) | `docker compose build <serviço> && docker compose up -d <serviço>` |
| `apps/api` | systemd (`b2bcall-api.service`), `pnpm dev` (modo dev, não build de produção — decisão deliberada, ver `infrastructure/systemd/README.md`) | `systemctl restart b2bcall-api.service` |
| `apps/frontend` | systemd (`b2bcall-frontend.service`), `pnpm dev` | Hot-reload sozinho (Next.js dev server); `systemctl restart` só depois de mudar dependências |
## Problemas comuns
- **`EADDRINUSE` na porta 3000/3001**: confira que `API_PORT=3000` está no
`.env` e que a unit do frontend tem `Environment=PORT=3001` — sem os
dois fixos, os dois processos competem pela mesma porta.
- **Timeout conectando no ESL do FreeSWITCH** (telas de
`Platform > Infraestrutura`): confira `ESL_HOST=127.0.0.1` no `.env`
(nunca `freeswitch` — esse nome só resolve dentro da rede Docker) e que
a porta 8021 está publicada em loopback (`docker compose ps` deve
mostrar `127.0.0.1:8021->8021/tcp` no FreeSWITCH).
- **RLS bloqueando tudo silenciosamente** (contagens sempre zero, listas
sempre vazias mesmo com dado no banco): confirme que
`scripts/db-setup-app-role.sh` rodou depois da migration — sem a senha
do role `b2bcall_app` setada, `APP_DATABASE_URL` não autentica e
`apps/api` provavelmente nem sobe.
- **Container do FreeSWITCH não builda**: quase sempre `FREESWITCH_PAT`
errado/expirado no `.env``docker compose logs freeswitch` mostra o
erro de autenticação do `apt` contra o repositório do SignalWire.