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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
227
docs/QA_SETUP.md
Normal 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` lê `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.
|
||||
Reference in New Issue
Block a user