Files
B2BCall-dialer/docker-compose.yml
Matheus 97ef8a6ba8 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
2026-08-30 22:14:39 -03:00

226 lines
8.9 KiB
YAML

services:
postgres:
image: postgres:18-alpine
container_name: b2bcall-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "127.0.0.1:5432:5432"
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
container_name: b2bcall-redis
restart: unless-stopped
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep -q PONG"]
interval: 5s
timeout: 5s
retries: 10
fs-config:
build:
context: .
dockerfile: apps/freeswitch-config/Dockerfile
container_name: b2bcall-fs-config
restart: unless-stopped
depends_on:
- postgres
- redis
environment:
# Hostname interno do compose (postgres/redis/freeswitch), nao
# localhost — ver docs/NETWORK_ARCHITECTURE.md.
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
FS_CONFIG_USER: ${FS_CONFIG_USER}
FS_CONFIG_PASSWORD: ${FS_CONFIG_PASSWORD}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
ESL_HOST: freeswitch
ESL_PORT: "8021"
ESL_PASSWORD: ${ESL_PASSWORD}
SOFIA_EXTERNAL_GATEWAYS_DIR: /gateways
CALLCENTER_QUEUES_DIR: /callcenter-queues
volumes:
# Compartilhado com o FreeSWITCH (agente.md secao 41-42): fs-config
# escreve os XML de gateway aqui, o profile "external" ja inclui
# sip_profiles/external/*.xml automaticamente (config vanilla).
- freeswitch_external_gateways:/gateways
# Compartilhado com o FreeSWITCH (agente.md secao 50-51): fs-config
# escreve um XML de fila por arquivo aqui, incluido via
# X-PRE-PROCESS no nosso callcenter.conf.xml (mesmo padrao acima).
- freeswitch_callcenter_queues:/callcenter-queues
# Sem porta publicada: so o FreeSWITCH (mesma rede do compose) chama isto.
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:8080/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
interval: 10s
timeout: 5s
retries: 10
start_period: 10s
freeswitch:
build:
context: ./infrastructure/freeswitch
secrets:
- freeswitch_pat
container_name: b2bcall-freeswitch
restart: unless-stopped
depends_on:
- fs-config
environment:
ESL_PASSWORD: ${ESL_PASSWORD}
FS_CONFIG_USER: ${FS_CONFIG_USER}
FS_CONFIG_PASSWORD: ${FS_CONFIG_PASSWORD}
volumes:
- freeswitch_external_gateways:/etc/freeswitch/sip_profiles/external
- freeswitch_callcenter_queues:/etc/freeswitch/autoload_configs/callcenter_queues.conf.d
# Bind mount (não volume nomeado do Docker) porque apps/api roda no
# host e precisa enxergar os mesmos arquivos que o FreeSWITCH grava
# (agente.md secao 90-91) — ver docs/RECORDING.md.
- ./data/recordings-spool:/recordings
# Prompts de áudio do IVR (PHASE 59) — mesmo raciocínio do bind
# mount acima, mas na direção contrária: apps/api (host) ESCREVE o
# arquivo que o usuário sobe, e o FreeSWITCH precisa LER esse mesmo
# arquivo ao vivo durante `play_and_get_digits`. Só funciona como
# bind mount de disco local — um fetch em rede (S3/HTTP) durante
# uma chamada ativa seria latência/confiabilidade desnecessárias
# pra um prompt de poucos segundos.
- ./data/ivr-prompts:/ivr-prompts
# Event Socket (8021) NUNCA publicado pra REDE — só em loopback do
# próprio host (agente.md secao 22: "NÃO pública... acesso somente
# pela aplicação autorizada"). Achado real testando as telas de
# Infraestrutura (platform): o comentário original aqui dizia que
# elas SEMPRE falhariam nesta VM porque "apps/api roda fora do
# Docker" — isso estava incompleto. `apps/api` (systemd, no host)
# JÁ conseguia alcançar o IP do container na rede bridge sem nenhuma
# porta publicada (`docker inspect` + `/dev/tcp` confirmam: o HOST
# sempre alcança a rede bridge do Docker, só outras MÁQUINAS é que
# não). O bloqueio real era `ESL_HOST=freeswitch` — um nome DNS que
# só existe dentro da rede interna do Docker, nunca no resolver do
# host. `127.0.0.1:8021:8021` resolve isso sem abrir a porta pra
# ninguém além do próprio host — mesma garantia de segurança de
# antes, só que a aplicação autorizada agora alcança de verdade.
# SIP (5060) e RTP (16384-16584, range fixo no Dockerfile) publicados
# pra rede a partir da PHASE 54 — necessário pra registrar um
# softphone/telefone de fora da rede Docker.
ports:
- "127.0.0.1:8021:8021"
- "5060:5060/udp"
- "5060:5060/tcp"
- "16384-16584:16384-16584/udp"
healthcheck:
test: ["CMD-SHELL", "fs_cli -p \"$$ESL_PASSWORD\" -x status | grep -q 'is ready'"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
fs-events:
build:
context: .
dockerfile: apps/freeswitch-events/Dockerfile
container_name: b2bcall-fs-events
restart: unless-stopped
depends_on:
- freeswitch
- redis
- postgres
environment:
ESL_HOST: freeswitch
ESL_PORT: "8021"
ESL_PASSWORD: ${ESL_PASSWORD}
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
# Hostnames internos do compose (freeswitch/redis), diferente do
# REDIS_URL do .env que aponta pra localhost (uso pelo apps/api, que
# ainda roda no host) — ver docs/NETWORK_ARCHITECTURE.md.
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
# Gravações (agente.md secao 90-93) — mesmo padrão do REDIS_URL
# acima: path Docker-interno aqui, path do host no .env pro
# apps/api enxergar os mesmos arquivos (ver docs/RECORDING.md).
RECORDINGS_SPOOL_DIR: /recordings
STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local}
LOCAL_STORAGE_ROOT: /data/object-storage
S3_BUCKET: ${S3_BUCKET:-}
S3_REGION: ${S3_REGION:-}
S3_ENDPOINT: ${S3_ENDPOINT:-}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
volumes:
- ./data/recordings-spool:/recordings
- ./data/object-storage-local:/data/object-storage
predictive-dialer:
build:
context: .
dockerfile: apps/predictive-dialer/Dockerfile
container_name: b2bcall-predictive-dialer
restart: unless-stopped
depends_on:
- freeswitch
- redis
- postgres
environment:
ESL_HOST: freeswitch
ESL_PORT: "8021"
ESL_PASSWORD: ${ESL_PASSWORD}
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
# Real Outbound Safety (agente.md secao 186): as DUAS precisam estar
# explicitamente ligadas pra originar PSTN de verdade — nunca ativado
# por default. Ver docs/PREDICTIVE_DIALER.md.
DIALER_SIMULATION: ${DIALER_SIMULATION:-true}
ALLOW_REAL_OUTBOUND_CALLS: ${ALLOW_REAL_OUTBOUND_CALLS:-false}
# Só usado pra montar o path do record_session (agente.md secao 91)
# — quem de fato grava e' o processo FreeSWITCH, não este worker;
# não precisa do volume montado aqui, só saber o path Docker-interno.
RECORDINGS_SPOOL_DIR: /recordings
ai-worker:
build:
context: .
dockerfile: apps/ai-worker/Dockerfile
container_name: b2bcall-ai-worker
restart: unless-stopped
depends_on:
- postgres
environment:
APP_DATABASE_URL: postgresql://${POSTGRES_APP_USER}:${POSTGRES_APP_PASSWORD}@postgres:5432/${POSTGRES_DB}?schema=public
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
# Precisa ler as gravações que fs-events já subiu (agente.md secao
# 104-108) — mesmo par de env vars/bind mount de fs-events, ver
# docs/RECORDING.md e docs/AI_PIPELINE.md.
STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local}
LOCAL_STORAGE_ROOT: /data/object-storage
S3_BUCKET: ${S3_BUCKET:-}
S3_REGION: ${S3_REGION:-}
S3_ENDPOINT: ${S3_ENDPOINT:-}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
volumes:
- ./data/object-storage-local:/data/object-storage
secrets:
freeswitch_pat:
environment: FREESWITCH_PAT
volumes:
postgres_data:
redis_data:
freeswitch_external_gateways:
freeswitch_callcenter_queues: