# Trunks (Troncos SIP) Agente.md seções 41-42. Primeiro recurso que grava configuração real no filesystem do FreeSWITCH (gateways Sofia) em vez de só responder via `mod_xml_curl`. ## Modelo `trunks` (tenant-scoped, RLS): `host`, `proxy`, `realm`, `register`, `username`/`password_enc` (AES-256-GCM, mesmo padrão de `sip_password_enc`), `from_user`/`from_domain`, `register_proxy`/`outbound_proxy`, `expire_seconds`, `retry_seconds`, `dtmf_mode`, `ping`/`ping_frequency`, `transport`, `max_cps`/`max_channels`, e `status`/`status_updated_at` (refletidos pelos eventos do FreeSWITCH, nunca escritos manualmente pela API). ## Mecanismo: por que arquivo + rescan, e não XML Curl "configuration" Ao contrário de Extensions (resolvido 100% via `mod_xml_curl` na hora do lookup), gateways Sofia não têm um binding XML Curl limpo e específico sem reativar a seção `configuration` inteira — e isso já causou problemas reais na fase XML Curl (chamadas HTTP desnecessárias no boot pra configs de outros módulos). Em vez disso, `b2bcall-fs-config`: 1. Gera um arquivo `.xml` por trunk habilitado em `sip_profiles/external/` (volume Docker compartilhado com o FreeSWITCH — o profile `external` da config vanilla já tem ``, então não precisou editar a config do profile). 2. Roda `sofia profile external rescan` via ESL — relê os gateways sem derrubar chamadas em andamento (diferente de `restart`). **Achado real (fase Realtime Monitoring)**: `rescan` só lê arquivos novos/alterados — apagar o arquivo `.xml` de um trunk removido não tira o gateway da memória do Sofia, ele fica "fantasma" indefinidamente (confirmado criando e apagando um trunk de teste via API e checando `sofia status gateway` depois do rescan). Corrigido: `trunk-sync.ts` agora roda `sofia profile external killgw ` pra cada gateway removido, antes do rescan — descarrega um gateway específico na hora, sem precisar reiniciar o profile inteiro. Reverificado ponta a ponta (criar → aparece em `sofia status gateway` → apagar via API → some imediatamente). ## Gatilho de sincronização `apps/api` não roda no Docker (ainda está no host) e `fs-config` não expõe porta pro host — então a notificação "algo mudou, resincroniza" vai por Redis pub/sub (`b2bcall:trunks:sync`), o mesmo mecanismo já usado pra eventos normalizados. `apps/api` publica depois de criar/apagar um trunk; `fs-config` também roda uma sincronização completa ao subir (cobre trunks criados enquanto ele estava fora do ar). **Achado real**: a primeira sincronização no boot corria antes da conexão ESL do `fs-config` terminar de se estabelecer, gerando um erro cosmético ("FreeSWITCH ESL nao conectado") — a escrita dos arquivos funcionava, só o rescan falhava. Corrigido com `FreeSwitchTelephonyProvider.waitUntilConnected()` (timeout de 5s) antes de tentar o rescan. ## Status do trunk (secao 42) `b2bcall-fs-events` já escutava `sofia::gateway_state` desde a fase Event Socket (normalizado pra `GATEWAY_UP`/`GATEWAY_DOWN`); nesta fase, ele passou a também escrever esse estado de volta em `Trunk.status`. Como o nome do gateway no FreeSWITCH é o `Trunk.id` (UUID) e o evento não diz de qual tenant é, a busca percorre os tenants ativos (`packages/database`'s `withTenantContext`) até achar o trunk dono daquele id — aceitável dado que mudança de estado de trunk é rara, não é um evento de alto volume por chamada. Mapeamento de estados brutos do Sofia pro enum interno (`apps/freeswitch-events/src/trunk-status.ts`): ``` UP, REGED → REGISTERED/UP TRYING, REGISTER → TRYING FAILED, FAIL_WAIT → FAILED DOWN → DOWN NOREG, UNREGED → UNREGISTERED qualquer outro → UNKNOWN ``` ## Verificado Criei um trunk de teste apontando pra um host inexistente (`sip.trunk-inexistente.invalid`, nunca resolve — RFC 2606) via API: - `fs-config` sincronizou (`count: 1`), escreveu o arquivo `.xml`, rodou o rescan com sucesso. - `sofia status gateway` no FreeSWITCH mostrou o gateway real, estado `FAIL_WAIT` (esperado — host não existe). - Confirma o pipeline `API → Postgres → fs-config → arquivo XML → rescan → FreeSWITCH` funcionando ponta a ponta sem precisar de nenhum tronco/ credencial real. ## O que falta - ~~Quota de troncos~~ — implementada na fase Plans/Entitlements (ver docs/ENTITLEMENTS.md), `assertQuota` chamado antes de criar. - `GET /trunks` não mostra `sofia status gateway` ao vivo, só o último status conhecido no banco — resolvido na fase Realtime Monitoring via WebSocket (ver docs/REALTIME.md). ## Correção: `GATEWAY_UP`/`DOWN` → `Trunk.status` (fase Realtime Monitoring) Documentado antes como "não confirmado com evento real" — na verdade nunca funcionava, e a causa raiz não tinha nada a ver com o tipo de falha do gateway. `b2bcall-fs-events` assinava os eventos CUSTOM do ESL passando `"CUSTOM"` como o **último** elemento da lista pro comando `event json`, sem nenhum subclass depois — e o protocolo do FreeSWITCH exige que os nomes de subclass (`sofia::gateway_state`, `sofia::register`, `callcenter::info`, ...) venham imediatamente depois do token `CUSTOM` no mesmo comando `event json`, senão zero eventos CUSTOM chegam, de qualquer subclass. Achado e corrigido durante a fase Agentes/Realtime Monitoring (mesmo bug afetava `callcenter::info`, ver docs/AGENTS.md). Reverificado ponta a ponta: criar um trunk com `register: true` apontando pra um host inexistente → `Trunk.status` no banco passa de `UNKNOWN` pra `FAILED` sozinho, sem polling, assim que o Sofia tenta e falha o registro.