feat(ivr): tela de autoria de menu de IVR no frontend

Pedido do usuário: "constrói a tela de IVR no frontend". Até aqui um
menu de IVR só existia se alguém escrevesse as regras à mão no editor
genérico de dialplan (o que eu fiz manualmente pra testar na PHASE 56) —
sem UI nenhuma pra isso.

`IvrMenu`/`IvrMenuOption` (RLS real): nome + contexto (derivado do nome)
+ opções (dígito → ramal + rótulo opcional). Nenhuma tabela nova pro
dialplan em si — `IvrMenusController` compila o menu inteiro em
`DialplanExtension`/`DialplanVersion` do contexto do menu
(`buildIvrDialplanExtensions`, packages/telephony) usando as MESMAS 2
formas de `<extension>` já testadas com DTMF real na PHASE 56 (entrada
com `play_and_get_digits` + `transfer` usando o dígito coletado como
novo destination_number, uma extension por dígito) — e já gera + ativa
a versão nova automaticamente, o mesmo generate+activate manual que o
editor de dialplan faz, só que embutido no create/update do menu.

`greeting` (o prompt do menu) é texto livre do tenant, então passa pela
MESMA proteção anti-RCE já aplicada em `data` de dialplan
(`IsSafeDialplanData`) — nunca pode virar `${system(...)}`.

Tela "Telefonia > IVR": lista de menus com as opções de cada um, criação
com nome/contexto (auto-gerado do nome, editável) + linhas dinâmicas de
opção (dígito + select de ramal já cadastrado + rótulo), remoção com
confirmação de 2 cliques. Mostra o contexto/destino fixo (`ivr_entry`)
que uma Rota de Entrada precisa usar pra apontar pro menu.

Testado ponta a ponta criando um menu DE VERDADE pela tela/API (não só
lendo o XML manualmente escrito antes): softphone externo discou um DID
apontado pro menu recém-criado, atendeu, tocou o prompt, colheu o dígito
com DTMF real (`uuid_recv_dtmf`) e bridged com o ramal certo — confirma
que o compilador produz XML funcionalmente idêntico ao testado
manualmente. Falta pipeline de upload/TTS de áudio, sub-menus e destino
"fila" — ver docs/INBOUND_ROUTES.md.

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 17:24:07 -03:00
parent 83eb2c5aea
commit 36e85c1abf
16 changed files with 1000 additions and 6 deletions

29
TODO.md
View File

@@ -2117,6 +2117,35 @@ reportado pelo usuário: "a ligacao entre ramais nao esta funcionando")
contexto `default` já nasce `ACTIVE` com as 2 regras. Tenant de
teste suspenso ao final (sem endpoint de delete de tenant)
## PHASE 58 — Tela de autoria de IVR (pedido do usuário: "constrói a
tela de IVR no frontend")
- [x] `IvrMenu`/`IvrMenuOption` (novo, RLS real) — nome + contexto
(derivado do nome) + opções (dígito → ramal + rótulo). Nenhuma
tabela nova pro dialplan em si: `IvrMenusController` compila o
menu em `DialplanExtension`/`DialplanVersion` do contexto do menu
(mesmas 2 formas de `<extension>` já testadas com DTMF real na
PHASE 56 — entrada com `play_and_get_digits` + `transfer`, uma
extension por dígito) e já gera + ativa a versão nova, automático
- [x] `greeting` (prompt do menu) passa pela MESMA proteção anti-RCE já
aplicada em `data` de dialplan (`IsSafeDialplanData`) — é texto
livre do tenant, não pode virar `${system(...)}`
- [x] Tela "Telefonia > IVR": lista de menus com as opções de cada um,
criação com nome/contexto (auto-gerado, editável) + linhas
dinâmicas de opção (dígito + select de ramal existente + rótulo
opcional), remoção com confirmação de 2 cliques. Mostra o
contexto/destino fixo (`ivr_entry`) que uma Rota de Entrada precisa
usar pra apontar pro menu
- [x] Testado ponta a ponta criando um menu DE VERDADE pela API (não só
lendo o XML manualmente escrito antes): softphone externo discou
um DID apontado pro menu recém-criado, atendeu, tocou o prompt,
colheu o dígito com DTMF real (`uuid_recv_dtmf`) e bridged com o
ramal certo — confirma que o compilador produz XML funcionalmente
idêntico ao testado manualmente na PHASE 56
- [ ] Sem pipeline de upload/TTS de prompt de áudio (texto livre/tom
padrão só); sem sub-menu (IVR dentro de IVR) nem destino "fila";
"Rotas de Entrada" ainda não tem um seletor dedicado de "IVR" como
destino (usuário copia contexto/`ivr_entry` da tela de IVR)
---
## Riscos conhecidos