Commit Graph
8729 Commits
Author SHA1 Message Date
melgarafaelandClaude Opus 5.5 31abf73480 fix(campanhas): busca de campanhas que falha deixa de se passar por rodada vazia
A rodada descartava o `error` da busca das campanhas em andamento e
respondia "nada_a_fazer" — uma falha do banco era indistinguível de uma
rodada sem trabalho, e nada ficava registrado. Agora o erro vai para o
logger (como promoverAgendadas já fazia) e a rodada devolve
`detalhe: "busca_falhou"`. Trocar a lista de ids de orgs paradas por um
filtro no banco fica para uma issue própria.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:59:08 -03:00
melgarafaelandClaude Opus 5.5 099fac569c fix(followup): o turno que rodava na hora da suspensão não mata mais o follow-up na volta
A suspensão descarta o turno de envio `pending` e grava `turn_discarded`,
que faz o motor enfileirar um turno novo na reativação. O turno que JÁ
rodava naquele segundo seguia, tinha o envio barrado (OrgNaoOperanteError)
e era cancelado sem o evento: na reativação os rechecks esgotavam o
dead-man e a inscrição morria com `action_turn_never_completed` — um
follow-up morto na Central com motivo falso, e o lead sem a mensagem.

Uma regra só para "o turno saiu sem enviar": `fn_followup_turno_descartado
(org, job)` (seção C0a da 0501, invoker, só service_role). A C0 passa a
chamá-la em vez de repetir o INSERT, e os dois donos de turno em voo a
chamam antes de devolver o job: o agent-worker (createFollowupTurnHandler,
só purpose send_message) e o envio inline (enviarTextoFixoPendente). Erro
que não é suspensão não grava nada — o dead-man segue valendo.

Prova: tests/invariants/followup-org-suspensa.test.ts reproduz a sequência
(turno running → suspende → cancela → reativa) com controle que mostra a
morte sem o evento; tests/unit/turno-rodando-na-suspensao.test.ts e
lib/followup/enviar-texto-fixo.test.ts cobrem as duas chamadas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:59:03 -03:00
melgarafaelandClaude Opus 5.5 f9df965345 fix(suspensao): a data de anonimização da empresa entra na trava do banco
O gatilho que impede a sessão (tela/PostgREST) de mexer no estado da
organização cobria status, suspensão e autoria, mas não `redacted_at`: um
platform admin só-leitura ainda gravava uma data de anonimização falsa. A
coluna entra na comparação da migration 0501 e, byte a byte, no apêndice do
baseline; o único escritor legítimo (lgpd-redact-worker, service_role)
segue passando. Caso novo em org-suspensa.test.ts para os dois scopes de
platform admin, e o controle do service_role passa a gravar a data como o
worker grava.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:58:46 -03:00
melgarafaelandClaude Opus 5.5 7d19a4bac0 test(suspensao): a reativação prova que o job remanescente virou falha, não só que saiu da fila
O caso de reativação conferia apenas que não sobrou job 'pending' — o que
também passaria se o job tivesse sido apagado ou marcado 'done' por engano.
O job agora nasce com id fixo (JOB_REMANESCENTE, no padrão de JOB_A/JOB_B)
e o teste afirma 'failed|org_nao_operante' por esse id, como o caso irmão
da suspensão já fazia.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:57:45 -03:00
melgarafaelandClaude Opus 5.5 0938e8d8f5 fix(lgpd): o link do e-mail de prazo decide no clique, não no envio
O e-mail enviado com a empresa ativa e clicado depois da suspensão
ainda caía na lista do hub, sem o pedido: o link escolhia a porta
(/app ou o hub) no momento do envio, e a empresa pode mudar de estado
antes do clique.

Agora o link é sempre /lgpd/pedido/<id>, uma porta neutra fora de
/app que entrega o pedido ao hub; o hub já decide com as duas réguas
do layout de /app: empresa parada vê o pedido ali, empresa que opera
é devolvida a /app/lgpd/requests/<id>. A porta não é pública, então
quem abre o e-mail sem sessão passa pelo login com next= e volta ao
pedido (o hub, público, mandava ao login sem next).

Sai o que o conserto anterior precisava para escolher no envio: o
status da org no cron e no alarme, lib/lgpd/caminho-do-pedido.ts e a
entrada dele em NAO_E_FILTRO da cerca de crons, que volta ao que era.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 15:09:35 -03:00
melgarafaelandClaude Opus 5.5 e05b42dc93 feat(mcp): empresa suspensa ainda consulta os pedidos de LGPD pelo MCP
Decisao do dono (30/09): a ferramenta de privacidade do MCP fica liberada
para empresa suspensa, porque LGPD nunca e bloqueada. Antes o token de
empresa parada morria no /api/mcp inteiro com 403.

O /api/mcp agora valida o token com permiteOrgSuspensa e o recebe marcado
(orgSuspensa). O servidor recusa com org_suspended toda ferramenta que nao
declara permiteOrgSuspensa - hoje so crm_list_privacy_requests, que e
leitura. A recusa fica depois do teto (a integracao em laco nao escreve
auditoria sem freio) e dentro do try que audita. O token de API nas rotas
REST (auth-dual, /api/v1/contacts) segue 403: so quem passa a opcao abre
a porta.

A cerca org-suspensa-so-nas-rotas-permitidas passa a admitir a chave nos
dois arquivos do MCP e exige que o /api/mcp e a ferramenta de privacidade
a passem; o fragmento diz o que continua respondendo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 14:36:01 -03:00
melgarafaelandClaude Opus 5.5 efd1356f90 fix(lgpd): o e-mail de prazo abre o pedido mesmo com a empresa suspensa
O link do alarme de SLA apontava para /app/lgpd/requests/<id>; com a
empresa suspensa o layout de /app desviava para o hub sem dizer qual
pedido era, e o DPO caia na lista com o prazo legal correndo.

Agora quem monta o link escolhe a porta (lib/lgpd/caminho-do-pedido.ts):
empresa parada recebe /account-suspended?pedido=<id>. E o hub, quando a
empresa voltou a operar antes do clique, devolve ao pedido em vez de a
/app. As duas pontas usam a mesma funcao, entao o link vale nos dois
estados do clique; status ilegivel cai no hub, que redireciona certo.

A cerca de crons ganha caminho-do-pedido.ts em NAO_E_FILTRO: escolher a
porta de um link nao e filtro, e o alarme segue saindo para a empresa
parada, de proposito.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 14:31:11 -03:00
melgarafaelandClaude Opus 5.5 858541085f fix(suspensao): a tela de conta suspensa diz qual empresa parou
Quem participa de mais de uma empresa via só "Conta suspensa" e ficava sem
saber qual — a própria tela oferece trocar para as outras. O nome da
empresa ativa aparece logo abaixo do título (é dado, então não passa pelo
dicionário).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:58:26 -03:00
melgarafaelandClaude Opus 5.5 cc62928d09 fix(admin): admin só-leitura que tenta salvar lê o motivo, não a tela de erro
As server actions do painel (marca, e-mail, Google, Meta, destinos internos,
módulos, comportamento, cadastro, pedidos de cadastro e configuração da
instalação) chamavam requirePlatformAdminEscrita sem try/catch: a recusa de
support_readonly ou de MFA pendente subia ao error boundary, e a pessoa não
lia "seu acesso é somente leitura" nem "confirme a verificação em duas
etapas". Nada era gravado — a perda era a explicação.

Agora todas passam por escritaDeAdminOuRecusa(), que devolve
{ok:false, error:'forbidden_scope'|'mfa_required'} e deixa o redirect de
quem não é platform admin subir como antes. As telas leem a frase da mesma
tabela que o servidor usa (lib/auth/recusa-de-escrita-de-admin.ts, sem
next/*), com espanhol. A cerca admin-escrita-exige-scope-full ganha a regra
D: arquivo "use server" não chama o helper que lança.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:56:56 -03:00
melgarafaelandClaude Opus 5.5 ec8f52a8c6 fix(admin): suspender ou reativar sem efeito avisa "nada mudou", não "sucesso"
As rotas respondem 200 {changed:false, motivo} quando a empresa já estava
no estado pedido — outro admin agiu antes e a tela estava velha. Os hooks
ignoravam a resposta e mostravam "Tenant suspenso/reativado com sucesso";
na suspensão por cobrança o admin leria "reativado" com a empresa parada.
Agora changed:false vira toast.info "Nada mudou" com o motivo traduzido
(pt/es), e motivo desconhecido não inventa causa.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:47:25 -03:00
melgarafaelandClaude Opus 5.5 d0aaa8761f fix(suspensao): reativar só fala de cobrança com empresa suspensa de fato
O pré-check do 409 suspensao_de_cobranca perguntava "a empresa está
parada?" em vez de "está suspensa?". Uma empresa redigida pela LGPD é
parada e guarda o tipo residual 'cobranca' (o redact não limpa a coluna),
e o admin receberia a instrução errada de negociar pagamento. Agora a
régua é status = 'suspended'; a função responde nao_suspensa como sempre.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:45:05 -03:00
melgarafaelandClaude Opus 5.5 41da980e6a fix(suspensao): empresa suspensa recebe 403 claro na API, e a tela a leva ao hub
Quem estava com o CRM aberto na hora da suspensão ficava com listas vazias
e erros até recarregar: 20 rotas de app/api resolviam a org com
resolveActiveOrg, que faz redirect("/account-suspended"); o fetch seguia o
307 e entregava o HTML da página à tela como se fosse o dado.

- orgAtivaDaApi (lib/auth/require-role.ts): a org ativa para Route Handler
  que não passa por requireRole; org não operante vira o MESMO 403
  org_suspended em JSON de requireRole (mensagem única, traduzida).
- As 20 rotas passam a usá-la; páginas e server actions seguem com o
  redirect de resolveActiveOrg.
- lib/api/client.ts: 403 org_suspended leva a janela a /account-suspended
  (sem navegar de novo quando já está lá).
- Cerca por AST (tests/unit/api-nao-redireciona-org-suspensa.test.ts):
  resolveActiveOrg não volta a app/api, e orgAtivaSemPortao só onde ver a
  org parada é o objetivo (allowlist que só encolhe: impersonate).
- Teste de comportamento por 4 rotas reais e casos de orgAtivaDaApi.

Acabamentos do PR 1, itens 6 e 23.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:40:02 -03:00
melgarafaelandClaude Opus 5.5 b095c7969e chore(suspensao): renumera a migration para 0501, acima do que está na main e em voo
A main recebeu 0497–0499 (carimbos até 20260930160000) e dois PRs abertos
usam 0500@20260930170000. A 0496@20260930130000 ficaria abaixo de migrations
já aplicadas pela cadeia do Supabase CLI. Passa a 20260930180000_0501; o
apêndice do baseline, o COMMENT da coluna, o MANIFEST e a prosa acompanham.
Os UUIDs de fixture c0de0496 são identificadores e ficam.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 13:11:47 -03:00
melgarafael 390cc9e884 Merge remote-tracking branch 'origin/main' into feat/org-operante 2026-09-30 13:09:33 -03:00
Rafael Melgaço fee22e9dc3 Merge pull request #2006 from arodalves/fix/escopo-do-agente-no-espelho-do-funil
fix(agent): respeita escopo de funis no espelho de etapa
2026-09-30 12:44:15 -03:00
Rafael Melgaço 14e2953bdb Merge pull request #2007 from melgarafael/release/1.67.0
Release 1.67.0
v1.67.0
2026-09-30 12:16:35 -03:00
deskcomm-release[bot] 0a5cad7524 release(1.67.0): a versão montada a partir dos fragmentos declarados 2026-09-30 15:00:52 +00:00
Codex eb33f7d39b fix(agent): respeita escopo de funis no espelho de etapa 2026-09-30 14:40:01 +00:00
Rafael Melgaço 59c064d436 Merge pull request #1997 from webtecnica/fix/1856-debounce-configuravel
feat(agent): janela de rajada do WhatsApp configurável por agente (0498)
2026-09-30 11:29:48 -03:00
melgarafael ccb0b5141b Merge remote-tracking branch 'origin/main' into triagem/1997-debounce 2026-09-30 11:01:02 -03:00
melgarafaelandClaude Opus 5.5 ef05841653 fix(prospecting): o hash da versão trata inbound_debounce_ms nulo como ausente
O setup do agente de prospecção grava na criação o hash da versão montada, a
partir do objeto parseado, que não tem a chave `inbound_debounce_ms`. Depois ele
confere esse hash contra a linha lida do banco, onde a coluna nova da 0498 vem
`null`. Os dois hashes divergiam, e o setup recusava com "O rascunho foi alterado"
ou "O conteúdo do agente mudou" nas 5 specs de
tests/invariants/prospecting-agent-setup.test.ts (invariants vermelho no #1997).

`trigger_config` já tinha essa exceção. A lista passa a nomear as duas colunas.

Medido: test:db do arquivo, 8/8 com o conserto; sabotado (coluna fora da
lista), 5 failed | 3 passed, as mesmas 5 do CI; restaurado e conferido por grep.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 11:00:38 -03:00
Rafael Melgaço 0070779fc3 Merge pull request #1996 from webtecnica/fix/653-atraso-humano-knobs
feat(agent): atraso humano configurável por conexão (0499)
2026-09-30 10:56:35 -03:00
melgarafaelandClaude Opus 5.5 d645fe8fb9 fix(ia): a janela de rajada do agente sobrevive a salvar, reverter e criar pela tela
Os três INSERTs de versão da server action (rascunho novo de agente
publicado, revert pelo Histórico e criação pela tela) não levavam
inbound_debounce_ms: só o PATCH de rascunho existente gravava o campo, e a
janela voltava calada para a env. A coluna entra nos três, e
agent-version-columns-drift ganha a cerca que lê cada .insert({...}) de
ai_agent_versions na action (sabotada: tirar a chave de um INSERT deixa o
caso vermelho).

Ajustes de acabamento:
- drain.ts: o comentário dizia "a MESMA resolução que o turno"; a
  resolução é a de loadConversationAgentConfig (dono ou agente da sessão),
  sem router/campanha. Falha da consulta degrada para a env com log.warn,
  como a checagem de elegibilidade, em vez de mandar o evento para retry.
- AgentForm: o campo sai do meio do par "Mensagens anteriores que ele lê"
  / "Tamanho máximo desse histórico"; o valor em ms é arredondado.
- database.types.ts e o comparador do Histórico (VersionDiff) conhecem a
  coluna. Chave i18n junto das irmãs; fragmento com o crédito do PR.

Refs: #1997, #1856

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:42:37 -03:00
webtecnicaandClaude Opus 5.5 444b7ea1da fix(db): 0498 com carimbo em ordem e apêndice no fim do baseline
A 0498 levava o carimbo 20260930121224, anterior ao da 0497 já na main
(20260930140000): a ordem número x carimbo quebrava. Passa a
20260930150000, entre a 0497 e a 0499 do #1996; MANIFEST acompanha.

No baseline, a coluna e a CHECK saem do CREATE TABLE do dump e o bloco
sai do meio do arquivo: o mesmo DDL da migration vira o apêndice rotulado
"(migration 0498)" no fim. Não cria função, então não conflita com a
varredura de anon.

Refs: #1997, #1856

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:35:44 -03:00
melgarafael 00015ff1b8 Merge remote-tracking branch 'origin/main' into triagem/1997-debounce 2026-09-30 10:34:08 -03:00
melgarafaelandClaude Opus 5.5 43d6d424c0 fix(ia): o PR do atraso humano passa nas cercas e recusa minimo acima do maximo com 422
Por cima do trabalho de @webtecnica (#1996), quatro ajustes da triagem:

- baseline.sql e migration 0499: o `drop constraint if exists` fica logo
  antes do `add constraint`. Mesmo efeito; entra na janela de 10 linhas que
  tests/unit/baseline-reaplicavel.test.ts procura.
- dicionario.ts: "Minimo" e "Maximo" ganham espanhol (i18n-espanhol-cobre-a-tela),
  e "aquecido demais parece robo" vira "rapido demais parece robo".
- PUT /api/v1/ai/pacing: minimo acima do maximo efetivo devolve 422 com
  mensagem clara. Antes, com os dois gravados, o CHECK do banco recusava e a
  tela lia um 500 generico; com so o minimo acima do teto padrao (7500), o
  clamp o ignorava sem aviso.
- .changes/atraso-humano-por-conexao.md, com o credito.

Refs: #1996, #653

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 10:29:36 -03:00
webtecnica 38cd9344e9 Merge remote-tracking branch 'origin/main' into fix/653-atraso-humano-knobs 2026-09-30 10:19:02 -03:00
webtecnica 7592f332c7 feat(agent): atraso humano configurável por conexão (0499)
Os quatro números que governam o atraso humano ANTES da primeira bolha
(atraso-humano.ts: NOTAR 900ms, POR_CARACTERE 22ms, MINIMO 1200ms, MAXIMO
7500ms) eram literais no módulo, sem superfície. Uma clínica e um e-commerce
querem ritmos diferentes e recebiam sempre o mesmo valor cravado (issue #653).

Os quatro saem do literal e passam a ser 4 colunas NULL em channel_knobs — a
MESMA tabela dos knobs de pacing por conexão (0010/0495); NULL cai no default
conservador em defaults.ts que espelha os valores de antes (regressão zero).
Exposição na ficha Anti-ban via /api/v1/ai/pacing (POST/GET), com CHECK de
sanidade (max >= min, teto intervalMaxMs).

O jitter entre bolhas (literal 1200 + rand*800 em inbound-turn.ts) NÃO ganha
coluna: ele já é throttle_ms + jitter_max_ms (defaults 1200/800) e o call site
passa a LER os knobs da conexão em vez do literal.

DoD: migration 0499 + apêndice idempotente no baseline + linha no MANIFEST;
leitor testado (default, null -> default, knobs da conexão), regressão zero em
calcularAtrasoHumano; teste novo vermelho sem a mudança (sabotagem: 4 falharam).
2026-09-30 10:18:29 -03:00
webtecnica 4a8a65bcdd feat(agent): janela de rajada do WhatsApp configurável por agente (0498)
O debounce de rajada (mensagens do MESMO contato dentro da janela viram uma
resposta só) era só a env do worker INBOUND_DEBOUNCE_MS (default 8000, global,
invisível na tela, exige VPS). Agora vira campo opcional por agente em
'Freios de segurança': vazio = usa a env da instalação (regressão zero),
0 desliga a coalescência, teto de 60s clampa pelo leitor (debounceEfetivo).

O drain lê a config do agente da conversa (loadConversationAgentConfig, mesma
resolução do turno) e usa o debounce do agente no lugar do literal.

DoD: migration 0498 + apêndice idempotente no baseline + linha no MANIFEST;
leitor testado (default/env, campo, teto no limite); gate de consumidor de
knob atendido.
2026-09-30 10:03:31 -03:00
Rafael Melgaço ffb81e6e9e Merge pull request #1994 from webtecnica/fix/1959-para-evento-google-nunca-notes
test(agenda): cerca — paraEventoDoGoogle nunca vaza a anotação interna (#1959)
2026-09-30 09:39:23 -03:00
melgarafaelandClaude Opus 5.5 b24bfe6473 test(agenda): a cerca do notes cobre o caso sem observação (a linha exata da #1959)
O caso da #1994 sempre montava o agendamento com description "Primeira
consulta", então o ramo "observação vazia -> usa o notes" nunca executava:
a linha literal da #1959 (`a.description?.trim() || a.notes?.trim()`)
passava verde, 36/36. Vira `it.each` com observação visível, null e em
branco; nas duas últimas a description tem de sair undefined.

Corrige também o comentário: acrescentar `notes` aos tipos não quebra a
compilação (medido, tsc exit 0). A cerca é de runtime, porque o
sync-executor passa o snapshot inteiro com `...a`.

Medido: 38/38 verde; com a linha da #1959 aplicada, 2 casos vermelhos.

Refs: #1994, #1959

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 09:23:39 -03:00
webtecnica 2dc3af26a1 test(agenda): cerca — paraEventoDoGoogle nunca propaga a anotação interna (notes) (#1959) 2026-09-30 09:10:09 -03:00
Rafael Melgaço 1c8aa57222 Merge pull request #1992 from melgarafael/release/1.66.1
Release 1.66.1
v1.66.1
2026-09-30 08:50:07 -03:00
deskcomm-release[bot] 11521d29f4 release(1.66.1): a versão montada a partir dos fragmentos declarados 2026-09-30 11:36:59 +00:00
Rafael Melgaço 3c6832b20b Merge pull request #1989 from melgarafael/fix/lgpd-anonimizar-apaga-transcricao
fix(lgpd): anonimizar apaga também a transcrição da mídia (media_derived_text)
2026-09-30 08:35:50 -03:00
melgarafaelandClaude Opus 5.5 d2c26a4d78 docs(release): fragmento da anonimização que apaga a transcrição
Efeito nada_mudou: o operador não faz nada; a atualização só corrige o
que a anonimização apaga.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 07:51:01 -03:00
melgarafaelandClaude Opus 5.5 4a2ee6030f test(lgpd): a transcrição sai na anonimização, provado no banco e por coluna
Invariante novo prova pelo comportamento: a virada de is_anonymized
zera a transcrição da conversa do contato e da mensagem de grupo dele;
o pedido formal chega ao mesmo estado; o vizinho da mesma org fica; a
cura do apêndice (lida do baseline) alcança quem já era anonimizado e
poupa quem voltou.

Os invariantes de catálogo decidem por TABELA: um comando em messages
bastava para ela contar como coberta, e foi assim que a coluna passou
com os gates verdes. A régua por coluna, em todo UPDATE de messages dos
dois corpos instalados, nasce aqui porque aqueles arquivos estão sob o
congelamento de tests/invariants/**.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 07:51:01 -03:00
melgarafaelandClaude Opus 5.5 e4dc88018e fix(lgpd): a varredura diária apaga a transcrição que sobrou
A 0497 fecha o gatilho e cura o passado, mas nenhum gatilho alcança a
corrida do media-derive-worker: ele lê a mídia antes da anonimização e
grava o texto depois dela. A cascata da aplicação, que a rota e o cron
de retenção compartilham, ganha o passo 9: mensagem do contato com body
'[mensagem anonimizada]' que ainda guarda media_derived_text perde a
transcrição. O marcador do body poupa quem voltou a escrever. O filtro
de não-nulo vai ao banco (.not is null) para a rodada saudável não
trazer todas as mensagens de todo contato anonimizado.

O adaptador pg-como-supabase ganha not(is), com caso em arquivo próprio
(o do adaptador está congelado): sem ele o invariante da triagem 194
estourava, como o adaptador manda.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 07:50:53 -03:00
melgarafaelandClaude Opus 5.5 816e59a627 fix(lgpd): a cascata do banco apaga a transcrição da mídia (0497)
messages.media_derived_text guarda a transcrição do áudio e o OCR da
imagem que o media-derive-worker extrai. Nenhum caminho de anonimização
a zerava: o body virava '[mensagem anonimizada]' e o texto do áudio
seguia legível, inclusive para o agente.

Uma linha `media_derived_text = null` em cada UPDATE de messages de
fn_redigir_conversas_ao_anonimizar (corpo da 0494, conversas do contato
e mensagens de grupo dele) e de fn_lgpd_cascade_redact_contact (corpo
da 0483, passo 3). Os corpos foram gerados a partir do atual por script
e o diff contra a origem só tem essas três linhas a mais. Cura
idempotente pelo marcador do body: alcança quem já foi anonimizado e
poupa a mensagem nova de quem voltou a escrever.

--no-verify: o hook local check-migration-triple.sh não termina neste
clone (defeito conhecido, conserto no #1978). O triplo foi conferido
com o hook do #1978 (exit 0; controle positivo com NNNN=0495 recusado).

Achado na triagem do #1988 (@AlecLimaDev).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 07:48:59 -03:00
melgarafaelandClaude Opus 5.5 60ccab5bb0 docs(suspensao): cabeçalho do apêndice, MANIFEST, comentário do dreno e fragmento dizem o que existe
- baseline.sql: o cabeçalho do apêndice 0496 lista as seções copiadas de
  fato (A, B, C0, C, E, F e G), não só A, B, C e E.
- MANIFEST.md: a linha da 0496 descreve C0 (descarte da fila que assenta
  rascunho, Meet e turn_discarded), F (claim do follow-up sem org parada,
  retomada na reativação) e G (turn_discarded só pelo servidor), e cita o
  invariante followup-org-suspensa.
- dispatcher-org-parada.test.ts: o comentário do caso da org operante diz
  o que a sabotagem mediu — `!ehOperante` já avermelhava; só "toda org
  parada" escapava sem o caso novo.
- .changes/suspensao-que-suspende.md: os follow-ups ficam parados na
  suspensão e retomam na reativação; o passo com envio descartado ganha
  envio novo, no ritmo normal da fila.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 04:29:09 -03:00
melgarafaelandClaude Opus 5.5 a3a6cae7e6 docs(invariante): o par suspended_kind cita a migration 0496
O comentário do par organizations.suspended_kind × TIPOS_DE_SUSPENSAO
dizia "migration 0495"; a migration é a 0496 (na main, 0495 é outra).

Troca num comentário do bloco criado por este PR; contra a origin/main o arquivo segue só com acréscimos.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 04:28:17 -03:00
melgarafaelandClaude Opus 5.5 7557a2f44a fix(admin): suspender/reativar sob a trava do aviso do Meet responde 409 retry_later
fn_meet_notice pode lançar appointment_notice_busy (40001) dentro de
fn_org_parada_descarta_fila, e as duas rotas respondiam 500 genérico.
Com error.code '40001' da RPC, respondem 409 `retry_later` com "Outra
operação está em andamento para esta empresa. Tente de novo em
instantes." A transação inteira voltou, então nada foi gravado nem
auditado.

A trava continua sem espera no SQL: esperar ali, com a linha da org em
`for update`, arrisca deadlock.

Código novo em lib/api/errors.ts; um caso por rota no teste unitário.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 04:28:05 -03:00
melgarafaelandClaude Opus 5.5 86923ded00 fix(db): só o servidor grava o evento turn_discarded do follow-up
A policy followup_enrollment_events_insert (0490) deixa manager inserir
pela sessão, e a chave `…:descartado` não termina em `:<n>`, então a
guarda do passo interno não a pegava: um manager forjava turn_discarded
pelo PostgREST e o motor enfileirava um 2º turno de envio.

Seção G da 0496 (migration e apêndice idênticos): a definição vigente
da 0488 de fn_followup_generation_write, com o ramo da trilha recusando
também event_type = 'turn_discarded' quando há auth.uid() (42501
followup_step_internal). service_role e as funções de estado seguem
gravando.

Invariante novo em followup-org-suspensa.test.ts: manager pela sessão
recusado com 42501/followup_step_internal (não a recusa genérica da
RLS); service_role grava.

Invariante alterado sem afrouxar invariante da main — checar-invariantes-so-crescem.sh verde.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 04:26:39 -03:00
melgarafaelandClaude Opus 5.5 c51a86e647 docs(evidencia): telas da suspensão refeitas depois da rodada de correção
A e2e suspensao-administrativa rodou sobre a árvore corrigida (1 passed).
central-apos-reativar.png passa a mostrar o aviso só com o fato e a
orientação que manda procurar nas abas Fila e Automático.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 04:16:40 -03:00
melgarafaelandClaude Opus 5.5 dd3fde6d9a test(event-log): o dreno com a org ATIVA roda o handler 'pula' e fecha o evento
Nenhum teste passava uma org ativa pelo drainEventLog: o describe cobria só
a suspensa, a ausente e a leitura que falha, e nenhum invariante chama o
dreno. Inverter a régua (`!ehOperante`) calaria a IA e as automações de
todas as empresas com a suíte verde. Caso novo: org `active` → o handler
'pula' roda, o evento fecha `done` com a chave em consumed_by e sem
org_nao_operante. Sabotagem da régua conferida depois do commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:44:45 -03:00
melgarafaelandClaude Opus 5.5 3f908c23ce fix(db): o COMMENT de suspended_kind no apêndice do baseline cita a 0496
O apêndice dizia "(migration 0495)" — na main a 0495 é outra
(janela_de_resposta_separada) — e todo self-hoster grava esse texto no
catálogo. Era a única divergência entre o apêndice e a migration.

O comentário em tests/invariants/vocabulario-banco-x-typescript.test.ts:429
também diz 0495, mas o arquivo existe na main e a troca remove uma linha:
fica para o dono (a autorização é só acréscimo).

Prova: caso novo em org-suspensa.test.ts lendo col_description.

Invariante alterado sem afrouxar invariante da main — checar-invariantes-so-crescem.sh verde.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:44:15 -03:00
melgarafaelandClaude Opus 5.5 3b9b91a1d9 fix(admin): o diálogo de suspensão diz o que para e o que se perde
Dizia que a suspensão só bloqueava o acesso e que "pode ser revertida".
Desde a 0496 suspender cala a IA, os envios e as automações, e descarta
sem volta as mensagens na fila e as tarefas agendadas — o dono decidia
sobre uma afirmação falsa. O texto novo diz isso, e diz também o que o
conserto do follow-up decidiu: os follow-ups em andamento retomam.

Chave e `es` do dicionário trocados (a velha sai); o catálogo zh-CN
acompanha. Prova: SuspendDialog.test.tsx.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:43:04 -03:00
melgarafaelandClaude Opus 5.5 ac10cd176c fix(admin): acompanhar um tenant não cai no hub quando a org do próprio admin está suspensa
resolveActiveOrg redireciona para /account-suspended quando a org ativa
não opera. Dentro do route handler isso vira 307; o fetch do
ImpersonateButton segue o redirect, `res.ok` fica true e o admin cai em
"Conta suspensa" sem acompanhamento nenhum aberto. Regressão deste PR.

A rota só precisa saber para onde voltar ao fim do acompanhamento: passa a
usar orgAtivaSemPortao, e o comentário do helper ganha esse uso.

Prova: route.test.ts novo — com a org ativa do admin suspensa, o POST dá
200 e p_previous leva o id dela (controle com a org ativa).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:41:59 -03:00
melgarafaelandClaude Opus 5.5 b1e17024da fix(hub): 'Sair' do /account-suspended encerra a sessão
Era um <Link href="/login">: a pessoa continuava logada, e o próximo /app
a devolvia ao hub — que agora é onde todo membro de empresa suspensa cai.
Vira <form action={signOut}> com botão de envio, como a irmã
/acesso-revogado.

Provas: page.test.tsx (o botão 'Sair' é type=submit dentro de um form, e
não há link 'Sair'); no e2e da suspensão a atendente clica em Sair, cai no
/login, e /app a manda de volta ao login.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:40:55 -03:00
melgarafaelandClaude Opus 5.5 5cae8c7fa6 fix(central): o aviso de reativação manda procurar nas abas Fila e Automático
Numa empresa com IA, a conversa sem dono e sem silêncio é classificada
como `automatico` (comando-da-conversa.ts) e só cai na Fila quando a
empresa não tem atendimento automático — o aviso org_reativada mandava
revisar só a Fila. E o texto se repetia: o corpo gravado pelo banco e a
orientação da tela diziam a mesma instrução.

- fn_reativar_organizacao (migration e apêndice do baseline, idênticos):
  o corpo diz só o fato, e a contagem deixa de fora as conversas de grupo.
- Orientação em inbox-destino.ts nomeia as duas abas; chave e `es` do
  dicionário trocados (a velha sai); o comentário órfão do agent_case sai.
- Invariante: as duas strings novas e o caso em que o grupo não conta.
  Mapa de arquitetura e J37 acompanham.

Invariante alterado sem afrouxar invariante da main — checar-invariantes-so-crescem.sh verde.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:40:01 -03:00