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

80
TODO.md
View File

@@ -2337,6 +2337,86 @@ a funcionalidade de verdade)
sobreviveu ao F5. Confirmado ainda que corrigir a Hydration
eliminou o erro do console sem quebrar nenhum dos dois cenários
## PHASE 65 — 3 achados reais reportados pelo usuário numa mensagem só:
edição de rota de entrada faltando, telas de Infraestrutura sempre com
erro de ESL, e ramal externo registrando sem áudio
- [x] Rotas de Entrada não tinha edição depois de criada (só criar/
remover) — mesmo padrão de Filas (form inline por linha,
`Fragment`-wrapped). DID não pode ser trocado depois de criado
(é a chave de roteamento — trocar quebraria o vínculo com o
tronco/operadora). Testado ponta a ponta com Playwright: criar,
editar descrição, F5, confirma que persistiu.
- [x] Telas de `Platform > Infraestrutura` sempre mostravam "Timeout
conectando no ESL" nesta VM — suposição registrada até então
(incorreta) era que isso seria permanente (`apps/api` fora do
Docker, porta nunca publicada). Causa real: `ESL_HOST=freeswitch`
no `.env` é um nome DNS que só existe dentro da rede do Docker,
nunca resolve a partir do host. O host SEMPRE alcança o IP de
qualquer container na rede bridge diretamente, com ou sem porta
publicada — só outras máquinas são bloqueadas sem `ports:`. Fix:
publicar 8021 só em loopback (`127.0.0.1:8021:8021`) + trocar
`ESL_HOST` pra `127.0.0.1`. Ver docs/FREESWITCH.md ("Achados na
sessão de PHASE 65") pro segundo bug achado na mesma investigação
(`show gateways as json` não é um comando válido nesta versão do
FreeSWITCH). Verificado com curl + Playwright nas 3 telas.
- [x] "Registrei o ramal e não passou áudio" — diagnosticado com
contadores de pacotes do `iptables` (não suposição): SIP chegava,
RTP nunca chegava. Causa: esta VM está atrás de um roteador (NAT)
sem port-forward configurado pra faixa de RTP — infraestrutura de
rede fora do controle desta aplicação, não um bug de código (a
configuração de NAT-detection do próprio FreeSWITCH já estava
correta, testado direto via `fs_cli -x "acl <ip> nat.auto"`). Ver
docs/NETWORK_ARCHITECTURE.md pro diagnóstico completo e o
checklist de portas pra encaminhar no roteador.
## PHASE 66 — Softphone WebRTC embutido (pedido do usuário: "integre o
widget handphone.js, é um webrtc que vai conectar em outro proxy [OpenSIPS,
já em produção] que depois vai vir via sip comum ao b2bcall")
- [x] O FreeSWITCH deste projeto nunca fala WebRTC — quem faz a ponte
WebRTC↔SIP é um OpenSIPS externo já em produção. Cada ramal
continua um registro SIP puro; só o NAVEGADOR do agente conecta
via WebRTC no OpenSIPS.
- [x] Código-fonte do widget (`handphone.js`, já commitado sem histórico
neste repo) achado em `git.falehandix.com.br/Handix/handphone-2.0`
(branch `main`, a mais atualizada — a branch default `handphone-
2.0-alpha` está desatualizada). Nesse código, o endereço do proxy
(`server`) vinha de `VITE_SIP_SERVER`, uma env var de BUILD-TIME —
incompatível com "Super Admin configura em runtime, sem redeploy".
Patch mínimo de 2 linhas em `widgetStorage.mergeConfig`/
`widgetConfig.parseWidgetConfig` (só nesta cópia local, nunca
enviado pro repo externo do usuário): `data-sip-server`/
`window.HandphoneConfig.server` agora tem prioridade sobre o env
var de build, mantendo o comportamento antigo como fallback.
Rebuildado (`npm run build:widget`) e vendorizado em
`apps/frontend/public/handphone.js` (era um arquivo solto na raiz
do repo antes desta fase).
- [x] `PlatformSetting` (tabela chave-valor genérica, sem RLS — mesmo
padrão de Role/Permission) pra guardar o endereço WSS do proxy
OpenSIPS, configurável em `Platform > Infraestrutura > Softphone
WebRTC` (permissão `freeswitch.configure`, mesma família das
outras telas de infra).
- [x] `GET /agents/me/softphone-config` (novo, `agents-me.controller.ts`)
— credenciais SIP do PRÓPRIO ramal vinculado ao agente logado
(nunca um `extensionId` arbitrário) + o endereço do proxy. Sem
`@RequirePermission` (mesmo padrão dos outros endpoints de
`agents/me`) — é o próprio agente pegando a própria senha, não uma
ação administrativa tipo `POST /extensions/:id/reveal-password`
(que exige `extensions.manage`, permissão que um agente comum
nunca tem). Cada acesso fica no audit log.
- [x] `SoftphoneWidget` (novo componente client, ao lado do
`AgentStatusWidget` na topbar do tenant) injeta um `<script
src="/handphone.js" data-sip-*>` sob demanda (só quando
`hasExtension`, uma vez por carregamento de página — nunca no
layout server-side, pra não decifrar a senha nem gravar audit log
a cada navegação). O widget se autoconecta sozinho via WebRTC
independente do estado de ACD (Disponível/Pausa/Offline) — mesmo
tipo de registro SIP que um telefone físico faria.
- [x] Testado ponta a ponta com Playwright, tenant/agente/ramal reais
criados na hora: tela de config do Super Admin salva e recarrega
certo; script do softphone injetado na topbar com
`sip-user`/`sip-domain`/`sip-server` corretos e a senha presente
(não vazia) assim que o agente com ramal vinculado abre `/app`.
---
## Riscos conhecidos