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)