Novidades· 15 itens

Versão 1.73.0

Adicionado4

A auditoria da edição do negócio passa a guardar o valor anterior e o novo de valor, moeda, responsável e data prevista

Antes, a linha lead.updated do log de auditoria dizia só QUAIS campos mudaram. Agora ela também traz, em metadata.valores, o antes e o depois de cinco campos: valor, moeda, responsável (pessoa ou agente) e data prevista de fechamento — e só quando o campo mudou de fato. Isso permite responder "quem mudou o valor desta proposta, e de quanto para quanto?".

Título, descrição, etiquetas e campos personalizados continuam registrados só pelo nome: o log de auditoria não é reescrito pela anonimização da LGPD, então o texto livre do cliente não entra nele. Os cinco campos que entram são os mesmos que a anonimização preserva de propósito no próprio negócio. Nenhuma ação do operador.

Contribuição de @webtecnica (#2292, fecha #1755).

O produto se edita pela tela do catálogo

Quem administra corrigia um preço ou uma descrição do catálogo só reimportando a planilha — e a importação, de propósito, não grava descricao nem ativo. Agora a tela Produtos tem o botão Editar: ele abre o formulário já preenchido com o que está cadastrado e grava pelo PATCH /api/v1/products/:id que a API já aceitava, mandando para o servidor somente os campos que realmente mudaram (sem mudança, a tela avisa e não faz chamada nenhuma).

Produtos sincronizados de uma integração (origem que não seja manual nem planilha) abrem o formulário somente leitura, com o aviso de que a edição vale na origem: a próxima sincronização sobrescreveria o que fosse mudado aqui. As fotos continuam editáveis pelo botão de sempre, porque a integração não mexe nelas.

Contribuição de @webtecnica (#2288, refs #2135).

Cada intenção do roteador pode levar o negócio para o funil certo

Na tela do roteador, cada intenção ganha um campo opcional de funil de destino (e, se quiser, a etapa; sem etapa, vale a primeira etapa aberta do funil). Quando o roteador escolhe a intenção, o negócio do cliente vai para esse funil antes de o agente responder, e o agente passa a trabalhar no card do time certo. Até aqui o roteador só escolhia o agente, e o card ficava no funil de entrada.

Sem destino configurado, nada muda: quem atualiza continua com o roteamento de antes. Se o cliente troca de assunto para outra intenção que tem destino, o card vai para o novo funil e o negócio anterior é encerrado como transferência, com o motivo "Levado para outro funil", que não conta como perda nas métricas. A linha do tempo registra que foi o roteador de intenção quem levou o card. Se o contato já tem um negócio aberto no funil de destino, nenhum card novo é criado.

Contribuição de @webtecnica (#2290, fecha #2155).

Tempo na etapa (quem está nela agora) na tela de etapas, medido por stage_changed_at (#2032)

A tela de etapas passa a mostrar, ao lado da taxa histórica de ganho (#1753), quanto tempo os negócios estão NA etapa AGORA — medido pela ENTRADA do negócio na etapa (crm_leads.stage_changed_at, carimbada pelo trigger trg_stamp_stage_changed_at desde a migration 0071), com created_at de reserva para o lead sem carimbo. last_activity_at não entra na conta: é tempo sem resposta, e uma nota na conversa zerava o relógio de um negócio parado há semanas. É a mesma escolha do card do Kanban desde o #1908.

Quem entrega o número é lib/metrics/tempo-da-etapa.ts (puro, relógio injetado), publicado pela rota GET /api/v1/pipelines/{id}/stages/win-rates no bloco tempo_na_etapa, com medida e base escritos na resposta: a taxa ao lado é de OUTRA população (quem passou pela janela de dias, reconstruída das atividades stage_changed) e as duas não se somam. Por etapa saem quantidade, média e mediana de horas, além da amostra com_carimbo/sem_carimbo — número medido sobre reserva não é número medido sobre carimbo. Ganho e perda não recebem a frase: lá a coluna não segura trabalho em curso.

Sete testes do módulo cobrem os três casos da issue, inclusive o de last_activity_at mais recente ser ignorado; a rota e a tela têm teste próprio, e etapa vazia não ganha «0 h».

Contribuição de @webtecnica (#2225, fecha #2032).

Corrigido11

Instalação single-server atrás de um Traefik próprio ganha rota para as seis APIs do Supabase

Quem já tem um Traefik ocupando as portas 80/443 (em modo host, como na Hostinger; Traefik em bridge, como Coolify/Dokploy, ainda não é coberto) e instala o modo single-server com REVERSE_PROXY=traefik consegue concluir a instalação, desde que o entrypoint HTTPS desse Traefik se chame websecure ou que TRAEFIK_ENTRYPOINT seja exportado com o nome dele (a rota do Supabase lê o entrypoint só do ambiente; com outro nome, como https, ela é ignorada pelo Traefik): o Caddyfile do modo Caddy roteava /auth/v1, /rest/v1, /realtime/v1, /storage/v1, /functions/v1 e /graphql/v1 para o Envoy do Supabase, e o overlay do Traefik não tinha essa regra — o Traefik entregava esses caminhos ao app, que devolvia 404, e o instalador parava em "NEXT_PUBLIC_SUPABASE_ANON_KEY inválido". As seis prefixos agora viram etiquetas de rota no api-gw, ligadas só quando o instalador grava TRAEFIK_ENABLE=true no .env do Supabase, com prioridade acima da rota geral do app e abaixo do bloqueio do webhook do WAHA.

Para quem instala em Caddy — a maioria — nada muda: a rota nasce desligada (traefik.enable=false) e nenhuma variável nova do produto é lida. Para quem já instalou em Caddy não há efeito visível (o contêiner do Envoy é recriado uma vez na atualização), e para quem já instalou atrás de Traefik basta rodar de novo o install-single-server.sh com REVERSE_PROXY=traefik no ambiente, que regrava o .env do Supabase e recria o contêiner do Envoy com a rota.

Contribuição de @webtecnica (#2289, fecha #2099).

O assistente para de prometer retorno ao cliente sem que alguém fique responsável

Medido em produção em 16 de setembro. O assistente escreveu ao cliente: "Vou encaminhar as informações do site imobiliário para análise e te retorno com a proposta." O cliente saiu da conversa esperando um orçamento — e ninguém ficou devendo nada, porque o sistema não registrou nada: nenhum caso para alguém resolver, nenhum retorno marcado, nenhum aviso na Central.

Existe uma trava exatamente para isso — o assistente não pode prometer que uma pessoa da empresa vai agir sem que alguém fique responsável. A trava disparou uma vez, o assistente reformulou a frase, e a segunda formulação passou. O sistema dependia de reconhecer promessa por palavra: exigia "equipe", "setor" ou "responsável" colado ao verbo, e um objeto no meio da frase ("as informações") já a despistava. De sete frases equivalentes medidas, cinco passavam — inclusive uma que escrevia "equipe".

Agora a trava também pergunta ao classificador que já roda a cada envio se a mensagem promete que alguém da empresa volta a falar com o cliente. Os dois sinais valem juntos: o antigo, que é de graça e imediato, e o novo, que pega o que a palavra não pega. Nenhuma consulta a mais é feita — a pergunta entrou na mesma que já existia.

Quando quem promete voltar é o próprio assistente ("Combinado! Te retorno amanhã de manhã."), marcar o retorno na agenda de follow-up também resolve: se o assistente agendou o retorno naquele mesmo atendimento, a mensagem sai sem precisar de caso. Isso vale só para a promessa do assistente. Se a frase diz que alguém da empresa vai agir (a equipe, uma análise, o responsável, um setor), o caso continua obrigatório, com ou sem retorno agendado. Na dúvida, por exemplo quando a resposta do classificador vem incompleta, vale a regra mais rígida: o caso é exigido.

Um detalhe que vale saber na hora de decidir: a camada que faz a segunda pergunta segue a escolha que já existe na tela do assistente, no painel de segurança, e só o administrador da empresa a muda. Desligando, o reconhecimento volta a ser só por palavra — isto é, aquelas cinco frases voltam a passar.

Contribuição de @paulolimajr77 (#1873).

A catraca do espanhol passa a enxergar a chave montada em runtime e o t() sobre variável

O gate que cobra a tradução em espanhol só reconhecia texto escrito no código (t("literal")) e tabela resolvível (t(TABELA[k])): quando a chave é montada em runtime (`t(Meta de ${x} batida)) ou vem de uma variável que ninguém consegue ler (t(rotulo)), o gate ficava verde mesmo sem nada em espanhol — e, como traduzir()` devolve a própria chave quando ela falta, a frase saía em português para quem escolheu espanhol. A catraca agora cobre as duas formas, apontando arquivo:linha de cada uma.

34 sítios já existentes foram congelados em uma lista com a razão escrita de cada um (a lista só encolhe; nenhuma tradução foi mexida) — consertar um por um é decisão de produto, e está declarado assim. Métrica e gates na PR.

Contribuição de @webtecnica (#2298, continuação da #603).

Criar negócio direto numa etapa exigente também passa pela régua de campos obrigatórios

A criação de negócio (POST /api/v1/leads, o diálogo do Kanban com etapa escolhida, a ferramenta MCP crm_create_lead, a importação de planilha, a automação create_or_move_lead — ao criar e ao transferir entre funis — e a prospecção) passou a perguntar a mesma régua de campos obrigatórios que o arrasto, o lote, o encerramento e o clone já perguntavam. Até aqui a criação era o único caminho que nascia na etapa exigente com o campo em branco, e a cobrança só aparecia na escrita seguinte, com o negócio já lá. Agora a recusa é visível onde alguém configurou o caminho: a importação mostra a frase, e a automação fica com status de falha e a mesma frase, como já acontecia ao mover.

Os formulários de captação (webhook de entrada) seguem entrando como antes: o lead que chega de fora entra na etapa padrão da fonte mesmo que ela exija um campo que o formulário não mandou.

Nada muda para quem não configurou obrigatorio_em num campo do funil: a regra continua opt-in, e uma leitura indisponível das configurações deixa a criação acontecer como antes. Na transferência entre funis a recusa acontece antes de clonar e de fechar a origem, então o negócio continua aberto no funil de onde saiu.

Contribuição de @webtecnica (#2295, fecha #1710).

Agente criado pela API sem version, ou duplicado de um agente antigo sem versão, nasce com rascunho v1 editável na tela

Uma integração que criava agente por POST /api/v1/ai/agents sem o campo version, e a duplicação de um agente antigo que nunca teve versão, gravavam um agente sem nenhuma versão, que nem o atendimento do CRM nem o agent-engine enxergam: ele existia na lista e nunca respondia.

Agora os dois casos nascem como agente do formato atual (kind: "mcp_agent") com uma versão 1 em rascunho, montada do prompt e do modelo enviados. O agente aparece como parado e só atende depois que alguém publica a versão na tela, como qualquer agente novo; por isso ele também deixa de gerar o aviso de "agente sem versão".

A resposta da API para o formato sem version continua sendo a linha do agente, e o modelo omitido continua sendo anthropic/claude-sonnet-5. Muda o valor de kind, que passa de rag_bot para mcp_agent. Os agentes antigos que já existem não são alterados.

Contribuição de @webtecnica (#2296, refs #1357).

O diálogo de excluir contato avisa do compromisso na Agenda antes do clique

Ao abrir "Excluir contato?", a tela agora consulta o que vai barrar a exclusão e, se o contato tem compromisso na Agenda, diz isso já no diálogo, com um link para abrir a Agenda. Antes o aviso só aparecia depois de tentar excluir. Sem compromisso, o diálogo continua igual e a exclusão segue normalmente. A tela usa a mesma contagem que a exclusão.

Contribuição de @webtecnica (#2293, fecha #1925, continuação do #1949).

O item "demanda aberta sem próximo passo" do Radar vira link, e o caso cancelado ou resolvido marca um próximo passo na demanda que ele abriu

No Radar de risco, o item "demanda aberta sem próximo passo" deixava de ser só texto: agora é um link para a conversa vigente da demanda (ou, sem conversa, para a ficha do contato), como já eram as outras seções — dava para chegar à demanda pelo alerta.

E um caso cancelado ou resolvido passa a marcar um próximo passo na demanda que ele abriu por handoff ("Revisar o caso encerrado e registrar o desfecho da demanda"): antes ela ficava "em atendimento" e sem próximo passo para sempre, presa na seção de risco do Radar. O sistema não decide o desfecho — só tira a demanda de "sem próximo passo" e a deixa visível em "Demandas abertas" do painel da conversa, com "Encerrar demanda" ao lado, até alguém registrar o desfecho.

Demanda antes presa em em_atendimento num caso rescindido não é retroativamente corrigida por esta versão. Sem ação necessária.

Contribuição de @webtecnica (#2056, fecha #2035).

As recusas da régua de campos obrigatórios agora chegam a quem precisa ver

Com a régua na criação de negócio (#2295), quatro caminhos recusavam sem que a pessoa certa visse — ou deixavam um resto. Os quatro passam a se comportar assim:

1. Roteador de intenção (#2290). Quando a etapa de destino exige um campo, transfereParaOFunil lança e a recusa morria num runLog.warn que ninguém lê. Agora a recusa é capturada em aplicaDestinoDaIntencao — onde o negócio de origem é conhecido — e abre um aviso na Central (kind: other, apontando para o negócio), sem empilhar o mesmo aviso aberto. O negócio de origem continua aberto, como antes: nada se perde, e agora alguém fica sabendo. 2. Tool MCP crm_create_lead. Ganhou custom_fields no inputSchema: o agente que sabe o valor consegue criar um negócio numa etapa exigente, pelo mesmo createLeadHandler e a mesma régua de todo mundo. Sem a chave, z.object descartava o argumento antes de chegar a quem pergunta a exigência. 3. Import de planilha. O contato da linha 1 é criado antes do createLeadHandler da mesma linha, e um 422 da régua (ou um 404 de etapa) derruba a importação inteira: agora os contatos que ESTA requisição criou e que ficaram sem negócio são devolvidos ao banco antes da resposta de erro. Os que já estavam no cadastro e os que já viraram card nunca entram no desfazimento. 4. Captação. O motivo da recusa na tela "Leads recebidos" deixa de ser erro_ao_criar_lead para tudo: a régua vira recusa_da_regra com rótulo próprio, funil/etapa seguem com o rótulo original, e o resto vira erro_inesperado — o rótulo não promete mais "funil e etapa" numa recusa que não é deles.

A régua não afrouxa em nenhum dos quatro: a isenção da rota de captação (exigirCamposDaEtapa: false, decisão do #2295) continua intacta, e nenhum caminho passa a criar o que a régua recusava.

Contribuição de @webtecnica (#2302, fecha #2297).

O banco recusa nome de sessão WAHA acima do teto de 54, em vez de só o teste conferir

O teto de 54 caracteres que o WAHA impõe no nome da sessão era conferido em três lugares de código — o teste de banco, o teste unitário e a guarda antes de chamar o WhatsApp — e em nenhum deles dentro da escrita da linha. Um INSERT direto (um script, o PostgREST, ou uma migration que copiasse o corpo antigo) gravava um nome acima do teto sem que nada recusasse, e o erro só aparecia quando o operador tentava conectar o número, com o card preso em "Parado".

Agora a própria tabela channel_sessions recusa a linha com o mesmo código de erro das recusas de domínio da reserva: um nome novo acima de 54 não entra, e renomear uma linha para cima do teto também não. Instalação existente não é afetada — uma linha antiga com nome acima do teto continua recebendo mudanças de status, metadata e lease normalmente, porque a recusa olha apenas o nome que está sendo escrito; e o caminho de conserto (renomear para o formato curto de 45) continua cabendo na mesma linha.

De quebra, o teste de tela de pré-go-live deixa de montar prego_ à mão e passa a usar o mesmo gerador que o banco usa, com o formato real conferido.

Contribuição de @webtecnica (#2301, fecha #686).

Ligação telefônica de contato bloqueado é recusada e aparece como "Cancelada"

Quem mandou "PARAR" não recebe mais ligação da IA: no telefone (SIP), a chamada de entrada de contato bloqueado é desligada na hora, sem virar negócio, sem tocar aviso para a equipe e sem carimbar a linha do tempo — recusar é não-interação. A linha fica gravada no histórico e na tela de chamadas como "Cancelada", o status existente mais próximo de recusa (um rótulo próprio pode vir em PR seguinte), para a equipe saber que a ligação existiu sem precisar agir. No WhatsApp a ligação não é recusada: a perdida de bloqueado só deixa de gerar aviso e linha do tempo, e a atendida por alguém da equipe continua na linha do tempo. A saída (discar para bloqueado) já era recusada e continua igual; falha ao ler o bloqueio deixa a chamada passar, nunca derruba ligação legítima por instabilidade.

Contribuição de @paulolimajr77 (#2299).

O WAHA passa a assinar as entregas de webhook e a exigência de assinatura passa a funcionar

O compose entregava o segredo ao contêiner do WAHA numa variável que não existe na documentação dele (WHATSAPP_HOOK_HMAC em vez de WHATSAPP_HOOK_HMAC_KEY), então o WAHA ignorava e nunca assinava nada — e ligar "Exigir assinatura nas entregas do canal" cortava a entrada de mensagens com 401 signature_required.

Agora o nome é o exato da documentação (nos três composes e no runbook). Na atualização o contêiner do WAHA é recriado e passa a mandar X-Webhook-Hmac em toda entrega; quem tem a exigência ligada volta a receber mensagens, agora com valid_signature = true no log. Nenhuma ação do operador: o segredo é o mesmo, só o nome da variável mudou.

Contribuição de @paulolimajr77 (#2268).

← Voltar para todas as versõesVer no GitHub