nftables bloqueava incondicionalmente SIP (5060/udp) e RTP (10000-20000)
vindos da interface LAN — impedia qualquer softphone real de registrar,
já que o OpenSIPS que ficaria na frente (docs/OPENSIPS.md) não foi
implantado. Liberado para toda a faixa RFC1918 (10.0.0.0/8, 172.16.0.0/12,
192.168.0.0/16), mantendo AMI/ARI bloqueados incondicionalmente. Aplicado
ao vivo no servidor.
Ramais: agora dá para ver e trocar a senha SIP diretamente no formulário
de edição (GET /extensions/:id/password, decifra sob demanda — diferente
da senha de login, a senha SIP é cifrada, não hasheada, porque o próprio
Asterisk precisa dela em texto puro para autenticar; expor sob permissão
extensions.update e auditado é razoável). Evita ter que redefinir toda
vez que o operador esquece a senha configurada num softphone.
Causa raiz do 'body is empty' relatado pelo usuário ao clicar em
redefinir senha do ramal: o cliente sempre mandava Content-Type:
application/json mesmo em requisições sem corpo (POST/PATCH de ação, ex.:
reset-password, agent-console available/pause/unpause/logout, dialplan
publish/rollback, campanha start/pause/stop/drain). O Fastify rejeita
isso com 400 'Body cannot be empty when content-type is set to
application/json' — nunca detectado antes porque todo teste anterior foi
via curl sem fixar esse header, não reproduzindo o comportamento real do
fetch() do navegador. Corrigido na única função request() central
(apps/frontend/src/lib/api-client.ts): só envia o header quando há de
fato um corpo.
Bug relacionado encontrado na mesma revisão: DELETE /api/suppression/:id
exige removalReason no corpo, mas api.delete() nem aceitava um argumento
de corpo — o botão 'Desbloquear' da lista de bloqueio sempre falhava com
403. api.delete() agora aceita body opcional; a tela pede o motivo via
prompt antes de remover.
O endpoint POST /extensions/:id/reset-password já retornava a senha em
texto puro (mesmo padrão da criação), mas o frontend descartava o
resultado e só mostrava um toast genérico — o usuário nunca via a senha
nova, precisando adivinhar ou reconfigurar de novo. Reaproveita o mesmo
padrão de diálogo 'copie agora' já usado na criação do ramal.
Nova aba 'Dialplan -> Rotas de Saída': abstração amigável sobre o dialplan
bruto, no mesmo espírito das Outbound Routes do Issabel/FreePBX. O usuário
informa (prepend) + prefix | match pattern (sintaxe de padrão do Asterisk:
X/Z/N/faixas/coringas, sem o '_' inicial) e escolhe o tronco, sem escrever
exten=>/Dial() à mão.
- packages/database: model OutboundRoute + migration
- apps/api/src/outbound-routes: gerador determinístico (contexto
b2bcall-outbound-routes, incluído em b2bcall-agents — contexto padrão de
Extension.context), service/controller CRUD reaproveitando as permissões
dialplans.*, 6 testes unitários
- apps/api/src/dialplan/dialplan.service.ts: publish() agora gera e
verifica também o contexto das rotas, no mesmo pipeline de
versionamento/rollback automático das entradas de dialplan brutas
- apps/frontend: aba com formulário (prepend/prefix/padrão/tronco) e
preview ao vivo da máscara resultante
O prefixo é sempre removido do número antes de discar e substituído pelo
prepend — o tronco nunca vê o prefixo digitado pelo agente, só o número já
mascarado. Não usado pelo discador preditivo (campanhas já sabem o tronco
via Campaign.trunkId diretamente).
Build/lint/testes (api+frontend) verificados; teste end-to-end contra o
Asterisk real ficou pendente porque a senha do super_admin foi trocada
durante a sessão (acesso legítimo do usuário) — validação via UI delegada
ao usuário.
- apps/frontend: Next.js 15 (App Router) + Tailwind v4 + componentes estilo
shadcn/ui sobre Radix UI + TanStack Query. Tema light/dark, logo
processada. Menu completo (secao 52) com gating por permissao real.
Todas as telas do checklist de aceite (secao 90) conectadas a endpoints
reais (nao mockup): login, usuarios, perfis/permissoes, ramais/troncos,
dialplan, filas/agentes, console do agente, campanhas (CPS/CSV/
iniciar/pausar), monitoramento ao vivo (polling, nao WebSocket real),
TME/TMA, busca/export de chamadas, administracao do Asterisk, auditoria
- infrastructure/nginx: reverse proxy colocando frontend+API na mesma
origem (porta 80), antecipado da Fase 9 pois a API nao publica porta
propria
- apps/api: GET /api/monitoring/agents (estado corrente real via
agent_state_events em aberto) e filtro queueId em GET /api/reports/calls
Pendencia registrada: tela de Callbacks nao implementada (schema existe
desde a Fase 6, mas nunca houve controller/service — construir a tela sem
API real seria mockup). Verificacao visual em navegador nao foi possivel
neste ambiente headless; validado via tsc/eslint/next build limpos + curl
reproduzindo as chamadas do navegador (middleware de auth, 24 paginas
protegidas via Nginx, endpoints de dados com cookie de sessao).