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
6.7 KiB
FreeSWITCH
Imagem própria em infrastructure/freeswitch/ (agente.md secao 19), rodando como
serviço Docker freeswitch (container b2bcall-freeswitch).
Por que pacotes prontos, e não compilar da fonte
O servidor de desenvolvimento tem só ~1.9GB de RAM. Compilar FreeSWITCH (C/C++,
dezenas de módulos) arriscaria OOM e levaria muito tempo. Em vez disso, a imagem
usa os pacotes .deb pré-compilados do repositório oficial do SignalWire
(freeswitch.signalwire.com), autenticado com FREESWITCH_PAT — exatamente o
uso previsto para essa credencial (agente.md secao 11). O repositório do
SignalWire tem pacotes para o codename trixie, então a imagem usa
debian:trixie-slim como base (mesma versão do host, mas isso é coincidência —
o container é isolado e poderia usar qualquer Debian suportado pelo repo).
Manuseio do PAT (agente.md secao 11)
FREESWITCH_PAT entra no build via Docker BuildKit secret
(--mount=type=secret,id=freeswitch_pat), nunca como ARG/ENV. O arquivo de
credenciais do apt (/etc/apt/auth.conf.d/freeswitch.conf, que contém o PAT em
texto puro) é criado e apagado dentro do mesmo RUN, então nunca aparece em
nenhuma camada da imagem final — verificado com docker history --no-trunc.
docker-compose.yml declara o secret assim:
secrets:
freeswitch_pat:
environment: FREESWITCH_PAT
e o serviço freeswitch referencia secrets: [freeswitch_pat] em build:.
Pacotes instalados
freeswitch-meta-vanilla # core + config de referência (a mesma usada em
# praticamente todo tutorial/livro de FreeSWITCH)
freeswitch-conf-vanilla # ⚠ Recommends de meta-vanilla, não Depends —
# precisa ser listado explicitamente com
# --no-install-recommends (foi um bug real
# durante o setup: sem isso /etc/freeswitch
# fica vazio e o container entra em crash loop)
freeswitch-mod-callcenter # ACD (agente.md secao 37)
freeswitch-mod-avmd # detecção de caixa postal/beep (secao 87)
freeswitch-mod-curl # chamadas HTTP a partir do dialplan
mod_xml_curl está instalado mas desativado em
overrides/autoload_configs/modules.conf.xml — o módulo se recusa a carregar
sem pelo menos um binding com gateway-url configurada ("Binding has no
url!"), e essa URL só existirá quando o b2bcall-fs-config for criado (fase
"XML Curl", logo em seguida). Reativar lá.
mod_signalwire foi removido da lista de módulos: é específico da nuvem do
SignalWire, que não usamos (só o repositório de pacotes).
Event Socket (agente.md secao 22)
Senha alterada da padrão (ClueCon) para ${ESL_PASSWORD} (gerado com
openssl rand, vive só em .env) via entrypoint.sh, que faz um sed no
event_socket.conf.xml em runtime — a senha real nunca é copiada para a
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 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)
Seguindo a ordem do próprio agente.md (secao 232): esta fase só cobre "ter o
FreeSWITCH rodando e alcançável". As próximas fases constroem em cima:
- Event Socket:
b2bcall-fs-events, conexão ESL permanente (secao 21). - XML Curl:
b2bcall-fs-config, reativamod_xml_curlapontando pra esse serviço (secao 26). - Extensions/Trunks/Dialplan: hoje o directory/dialplan estático da config
vanilla continua com os 20 ramais de teste (1000-1019, senhas fracas — README
do próprio pacote avisa isso). Como as portas SIP não estão publicadas no
host, isso fica contido, mas precisa ser substituído por
mod_xml_curldinâmico antes de qualquer tronco/ramal real existir. - mod_odbc_cdr: não instalado ainda — só faz sentido junto da fase de CDR.
Verificação manual
source <(grep '^ESL_PASSWORD=' .env)
docker exec b2bcall-freeswitch fs_cli -p "$ESL_PASSWORD" -x "status"
docker exec b2bcall-freeswitch fs_cli -p "$ESL_PASSWORD" -x "module_exists mod_sofia"