GET /platform/freeswitch/channels|profiles|nodes fazem introspecção ESL real
(show channels/show calls/sofia status/show registrations/status/show gateways),
reaproveitando os métodos do FreeSwitchTelephonyProvider já verificados manualmente
contra o FreeSWITCH real. Mesmo padrão de conexão avulsa do PlatformHealthController
(connect → comando → disconnect).
Nunca deixa a indisponibilidade do ESL virar 500/503 — devolve { ok: false, error }
com 200, mesma filosofia do health check. Necessário: nesta VM apps/api roda fora do
Docker e a porta 8021 é deliberadamente não publicada no host, então as 3 telas
sempre mostram essa explicação aqui (mesmo texto já usado em Infraestrutura >
Saúde), mesmo com o endpoint 100% funcional — confirmado indiretamente pelo
b2bcall-fs-events, que fala ESL de dentro da rede Docker e está com heartbeat ativo.
"Nodes" mostra explicitamente 1 node (container único, sem clustering) em vez de
fingir uma lista.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
GET /billing/subscriptions e POST /billing/plan-versions já existiam desde a fase
Billing (PHASE 22) sem tela nenhuma — Assinaturas agora deixa escolher um tenant, ver
o histórico de versões de preço assinadas, e criar uma nova assinatura reaproveitando
uma versão existente ou versionando um preço novo na mesma ação (preço nunca é
sobrescrito, sempre uma linha nova).
GET /platform/quotas (novo) agrega uso vs. limite do plano em todos os tenants de
uma vez (ramais/agentes/troncos/filas/campanhas/chamadas-mês/armazenamento), com a
tela destacando quem está em 80%+ (amarelo) ou 100%+ (vermelho) do limite.
Achado real ao revisar o dashboard "Visão Geral" antes de escrever a agregação
cross-tenant de Quotas: aiUsageThisMonth/recordingStorageBytes sempre devolviam
zero/vazio, porque a query rodava direto no Prisma sem nenhum app.current_tenant_id
setado — ai_usage_records/recordings têm FORCE RLS, então a policy nega a leitura
silenciosamente (0 linhas, sem erro), não importa quanto uso real existisse. Mesma
classe de bug já corrigida 2x antes nesta sessão; corrigido com o mesmo padrão (loop
withTenantContext por tenant). Confirmado inserindo um AIUsageRecord de teste no
Postgres, vendo o número aparecer, e removendo o teste depois.
Testado ponta a ponta: fluxo completo de criar assinatura via UI pro tenant Beta
Corp (nova versão de preço + assinatura, confirmado na tela e no banco), Quotas
mostrando os números reais dos dois tenants de teste.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
GET /reports/consumo agrega os 2 ledgers imutáveis de uso (UsageEvent +
AIUsageRecord — os mesmos que o RatingEngine usa pra faturar) por meter/tipo no mês
corrente: chamadas, minutos, dias ativos, armazenamento de gravação, tokens de IA.
Nunca calcula valor em dinheiro (isso é billing/RatingEngine, platform-only) —
decisão deliberada pra não duplicar essa lógica fora dele. Devolve também os limites
do plano (maxMonthlyCalls/maxRecordingStorageGb) pra comparação.
Tela /app/relatorios/consumo reaproveita o InstrumentTile do dashboard, zeros
honestos em vez de esconder seção. Testado ponta a ponta contra o tenant Acme real
(2 dias-tronco já ledgerados aparecem certos, resto zerado por não ter chamada
rodada ainda).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Fecha os últimos gaps do módulo Administração (agente.md secao 169): tela de
Configurações self-service do próprio tenant (GET/PATCH /tenant-settings, nunca
aceita tenantId arbitrário — só user.tenantId das claims), e DELETE /users/:id pra
remover alguém do tenant, com duas proteções que não existiam antes (não deixa
remover a si mesmo, não deixa remover/rebaixar o último Tenant Admin).
Corrige um bug real achado testando a remoção: o delete de TenantMembership (FORCE
RLS) rodava dentro de um prisma.$transaction([...]) em forma de array, que nunca
seta app.current_tenant_id — Prisma devolvia P2025 "not found" com a linha
existindo (500 pro cliente). Mesma classe de bug já corrigida antes em
TenantsController.create; corrigido com $transaction(async (tx) => ...) + set_config
explícito.
Adiciona Discador > Callbacks (GET/PATCH /leads/callbacks, tenant-wide): reagendar,
tentar de novo sem esperar, ou desistir de um lead que pediu retorno em outro
horário. Remove "Importações" do menu — decisão já registrada na PHASE 39 de não
duplicar uma tela pro que já existe (CSV em lote no wizard/detalhe da campanha).
Testado ponta a ponta via curl e Puppeteer contra o tenant Acme real: tenant-settings
GET/PATCH, proteção de último-admin nos dois endpoints que a usam, convite+remoção
de um admin temporário, e o fluxo completo de callback (lead forçado pra CALLBACK
via SQL, reagendar rejeitado pro passado/aceito pro futuro, requeue confirmado).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
/app/discador/leads — seletor de campanha, busca por telefone/nome,
adicionar lead individual, remover com confirmação de 2 cliques. O
backend (GET/POST/DELETE /campaigns/:campaignId/leads) já existia desde a
PHASE 15; a prévia de leads no detalhe da campanha já apontava pra esta
tela ("a lista completa vive em Discador > Leads") desde a PHASE 26, só
faltava construir.
LEAD_STATUS_LABELS novo (16 valores) — badge própria, não reusa
StatusBadge (mesma cautela de gênero de AgentStateBadge/TenantStatusBadge:
READY/FAILED/COMPLETED colidiriam com rótulos de campanha).
Testado ponta a ponta: campanha de teste criada, 3 leads adicionados (2
via API, 1 via UI), busca filtrando corretamente, remoção confirmada via
API. Smoke test nas 19 telas do tenant + platform, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
POST /users (convidar) fecha uma lacuna real: até aqui só dava pra
adicionar alguém a um tenant criando o tenant inteiro ou via script. Se o
e-mail já existe na plataforma, só adiciona membership+role (sem tocar na
senha); se não existe, cria a conta com senha gerada e revelada uma única
vez. PATCH /users/:id/role troca o papel (substitui, 1 papel por tenant).
GET /roles novo (tenant-facing) — versão de /platform/roles filtrada só
pras roles de escopo TENANT, sem expor que platform_super_admin existe.
Frontend: /app/administracao/usuarios (convidar com papel, trocar papel
de qualquer membro exceto o próprio usuário logado) e /perfis (o que cada
papel pode fazer, só leitura).
Testado ponta a ponta contra a API real: convidada conta nova com senha
revelada, convidado usuário com papel Agente, trocado pra Supervisor via
PATCH, confirmado persistido. Perfis mostra as 3 roles de tenant certas.
Smoke test nas 19 telas do tenant + 10 telas platform, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
GET /platform/roles novo — lista as 4 roles do sistema com as permissions
de cada uma, mais o catálogo completo de permissions. roles/permissions
não têm RLS (catálogo global do seed); só leitura, RBAC é system-defined,
sem UI de criar role customizada ainda.
Frontend: /platform/sistema/permissoes, um card por role + tabela do
catálogo completo. Testado ponta a ponta: as 4 roles reais (platform_super_admin
33 permissions, tenant_admin 30, supervisor 17, agent 2) corretas. Smoke
test nas 19 telas do tenant + 9 telas platform, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
GET /billing/periods e /billing/statements só serviam o próprio tenant do
JWT — sem uso pra um platform admin escolhendo um tenant arbitrário.
Adicionado GET .../by-tenant/:tenantId nos dois (mesmo padrão já usado em
Subscriptions), e GET /billing/statements/:id ganhou um ?tenantId=
opcional só aceito de quem tem role de plataforma.
Frontend: /platform/billing/fechamentos (fecha/reabre período por
tenant, seletor via querystring pra não duplicar rota) e /relatorios
(statements por tenant, detalhe com itens por categoria).
Bug real achado testando o fluxo: fechar um período de 01/08 a 31/08
mostrava "31 de jul." a "30 de ago." — meia-noite UTC de uma data-only
vira o dia anterior no timezone local do servidor. Corrigido com
formatDateUTC novo, usado só em fronteiras de calendário (não em
timestamps de verdade, que continuam com formatDate local).
Testado ponta a ponta contra a API real: período fechado, statement
gerado (R$ 0,00 honesto — Acme sem assinatura/price book ainda), detalhe
correto. Smoke test nas 19 telas do tenant + 8 telas platform, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Três endpoints novos, todos platform-only: GET /platform/users (cross-
tenant, users não tem RLS) + PATCH .../status (desabilitar tem efeito
real — login() já checava status ACTIVE desde a PHASE 04); GET
/platform/audit-log (últimos 200 eventos, audit_logs também sem RLS,
linha imutável); GET /platform/health (Postgres/Redis + FreeSWITCH via
conexão ESL avulsa, sem manter estado).
Achado de arquitetura documentado explicitamente na própria tela: o check
de FreeSWITCH sempre falha neste ambiente porque apps/api roda no host e
a porta 8021 é deliberadamente não publicada (decisão da PHASE 01/05) —
não é um bug, é a rede isolada do jeito certo.
Frontend: /platform/sistema/usuarios, /auditoria, /platform/
infraestrutura/saude. Testado ponta a ponta contra dados reais (3
usuários da plataforma, audit log com eventos reais desta sessão,
inclusive uma referência órfã tratada corretamente). Smoke test nas 19
telas anteriores, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Não existia NENHUM endpoint pra criar/listar/editar tenant nem plano até
aqui — só via script/seed ad hoc. Dois controllers novos: TenantsController
(/tenants) e PlansController (/plans), platform-only.
POST /tenants cria o tenant E o primeiro usuário (Tenant Admin) numa
transação só — sem esse usuário o tenant fica inacessível. Senha gerada e
devolvida em texto puro só na resposta de criação (revela uma vez, mesmo
padrão de Ramais/SIP).
Bug real achado testando o próprio endpoint: GET /tenants calculava
memberCount sem contexto de RLS — tenant_memberships tem FORCE RLS, então
nem platform admin enxerga linha nenhuma sem app.current_tenant_id
setado, o campo sempre voltava 0. Corrigido abrindo o contexto de cada
tenant um de cada vez.
Frontend: /platform/clientes/tenants (lista+busca), /tenants/new (cria
tenant+admin, revela senha), /tenants/:id (troca status/plano, preview
dos limites ao vivo), /platform/clientes/planos (CRUD completo dos
limites). Testado ponta a ponta: plano novo -> tenant novo com admin real
-> troca de plano persistida, confirmada via API. Smoke test nas 19 telas
anteriores, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
O RealtimeGateway (backend, desde a PHASE 13) autentica a conexão
socket.io via auth.token no handshake — um JWT bruto que um
EventSource/WebSocket do browser não tem como mandar sem passar por JS
legível no client, quebrando o princípio seguido em todo o resto do
frontend (token só existe no cookie httpOnly). Resolvido com um proxy:
apps/frontend/src/app/api/monitoring/stream/route.ts roda no servidor,
conecta no socket.io real com o access token do lado do servidor, e
reencaminha cada evento pro browser como Server-Sent Events — o
EventSource do client só precisa do cookie de sessão, nunca do token.
/app/monitoramento: badge de conexão, contadores de sessão (chamadas
criadas/atendidas/encerradas), filas ao vivo (QUEUE_MEMBER_COUNT), estado
de agentes ao vivo (AGENT_STATE_CHANGED, semeado do GET /agents inicial),
feed dos últimos 50 eventos. Menu "Monitoramento" vira 1 link direto em
vez de 5 sub-itens placeholder — o painel novo já cobre tudo numa página
só.
Testado ponta a ponta contra o pipeline real (não simulado): cliente
socket.io cru confirmou a API entregando o evento, curl -N confirmou o
proxy reencaminhando, e com a página aberta de verdade num browser
(Puppeteer) disparei POST /agents/me/login e /logout por fora — o badge
do agente mudou ao vivo e os eventos apareceram no feed sem recarregar a
página. Smoke test de regressão nas 19 telas anteriores, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Achado numa revisão de segurança sobre o trabalho da PHASE 29: um FK do
Postgres só checa que a linha referenciada existe, não que ela é visível
sob a RLS da sessão atual — então um userId/extensionId de outro tenant
seria aceito silenciosamente no create do Agent (violação do princípio já
seguido em todo o resto do código: nunca confiar em id vindo do client
sem checar contra o tenant do JWT). O frontend já só oferece opções do
próprio tenant, mas isso é conveniência de UI, não autorização.
Corrigido com uma checagem explícita de TenantMembership/Extension antes
do create (400 se não pertencer). Reverificado ponta a ponta: criação
legítima continua funcionando igual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Três telas novas fecham quase todo o menu IA: Scorecards (critérios
ponderados, array dinâmico como no Dialplan), Prompts (templates com
versionamento — editar = criar versão + ativar numa ação só, já que não
existe endpoint pra listar histórico), e Configurações (providers BYOK +
modelos). Chave de API nunca é reexibida em texto puro, nem uma vez — só
preview mascarado, mesmo na resposta de criação.
Novo componente Textarea em components/ui/input.tsx (mesmo estilo de
Input/Select) — primeira tela que precisa de texto longo.
Testado ponta a ponta contra a API real: scorecard, template de prompt
(v1 -> editar -> v2 ativada), provider BYOK + modelo referenciando ele.
Smoke test de regressão nas 16 telas anteriores do tenant + platform,
todas 200. Typecheck limpo no monorepo inteiro.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Relatórios > Chamadas: lista das últimas 500 chamadas dos últimos 30 dias,
nomes de fila/agente/campanha/disposição resolvidos client-side, busca por
telefone. Só o filtro de telefone nesta primeira versão.
Gravações: lista + player + download. Achado de arquitetura resolvido
antes de codar: <audio src>/<a download> não mandam Authorization Bearer
(só cookie), e a API nunca expõe o storage por URL direta — criado um
proxy autenticado (Route Handler /api/recordings/[id]/audio) que lê o
cookie de sessão, chama a API real com o access token do lado do servidor,
e reencaminha o stream com Content-Disposition: inline (a API manda
attachment). Mesmo princípio de apiFetch: token nunca chega em JS legível.
Testado ponta a ponta contra a API real (estados vazios honestos, proxy
confirmado 401 sem sessão). Smoke test de regressão nas 15 telas
anteriores do tenant + platform, todas 200.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Fecha a lacuna de backend que travava Agentes: POST /agents exigia um
userId de um usuário já existente do tenant, mas não havia nenhum endpoint
pra listar usuários. Novo GET /users (gated por users.manage, não
agents.manage — listar identidade de login é administração de usuário)
via TenantMembership, mesmo padrão de RLS já usado em listUserTenants.
Duas telas novas: Call Center > Agentes (criar/listar/remover, seletor de
usuário só mostra quem ainda não é agente) e Telefonia > Dialplan (editor
estruturado do contexto default — regras com condição/ações dinâmicas,
painel de Versões com gerar/ativar, reativar versão antiga = rollback).
Testado ponta a ponta contra a API real do tenant Acme, incluindo o ciclo
completo de dialplan (criar regra -> gerar v1 -> ativar -> badge "Ativa").
Smoke test de regressão nas 13 telas anteriores do tenant + platform.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Quatro páginas novas em /app/relatorios/*, uma rota por relatório (não abas de
uma página só, pra não quebrar o realce de "ativo" da sidebar quando vários
itens de menu apontam pro mesmo relatório) — reusa os endpoints de reports
que já existiam desde as fases de CDR/AI, nenhum backend novo.
Security quality gate (agente.md secao 224): revisão dedicada sobre todo o
diff desde origin/main (billing + frontend inteiro, 7 commits) não achou
nenhuma vulnerabilidade de alta confiança. Suítes de teste de isolamento
multi-tenant, autenticação/RBAC e rating engine reexecutadas do zero e
verdes. TODO.md documenta o escopo real ainda faltando (Agentes bloqueado
por falta de endpoint de listagem de usuários, Dialplan, Monitoramento em
tempo real, Gravações, IA CRUD, Relatórios > Chamadas/Consumo) em vez de
alegar a aplicação "finalizada".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Cinco telas novas no app do tenant, todas contra endpoints de backend que já
existiam (Queues/PauseReasons/Dispositions/Trunks/Suppression) — mesmo padrão
de listagem+form inline+remoção com confirmação de 2 cliques usado em Ramais.
StatusBadge ganhou o mapa de TrunkStatus. Corrigido bug real: PauseReasonsController.list()
não filtrava enabled:true, então um motivo removido nunca sumia da lista.
Testado ponta a ponta contra a API real do tenant Acme.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
Tela completa de campanhas do discador preditivo: listagem com busca/
filtro/status, wizard de criacao em 7 passos exatamente como a
especificacao nomeia (Geral -> Telefonia -> Discagem -> Horarios ->
Gravacao e IA -> Leads -> Revisao, agente.md secao 170) — nada e criado
ate confirmar na revisao, upload de CSV de leads na mesma acao de
criacao — e detalhe com acoes de ciclo de vida (iniciar/pausar/drenar/
parar, cada botao so aparece quando a transicao e valida pro status
atual), pacing ao vivo, previa de leads com import adicional, e remover.
Corrige de quebra um erro real de build: Server Actions exportadas como
arrow function que so repassam argumentos pra outra funcao quebram
("Server Actions must be async functions") — precisam ser declaradas
como async function de verdade.
Testado ponta a ponta com fila+tronco reais: os 7 passos preenchidos e
revisados, campanha criada com CSV de 3 leads importado, ciclo de vida
completo start->pause->drain->stop->delete via API, e confirmado que uma
campanha iniciada de verdade e pega pelo PredictiveDialerEngine real
rodando em Docker (stats deixam de ser null depois de alguns segundos).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
Tela /app/telefonia/ramais completa: listagem com busca/ordenacao,
criacao com reveal de senha SIP (gerada pela API, mostrada uma unica vez),
detalhe com redefinir senha (novo POST /extensions/:id/reset-password —
unica forma de "editar" a senha, nunca reexpoe a existente) e remover
(soft delete), confirmacao inline de 2 cliques em vez de modal. Cada tela
deixa explicito de qual tenant os ramais sao, alem do isolamento por RLS
que ja existia.
Corrige de quebra um bug real que afeta qualquer acao futura sem payload:
apiFetch sempre mandava Content-Type: application/json mesmo em requests
sem body, e o parser do Fastify rejeita body vazio com esse header.
Testado ponta a ponta com um tenant real (Acme Call Center): criar ramal,
revelar senha, redefinir (senha nova confirmada diferente da original),
remover, lista voltando vazia. Dark mode e mobile conferidos.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
Corrige um gap real: o login sempre mandava pra /platform mesmo pra
usuarios sem role de plataforma, sem nunca chamar /auth/tenants ou
select-tenant. Agora POST /api/post-login decide o destino server-side
(plataforma / tenant unico / seletor com >1 tenant) antes de redirecionar,
trocando o access token quando necessario sem nunca expor token ao client.
Adiciona GET /reports/dashboard (secao 162) e a tela /app correspondente
— chamadas/agentes/TME/TMA/rates ao vivo, "consumo do plano" e "valor
estimado" seguindo a mesma disciplina de honestidade (null > numero
inventado) do dashboard de plataforma.
Shell (sidebar/topbar/drawer mobile) extraido pra components/shell,
compartilhado entre os menus Platform e Tenant (secao 168-169) via
wrappers client-only por area — corrige de quebra um erro real de
serializacao RSC (passar NavSection[] com icones como prop de Server
Component pra Client Component quebra: "Functions cannot be passed
directly to Client Components").
Testado ponta a ponta com um tenant semeado (Acme Call Center): login →
/app direto, platform admin barrado de /app e vice-versa, dashboard com
numeros reais (zeros honestos), drawer mobile, dark mode.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
Primeiro commit do frontend Next.js (agente.md secao 161-176): login
split-brand, dashboard "Visão Geral da Plataforma" com instrumentos ao
vivo, e a tela Billing > Tarifas (price books + rate decks) completa —
listagem com busca/ordenacao, criacao com itens/entradas dinamicos via
Server Actions, detalhe — ponta a ponta contra a API real de billing
(fase 22).
Corrige de quebra 2 bugs reais achados construindo Tarifas: a topbar
tinha o titulo fixo "Visao Geral" em toda pagina, e a sidebar fixa de
256px nao tinha nenhuma versao mobile (conteudo espremido em ~130px) —
agora vira drawer via Radix Dialog abaixo de lg, com titulo/descricao da
topbar resolvidos dinamicamente por rota.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY