Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: melgarafael/DeskcommCRM
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.4.0
Choose a base ref
...
head repository: melgarafael/DeskcommCRM
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.4.1
Choose a head ref
  • 16 commits
  • 25 files changed
  • 5 contributors

Commits on Aug 25, 2026

  1. fix(install): cópia irmã do repo era lida como re-execução e instalav…

    …a por cima
    
    `decide_proxy` liberava a instalação quando o dono das portas 80/443 tinha o
    MESMO nome de projeto que o meu — tratando isso como "sou eu rodando de novo".
    Mas o nome do projeto é o basename da pasta, e toda cópia do repo se chama
    DeskcommCRM: duas árvores irmãs colidem no nome `deskcommcrm` e cada uma
    declarava a outra re-execução de si mesma. Era exatamente o caso que a varredura
    de portas foi escrita para pegar ("um Caddy — inclusive o de outro DeskcommCRM
    instalado na mesma VPS"), e ela tinha ponto cego nele.
    
    Medido numa VPS de produção em 2026-08-24: uma instalação de aula em
    /root/apagar7/DeskcommCRM passou por aqui, recriou os contêineres do CRM que
    estava no ar (/root/DeskcommCRM) com o .env dela e trocou o banco da produção —
    sem um aviso. O WhatsApp seguiu conectado gravando no banco errado, e o sintoma
    que chegou primeiro foi "minha senha parou de funcionar" (a conta é outra no
    outro banco), o que manda a investigação para o lado errado.
    
    Quem separa as duas é a ÁRVORE (label com.docker.compose.project.working_dir),
    não o nome. Árvore desconhecida cai no comportamento anterior de propósito: não
    dá para AFIRMAR cópia irmã, e fechar no escuro quebraria a re-execução legítima
    que o próprio kit ensina como caminho para corrigir uma resposta.
    
    O eco de `dono_das_portas` não mudou (é contrato com 7 fixtures) — a árvore vem
    de um `docker inspect` no ocupante já identificado.
    
    O teste que deveria vigiar isto existia e não podia falhar: "Caddy de OUTRO
    Deskcomm → bloqueia" usava projeto "outro" vs "crm", nomes DIFERENTES, que é
    justamente o que não difere no caso real. Os 3 casos novos usam o fixture real
    (mesmo projeto, árvores diferentes) e prendem os dois lados: a cópia irmã
    bloqueia, a re-execução de verdade segue passando.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    rafaelmelgaco and claude committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    4312b40 View commit details
    Browse the repository at this point in the history
  2. test(install): prende o CALL SITE, não só a regra de decide_proxy

    Os casos de decide_proxy exercitam a função pura. O defeito pode voltar por dois
    caminhos independentes — a regra, ou deixar de passar a árvore na chamada — e um
    teste só da função fica verde enquanto o instalador sobe por cima da produção.
    
    Este roda o install.sh inteiro contra o `docker` dublê, reproduzindo a VPS de
    2026-08-24: um DeskcommCRM no ar em /root/DeskcommCRM e o instalador rodando de
    outra cópia cuja pasta também se chama DeskcommCRM. Verifica que recusa, que
    nomeia a pasta da instalação que já existe, que ensina o update.sh e que NÃO
    chega a subir stack nenhuma.
    
    O par é o teste: o segundo caso é a MESMA VPS com o instalador rodando de dentro
    da árvore dona — re-execução legítima, que o kit ensina como caminho para
    corrigir uma resposta. Sem ele, bastaria bloquear tudo para o primeiro passar.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    rafaelmelgaco and claude committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    c865338 View commit details
    Browse the repository at this point in the history
  3. fix(1.4.1): a release contou errado duas mudanças, e o gate que a gua…

    …rda mede a grandeza errada
    
    Forward-fix da v1.4.0, cortada hoje às 20:29. Triagem de 5 lentes independentes
    sobre a prévia do merge; 12 achados sobreviveram a um cético que tentou derrubar
    cada um. Quatro foram reverificados à mão antes deste commit.
    
    **A tag é imutável, e o `agent.sh` roda a cada 5 minutos.** Da tag até aqui
    passaram ~14 ciclos: toda VPS de pé já leu o texto da 1.4.0. Editar a seção dela
    na `main` não alcança ninguém — por isso os consertos de texto saem como
    `[1.4.1]`, que é o único caminho até a tela de quem atualizou.
    
    ─── 1. O aviso das conexões duplicadas estava INVERTIDO ────────────────────────
    
    O CHANGELOG dizia "a atualização mantém a mais antiga e marca a outra como
    conflito". O SQL do baseline faz o contrário:
    
        order by created_at desc nulls last, id desc   -- baseline.sql:13923
        and a.posicao > 1                              -- baseline.sql:13933
    
    Fica com o identificador limpo a MAIS RECENTE; a mais antiga recebe
    `-conflito-`. O comentário do próprio código, doze linhas acima, já dizia isso
    ("Fica com o identificador a sessão ativa MAIS RECENTE"), e o texto dizia o
    oposto — a duas telas de distância.
    
    Consequência: o aviso termina com "confira qual das duas deve continuar e apague
    a que sobra". Quem acreditasse na frase apagaria a conexão SEM o sufixo — que é
    justamente a que detém o `meta_phone_number_id` no provider e continua
    recebendo. Ação irreversível numa conexão de WhatsApp de produção.
    
    ─── 2. O domingo mudou para TODA instalação, sem uma linha de aviso ────────────
    
    `allowSunday` foi de `false` para `true` (`pacing/defaults.ts:62`). A coluna
    `allow_sunday` é nullable, sem DEFAULT, e nenhuma migration faz backfill: quem
    nunca tocou a chave tem NULL, e o `??` passa a resolver para `true` no instante
    do `update.sh`. Quem faz prospecção e contava com o silêncio de domingo começa a
    disparar no domingo sem ter decidido nada.
    
    Medido: o bloco `⚠️ Requer atenção` da 1.4.0 tem ZERO ocorrências de "domingo"
    (controle: 5 no arquivo inteiro, fora do bloco) — e termina dizendo "Fora isso,
    nada exige ação sua".
    
    Duas afirmações de estado que a virada deixou falsas foram corrigidas junto:
    `baseline.sql:6756` ("domingo evitado por default") e `pacing/engine.ts:9`. A
    migration 0050 NÃO foi tocada: migration aplicada é histórico.
    
    ─── 3. `salir` não descadastrava, e a release disse que sim ────────────────────
    
    O CHANGELOG anunciou que "`baja`, `salir` e `no quiero recibir` descadastram".
    Medido com as funções reais, com controle de vivacidade:
    
        "stop"                 pedido=true   provavel=true
        "sair"                 pedido=true   provavel=true
        "baja"                 pedido=true   provavel=true
        "salir"                pedido=false  provavel=false   ← a promessa
        "no quiero recibir"    pedido=true   provavel=true
        "tem como parar a dor?" pedido=false provavel=false   ← controle
    
    `salir` é o `sair` em espanhol, que está na lista desde sempre — faltar era
    assimetria, não decisão. No canal em que denúncia de spam derruba o quality
    rating e faz a plataforma recusar definições novas.
    
    Não alarga a regra: continua valendo só a palavra SOZINHA. Os três controles
    negativos novos ("voy a salir ahora", "puedo salir mas temprano?", "el pedido va
    a salir hoy?") existem para que ninguém "conserte" isso com um `includes`.
    Sabotado removendo `salir` do Set: **3 falhas, exatamente as previstas** (os três
    positivos; os controles seguem verdes). 71/71 com ele.
    
    ─── 4. Um controle citado por um nome que não existe na tela ──────────────────
    
    A seção mandava procurar "Parar a IA no limite" (duas ocorrências). Zero
    ocorrências desse texto em `components|app|lib`; o rótulo real é
    `Parar a IA ao chegar em ${fmtCents(teto)}` (`BudgetCard.tsx:399`). Dois dos três
    rótulos citados batiam e o terceiro não, o que é pior que errar os três — o
    usuário confia na lista.
    
    ─── 5. E o import assume Brasil sem dizer ──────────────────────────────────────
    
    `normalizaTelefone("(11) 99999-8888")` → `+5511999998888`. O bullet da 1.4.0 é
    minucioso (separador, colunas em português, 500 linhas, `.xlsx` recusado) e cala
    na única transformação que altera o dado do cliente. A MESMA release adicionou
    opt-out em espanhol, o que prova que o público não-brasileiro existe.
    Comportamento correto e decidido; faltava a frase.
    
    ─── 6. O gate da própria release media a grandeza errada ───────────────────────
    
    `tests/unit/changelog-cabe-na-tela-da-vps.test.ts` somava o offset ABSOLUTO do
    fim da seção no working tree — o que embute o `[Não lançado]` do momento. Num
    arquivo TAGUEADO essas duas coisas nunca coexistem: medido nas cinco tags do
    origin, `[Não lançado]` tem 20 bytes, sempre.
    
    Com 720 bytes de folga depois da 1.4.0, reproduzi com 3.645 bytes de changelog
    de um ciclo normal (o repo já teve 3.632 dois dias depois da v1.3.0):
    
        AssertionError: A seção [1.4.0] termina no byte 34288, além do corte de 30000
    
    O `verify` — status check OBRIGATÓRIO — ficaria vermelho na `main` por uma
    condição que nenhum cliente enxerga, mandando enxugar uma seção já congelada
    cuja cópia taggeada está correta. O conserto que a mensagem sugere não mudaria
    nada para ninguém.
    
    Agora mede o arquivo **como ele fica na tag** (`comoFicaNaTag()`: o
    `[Não lançado]` esvazia e o conteúdo dele vira a seção numerada), e cobra as duas
    seções que podem estourar separadamente — a já numerada e o `[Não lançado]`, que
    é a única das duas que ainda dá para enxugar. Ganhou guarda de vacuidade: sem
    ela, um parser quebrado zeraria os `it.each` e a suíte ficaria verde por não
    haver caso.
    
    Previsões declaradas antes de rodar, ambas batidas:
      - `[Não lançado]` com 3.645 bytes → **5 verdes** (o gate antigo reprovava);
      - seção nova inflada além do teto → **1 falha**, a do tamanho.
    
    A mensagem de erro deixou de afirmar que a seção "já foi taggeada" — ela não
    sabe, e o conserto certo depende disso.
    
    Caminho de produção, com o extrator real sobre os 30.000 bytes crus:
    `requiresAttention` = 1.315 bytes, com os dois avisos inteiros; `body` termina em
    frase completa. (Uma primeira sonda minha disse que o aviso das conexões não
    chegava — era ela procurando "MAIS RECENTE" numa linha só, e o texto tem a frase
    quebrada pelo wrap. Falso negativo do instrumento, não do produto.)
    
    `typecheck` exit 0 · `lint` 0 erros · `lint:channels` ok · `pnpm test:unit`
    **478 arquivos, 5336 testes, verdes**.
    
    NÃO MEDIDO: nada disto foi visto numa VPS real nem pela tela de atualização num
    browser (DoD 12); `pnpm test:shell` não foi rodado; e não sei quantas instalações
    existem nem em que versão estão — sei que o texto errado alcança todas as que
    leram a v1.4.0, não quantas são.
    melgarafael committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    d154ee1 View commit details
    Browse the repository at this point in the history
  4. docs: o invariante 8 valia para quem atualiza, não para quem instala

    A doutrina afirmava "uma segunda cópia do repo na mesma VPS recusa mexer" e
    listava "os dois call sites (agent.sh e update.sh)". A afirmação estava
    incompleta nos dois pontos: o install.sh é um terceiro call site, e era
    justamente ele que NÃO honrava o invariante — quem instalava passava por cima da
    produção enquanto quem atualizava era barrado.
    
    Acrescenta o anti-exemplo medido de 2026-08-24 (o da instalação, não o da
    atualização) e nomeia onde o terceiro call site é verificado, com a medição que
    justifica haver teste de integração além do teste da regra.
    
    CHANGELOG em linguagem de quem opera a VPS: o sintoma que a pessoa vê é a senha
    parar de funcionar, não "colisão de projeto Docker".
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    rafaelmelgaco and claude committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    7c882f5 View commit details
    Browse the repository at this point in the history
  5. feat(onboarding): o passo do telefone pergunta COMO se conecta, em ve…

    …z de assumir
    
    O produto conecta um número de três formas, e a tela de Conexões oferece as
    três. O wizard oferecia uma — e nem isso: ele não perguntava.
    
    `_client.tsx` montava e o primeiro efeito já disparava
    `POST /api/v1/onboarding/whatsapp/session`. A rota grava a linha em
    `channel_sessions` sem informar `provider`, e a coluna nasce com o default do
    transporte por código. Ou seja: **o canal nascia só de a pessoa pisar na rota**.
    Quem tem conta oficial na Meta já estava no caminho errado antes de clicar em
    coisa nenhuma, e só descobria depois, em outra tela, com o funcionário montado
    por cima.
    
    Medido na jornada, contando linhas do banco numa organização recém-criada:
    
        CANAIS CRIADOS SEM ESCOLHER: 0        (era 1)
        CANAIS APOS NAVEGAR PELOS TRES RAMOS: 0
        CANAIS APOS ESCOLHER O CODIGO: 1
    
    A do meio é a que importa: passear pelos três ramos não deixa canal pendurado.
    
    ── Três coisas que a medição decidiu, contra o palpite ─────────────────────
    
    1. NÃO se aponta para Conexões. Era o desenho óbvio — perguntar e encaminhar —
       e ele é impossível: `app/app/layout.tsx:51` devolve `/app/*` para
       `/onboarding` enquanto `onboarded_at` for nulo. O botão voltaria ao próprio
       passo, que é o controle decorativo do PR #295. Os clientes de Conexões são
       montados AQUI, inteiros, sem cópia — os dois são "use client", não recebem
       prop e buscam o próprio estado.
    
    2. A escolha NÃO é gravada. `cumprido` do passo é `Boolean(state.whatsapp)`:
       persistir no clique marcaria o passo como resolvido, e quem fechasse o
       navegador cairia no passo seguinte sem telefone e sem volta, porque o
       roteador só devolve o primeiro passo NÃO cumprido. Vive em `useState`, e o
       caso "dá para voltar e trocar de forma" é o que guarda isso.
    
    3. O caminho oficial avisa ANTES. O `install.sh` não escreve
       `META_WEBHOOK_VERIFY_TOKEN`, então em instalação fresca esse canal envia e
       nunca recebe. Dizer isso depois de a pessoa buscar três credenciais no painel
       da Meta é tarde; a tela diz antes, e aponta o caminho que funciona hoje.
    
    ── Forma ───────────────────────────────────────────────────────────────────
    
    `<label>` com radio nativo, copiado de `app/app/settings/atendimento/_form.tsx`.
    Não é `RadioGroup` porque esse componente não existe neste repo — instalá-lo
    criaria um quarto dialeto de escolha ao lado de três iguais. E não é `<Card>`:
    o wizard usa a div crua `rounded-lg border bg-background p-6`, 14 vezes.
    
    O QR ficou onde estava, de propósito. Extraí-lo obrigaria a apagar a linha 106
    de `scripts/lint-channels.ts` e a reescrever três comentários corretos só para
    escapar de um regex — e o cabeçalho daquele arquivo registra que reescrever
    prosa correta para fugir do gate piora o código.
    
    ── Verificação ─────────────────────────────────────────────────────────────
    
    `pnpm test:unit` 5338 (+9), typecheck limpo, `pnpm lint` 0 erros,
    `pnpm lint:channels` ok nas duas direções da catraca.
    
    Pela TELA, em `next start` de produção contra Supabase local com o
    `baseline.sql`, sem WAHA, sem chave de IA e sem `META_*` — o estado de quem
    acabou de instalar: `wizard-do-funcionario.spec.ts` 13/13, evidência em
    `evidence/canal-no-onboarding.md`.
    
    `vps-fresh-onboarding.spec.ts` (a P0 da doutrina de QA Visual) precisou de um
    clique novo: ela esperava o código aparecer sozinho e teria quebrado EM
    SILÊNCIO, porque não roda no CI.
    
    Sabotagem: devolver o POST para a montagem reprova 4 dos 9 casos do teste novo.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0126ezJHDNKTi7J1EM2GpfvJ
    melgarafael and claude committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    6f82e97 View commit details
    Browse the repository at this point in the history
  6. fix(agente): o relógio injetável era declarado 330 linhas DEPOIS das …

    …guardas que dependem dele
    
    O `invariants` do PR #319 falhou com 8 casos, e o diff dele nesses arquivos eram
    DUAS LINHAS DE COMENTÁRIO. A causa é anterior, e vinha reprovando a `main`
    inteira nove horas por dia sem ninguém ligar os pontos.
    
    ─── O defeito ─────────────────────────────────────────────────────────────────
    
    `InboundTurnDeps.clock` existe e o contrato dele é explícito
    (`inbound-turn.ts:620`):
    
        Relógio injetável (F2-13) — a janela horária do gate anti-ban é avaliada
        nele. Default `() => new Date()`; os testes fixam um instante dentro da
        janela para determinismo.
    
    Só que ele era declarado na linha **1411**, e as duas guardas que mais dependem
    dele estão nas linhas **1077** (janela anti-ban) e **1169** (horário de
    funcionamento) — antes de existir. As duas chamavam `new Date()` cru. O relógio
    injetado não alcançava justamente aquilo que ele existe para fixar.
    
    Em produção dá no mesmo: lá o `new Date()` É a hora certa. No CI, a hora de
    PAREDE decidia — e como a janela padrão é 7h–22h no fuso do tenant
    (`America/Sao_Paulo`), a suíte de invariantes reprovava das 22h às 7h. Nove
    horas por dia em que qualquer PR falha por motivo nenhum.
    
    A correlação está nos dados do próprio CI: TODAS as execuções verdes das últimas
    24h rodaram entre 12:36 e 23:28 UTC (23:28 UTC = 20:28 em São Paulo, dentro da
    janela). A do #319 rodou às 01:13 UTC = 22:13 — fora.
    
    ─── A prova, sem tocar em relógio nenhum ──────────────────────────────────────
    
    Reproduzido na máquina local às 22h, que é a hora em que a `main` reprova:
    
        código da main, 22:19 BRT ......... 8 falhas — as MESMAS 8 do CI
        com o clock movido para o topo .... 1 falha
        + o teste que nunca injetou clock . 9/9 verdes, ainda 22:21
    
    A segunda linha é o ponto. Consertar as duas guardas deixou UM caso de pé:
    `agent-no-credential.test.ts` nunca fixou o relógio — os arquivos irmãos
    (`limite-de-envios-por-turno`, `agent-send-template-turn`) já faziam, e ele ficou
    de fora. A guarda de horário vem ANTES da checagem de credencial no fluxo, então
    fora da janela o turno é adiado e nunca chega ao `credencial LLM` que a asserção
    procura: o teste media o motivo errado. Parar em "de 8 para 1, melhorou" deixaria
    o CI flaky pela terceira causa.
    
    Suíte de invariantes completa, às 22:30 (fora da janela): **113 arquivos, 847
    testes, exit 0**.
    
    A mudança em `tests/invariants/**` é ADITIVA — nenhuma linha removida contém
    asserção, e a contagem de casos não muda (1 antes, 1 depois). O que entra é o
    `clock` fixo que os irmãos já tinham. `DESKCOMM_GOV_INVARIANTS_EDIT=1`.
    
    ─── A classe inteira, não só a instância ──────────────────────────────────────
    
    Varridos os 11 arquivos que aceitam clock injetável em `lib/` e `workers/`. Os
    outros quatro `new Date()` crus deles NÃO são o mesmo defeito, e cada um foi
    conferido:
    
        followup/turn-bridge.ts:62 .............. default de parâmetro, legítimo
        agent/followup-turn.ts:274 .............. default, e o arquivo não tem
                                                  guarda de janela nenhuma (zero
                                                  ocorrências de janelaDeEnvioAberta)
        agent/followup-flow-classify.ts:206 ..... default, aplicado no próprio uso
        followup/gatilho-caso.ts:397 ............ `completed_at` — carimbo do
                                                  instante da gravação, não decisão
                                                  de janela; a hora real é a certa
    
    `inbound-turn.ts` era o único com o padrão "guarda de janela decidindo por
    relógio que não é o injetado".
    
    ─── Dois cuidados de método que mudaram o resultado ───────────────────────────
    
    1. A primeira execução saiu `exit=0` com falhas na tela: era o exit do `tail` do
       pipe, não do script. Refeito com `set -o pipefail`.
    2. A primeira suíte completa deu "53 arquivos falhando" e a razão era
       `No such container` — outra sessão rodava `test:db` ao mesmo tempo, e
       `scripts/test-db.sh` usa PORTA FIXA (`TEST_DB_PORT:-54329`). O Postgres sumiu
       debaixo dos testes. Medição descartada e refeita em `TEST_DB_PORT=54399`.
       (Que a porta seja fixa é defeito próprio do script, e o modo de falha se
       disfarça de teste quebrado — fica anotado, não é deste PR.)
    melgarafael committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    3da5587 View commit details
    Browse the repository at this point in the history
  7. fix(changelog): a entrada nova empurrava a seção 1.4.0 para fora do c…

    …orte da VPS
    
    `tests/unit/changelog-cabe-na-tela-da-vps.test.ts` reprovou: a seção [1.4.0]
    terminava no byte 30203 contra o teto de 30000 que o agent.sh aplica
    (`git show <tag>:CHANGELOG.md | head -c N`). Texto acrescentado ACIMA dela em
    [Não lançado] empurra o fim da seção, e o dono da VPS receberia o changelog
    cortado no meio de uma frase.
    
    Medido: a folga real era de 720 bytes e a entrada tinha 923. Enxugada para 464 —
    a seção agora termina em 29744, com 256 bytes de sobra.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    rafaelmelgaco and claude committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    2686cc3 View commit details
    Browse the repository at this point in the history
  8. test(janela): a guarda dedicada do #323 — a que pega o defeito DE DIA

    Terceira convergência independente desta rodada: o PR #323 (outra sessão)
    consertou exatamente as mesmas duas linhas deste PR, sem contato entre os dois.
    Isso é a evidência mais forte de que o defeito era real.
    
    Os consertos de código são equivalentes e ficou o daqui, por não duplicar: ele
    repete `(deps.clock ?? ((): Date => new Date()))()` nos dois pontos, o que somado
    à declaração que já existia na linha 1411 daria TRÊS lugares resolvendo o mesmo
    default. Aqui o `clock` é declarado uma vez, no topo da função.
    
    **Mas o teste dele é melhor que o meu conserto, e é ele que entra.** A matriz de
    sabotagem no cabeçalho do arquivo mostra o que eu não tinha visto:
    
                          vizinho (3)   "fora→adia"      "dentro→corre"
        defeito à noite     PEGAM       passa (motivo      PEGA
                                         errado)
        defeito de dia      passam       PEGA              passa
                            (motivo
                             errado)
    
    Os casos que já existiam fixam um instante DENTRO da janela: com o defeito de
    volta eles só reprovam à noite — uma guarda que dorme das 7h às 22h, que é
    justamente quando o time trabalha. O caso "fora→adia" é o espelho, e de dia é a
    ÚNICA coisa entre o defeito e um CI verde. O segundo caso existe pela razão
    oposta: um gate que adiasse SEMPRE também satisfaria o primeiro, e "adia sempre"
    é outro jeito de o produto emudecer.
    
    Verificado contra o conserto DAQUI (declaração única), não só contra o dele:
    
        3 arquivos, 6 casos, 23:01 BRT ............................ exit 0
        sabotado (new Date() cru de volta), mesma hora ............ 5 falhas, 1 passa
    
    A previsão foi escrita antes de rodar e bateu: pegaram o "dentro→corre", os três
    do teto de envios e o `agent-no-credential`; passou só o "fora→adia" — pelo
    motivo errado, exatamente como a matriz dele prevê para o período noturno.
    
    O que este PR tem e o #323 não: o conserto de `agent-no-credential.test.ts`, que
    nunca injetou relógio. Sem ele o `invariants` segue vermelho fora da janela pela
    terceira causa — foi o caso que sobrou quando consertar as duas guardas levou de
    8 falhas para 1.
    
    Crédito do arquivo de teste é da sessão do #323, que fica fechado apontando para
    cá.
    melgarafael committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    c6bd41d View commit details
    Browse the repository at this point in the history
  9. Merge PR #319 — a 1.4.1, e o relógio de parede que reprovava a main n…

    …ove horas por dia
    
    Forward-fix da v1.4.0 (cortada às 20:29) mais o conserto do defeito que a triagem encontrou no caminho.
    
    A 1.4.1: a seção da 1.4.0 inverteu o aviso das conexões duplicadas (o SQL mantém a MAIS RECENTE, o texto dizia a mais antiga), não avisou que o domingo passou a enviar em toda instalação, prometeu que 'salir' descadastra quando não descadastrava, e citou um rótulo de tela que não existe. A tag é imutável e o agent.sh roda a cada 5 min — só uma versão nova alcança quem já leu.
    
    O relógio: inbound-turn.ts declarava o clock injetável na linha 1411, e as duas guardas de janela que dependem dele estão nas 1077 e 1169. A suíte invariants reprovava das 22h às 7h no fuso do tenant. Reproduzido localmente às 22h: 8 falhas na main, 9/9 verdes com o conserto. O teste dedicado veio do #323 (convergência independente), que fica fechado apontando para cá.
    
    Cinco checks verdes, com o invariants rodando às 23:01 BRT — fora da janela, que é a hora em que a main reprovava.
    melgarafael authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    abbc518 View commit details
    Browse the repository at this point in the history
  10. Configuration menu
    Copy the full SHA
    855adae View commit details
    Browse the repository at this point in the history
  11. fix(onboarding): "pode receber pelo canal oficial" media METADE da co…

    …ndição
    
    Sobre o PR #321. O passo do telefone avisa quando a instalação ainda não
    consegue RECEBER pelo canal oficial — e a sonda que decide esse aviso conferia
    uma de duas variáveis:
    
        const oficialPodeReceber = Boolean(process.env.META_WEBHOOK_VERIFY_TOKEN);
    
    São dois segredos com papéis diferentes, e medido na ROTA inteira, não no
    helper (`app/api/v1/webhooks/meta/[token]/route.ts:63-66`):
    
        sem META_WEBHOOK_VERIFY_TOKEN → o handshake GET falha; o webhook nem é aceito
        sem META_APP_SECRET           → o handshake devolve 200 e TODA mensagem
                                        inbound assinada devolve 401
                                        `invalid_signature`, com ZERO ingestão
    
    O segundo é o caro, e é justamente o que a sonda não via: quem preenchesse só o
    verify token via a tela dizer que está pronto, cadastraria o número e não
    receberia nada — **sem erro em lugar nenhum**, que é o modo de falha que este
    aviso existe para impedir. E nenhum dos dois é escrito pelo `install.sh`
    (`git grep 'META_' -- '*.sh'` devolve vazio), então numa instalação nova os dois
    faltam: é exatamente quando o aviso importa.
    
    ─── Por que virou função, e não uma linha maior no `page.tsx` ─────────────────
    
    O teste que já existia cobre o COMPONENTE recebendo a prop
    (`onboarding-escolhe-como-conecta.test.tsx:83`, `montar({ oficialPodeReceber })`)
    — e o defeito estava no CALL SITE, que decide o valor da prop. Teste que guarda
    a função e não o call site não pegaria isto, e não pegou.
    
    A regra passa a morar em `lib/channels/meta/webhook.ts`, ao lado de
    `verifyMetaSignature`, que é quem consome o app secret. O `page.tsx` chama.
    
    `.trim() !== ""` e não `Boolean()`: o contrato do `.env` deste projeto é que
    vazio é ausente — o template gera `CHAVE=` —, e é assim que `preenchida()` em
    `lib/instalacao/ambiente.ts:61` já decide. `Boolean("")` é `false`, mas
    `Boolean("   ")` é `true`. A fonte é `Record<string, string | undefined>`, o
    mesmo tipo que `ambiente.ts` usa: `NodeJS.ProcessEnv` exige `NODE_ENV` e
    obrigaria todo teste a montá-lo.
    
    ─── A rede: 6 casos, sabotados nas duas direções ──────────────────────────────
    
    Previsões escritas antes de rodar, ambas batidas:
    
        voltar a exigir só o verify token .... previsto 3, medido 3
        tirar o .trim() ...................... previsto 1, medido 1
                                               (o caso "só espaços" — o único que
                                                `Boolean()` deixaria passar)
    
    ─── De quebra ────────────────────────────────────────────────────────────────
    
    `evidence/canal-no-onboarding.md:35` dizia "passear pelos três ramos" para
    reproduzir a contagem 0 da linha 31. Visitar o terceiro ramo (o código) CRIA a
    sessão — a própria linha 32 do arquivo mostra `CANAIS APOS ESCOLHER O CODIGO: 1`.
    Quem reexecutasse a evidência mediria 1 onde o texto promete 0 e concluiria que
    a guarda quebrou. Corrigido para "os dois ramos de credencial".
    
    `tsc --noEmit` exit 0 · `lint` 0 erros · `lint:channels` ok · `pnpm test:unit`
    **5352 testes verdes**.
    
    NÃO MEDIDO: nada foi dirigido num browser contra ambiente fresco de verdade
    (Supabase pg17 + baseline + bootstrap-owner + build de produção + WAHA + Redis).
    A prova aqui é de função pura, componente em jsdom e leitura da rota.
    
    Co-authored-by: melgarafael <melgarafael@users.noreply.github.com>
    melgarafael and melgarafael committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    24041b8 View commit details
    Browse the repository at this point in the history
  12. fix(install): o guarda de cópia irmã cobria 1 de ≥4 entradas — agora …

    …é o invariante 8
    
    Sobre o PR #320, que está certo no diagnóstico e conserta o caso que de fato
    aconteceu. O que a triagem mediu é que o alcance é menor do que parece.
    
    ─── O que o PR entrega, e onde ele não alcança ────────────────────────────────
    
    O painel novo de cópia irmã (`install.sh:911`) vive dentro de DOIS aninhamentos:
    `if [ -z "${REVERSE_PROXY:-}" ]` (:855) e o ramo do `case` em que o irmão é o
    DONO das portas 80/443. Medido com o harness de VPS falsa do próprio kit, três
    entradas em que ele nunca é alcançado:
    
      (a) VPS com Traefik do painel (Coolify, Hostinger, Dokploy) — `decide_proxy`
          sai por `traefik` (:573) ANTES de comparar árvore. É o caminho que o
          CLAUDE.md descreve como o normal em VPS com proxy próprio;
      (b) pasta que já concluiu uma instalação — o próprio `install.sh` grava
          `REVERSE_PROXY="caddy"` no `.env` (:1413), e na rodada seguinte o
          `if [ -z … ]` é falso. **O instalador desligava o próprio guarda**, sem
          ninguém digitar nada;
      (c) portas 80/443 livres — `decide_proxy` devolve `caddy` na primeira linha.
    
    Nas três, `docker compose … up -d` sobe sobre o parque da produção com o `.env`
    da pasta nova: outro banco, outras chaves, e o sintoma que chega primeiro é a
    senha "parar de funcionar".
    
    ─── O conserto: ligar o guarda que já existia ─────────────────────────────────
    
    `recusar_projeto_de_outra_arvore` (`_common.sh:118`) já protegia `update.sh:33` e
    `agent.sh:45`, e não protegia o `install.sh` — a porta por onde o incidente
    entrou. Ele pergunta pelos CONTÊINERES do projeto, não pelo proxy, então vale nas
    três entradas acima.
    
    Fica logo depois de `PROJECT_DIR` (que a função precisa) e ANTES da coleta de
    config: recusar depois de arrancar sete respostas de quem instala é fazer a
    pessoa trabalhar para ouvir "não".
    
    Os dois mecanismos convivem de propósito: o painel do PR dá o diagnóstico melhor
    (nomeia as duas pastas, ensina o `update.sh`) quando é alcançado; o guarda é o
    que fecha a classe.
    
    ─── Os cinco controles, com o previsto escrito antes ──────────────────────────
    
    Harness real do kit (`montar_vps`/`rodar` de `test-validators.sh`):
    
        instalação NOVA (nenhum contêiner no ar) .... previsto SEGUE    medido SEGUE
        re-executar na MESMA pasta .................. previsto SEGUE    medido SEGUE
        instalação MOVIDA (pasta antiga sumiu) ...... previsto SEGUE    medido SEGUE
        cópia irmã VIVA — o caso do incidente ....... previsto BLOQUEIA medido BLOQUEIA
        cópia irmã + DESKCOMM_ASSUMIR_PROJETO=1 ..... previsto SEGUE    medido SEGUE
    
    O terceiro controle é o que impede o guarda de nascer vermelho: quem MOVE a
    instalação de pasta deixa contêineres apontando para um caminho que não existe
    mais, e o filtro "árvore viva" (`projeto_pertence_a_outra_arvore`) já trata isso.
    
    O quinto divergiu na primeira tentativa (previsto SEGUE, medido BLOQUEIA) e a
    causa era o MEU instrumento, não o código: pus `DESKCOMM_ASSUMIR_PROJETO` no
    `.env`, e o guarda roda antes de o `.env` ser lido. Pela via que a mensagem
    ensina — variável de ambiente — funciona. Isso virou uma linha na doutrina,
    porque quem ler a mensagem e escrever no arquivo vai achar que o escape quebrou.
    
    O caso (a) foi provado ponta a ponta: com o `coolify-proxy` como dono das portas,
    o instalador PARA em "Localizando o projeto", nomeia as duas pastas e não chega a
    perguntar nada.
    
    ─── A rede ────────────────────────────────────────────────────────────────────
    
    `tests/shell/dono-do-projeto.test.sh` já prendia os call sites de `agent.sh` e
    `update.sh` — "guardar a função não basta se ninguém a chama". O terceiro entra
    com a mesma régua, mais a ORDEM (guarda antes da coleta). Sabotado removendo a
    chamada: **2 falhas, exatamente as previstas**.
    
    `docs/doctrine/packaging.md` afirmava "O terceiro call site é o `install.sh`"
    quando `grep -c 'recusar_projeto_de_outra_arvore' install.sh` devolvia **0** — a
    frase descrevia cobertura de teste e se lia como call site. Agora é verdade, e o
    texto passa a separar os dois mecanismos e a declarar o alcance de cada um.
    
    `pnpm test:shell` verde (o único gate que exercita o kit, e que NENHUM job de PR
    invoca) · `typecheck` exit 0 · `lint` 0 erros · `pnpm test:unit` 5338 testes.
    
    ─── O conflito de CHANGELOG ───────────────────────────────────────────────────
    
    Resolvido somando: a entrada do #320 é trabalho ainda não lançado e fica sob
    `[Não lançado]`; a `[1.4.1]` do #319 fica abaixo, como release. A régua do
    `changelog-cabe-na-tela-da-vps` segue folgada — a seção mais nova é a `[1.4.1]`,
    curta, com ~26 KB de sobra.
    
    NÃO MEDIDO: nada numa VPS real — todos os cenários usam o dublê de `docker` do
    kit, que não estressa `docker inspect` real, label ausente por versão do compose,
    nem contêiner criado fora do compose. Também não medi a corrida entre o
    `install.sh` de uma árvore e o `agent.sh` da outra (cron de 5 min), que é o
    estado em que o incidente de 3 dias aconteceu.
    
    Co-authored-by: melgarafael <melgarafael@users.noreply.github.com>
    melgarafael and melgarafael committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    6e51ae9 View commit details
    Browse the repository at this point in the history
  13. Merge PR #321 — o passo do telefone pergunta COMO se conecta

    O primeiro acesso deixa de impor QR: quem tem conta oficial da Meta ou provedor parceiro escolhe o caminho dele.
    
    Na triagem: a tela avisava 'o canal oficial pode receber' checando UMA de duas variáveis. Medido na rota inteira, sem META_APP_SECRET o handshake devolve 200 e toda mensagem inbound morre em 401 invalid_signature com zero ingestão — quem preenchesse só o verify token cadastraria o número e nunca receberia nada, sem erro em lugar nenhum. A regra virou metaPodeReceber() em lib/channels/meta/webhook.ts, com 6 casos e sabotagem prevista-e-medida nas duas direções (3 e 1).
    
    Cinco checks obrigatórios verdes.
    melgarafael authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    05ac672 View commit details
    Browse the repository at this point in the history
  14. Merge PR #320 — instalar de uma cópia irmã derrubava o CRM que estava…

    … no ar
    
    O instalador confundia a instalação de outra pasta com ele mesmo sendo rodado de novo e subia por cima: o site seguia no ar usando o banco da pasta nova, e o primeiro sintoma era a senha 'parar de funcionar'.
    
    Na triagem: o painel novo vive dentro de dois aninhamentos e cobria 1 de pelo menos 4 entradas. Escapavam a VPS com Traefik de painel (Coolify/Hostinger), as portas 80/443 livres, e — a pior — a pasta que já concluiu uma instalação, porque o próprio install.sh grava REVERSE_PROXY no .env e na rodada seguinte desligava o próprio guarda. O conserto liga recusar_projeto_de_outra_arvore, que já protegia update.sh e agent.sh e pergunta pelos contêineres em vez do proxy. Cinco controles com previsão escrita antes, todos batendo; sabotagem do call site dá as 2 falhas previstas.
    
    Cinco checks obrigatórios verdes; pnpm test:shell verde.
    melgarafael authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    830f53e View commit details
    Browse the repository at this point in the history
  15. docs(release): a 1.4.1 passa a descrever o que ela realmente contém

    A seção `[1.4.1]` já existia, escrita antes de o onboarding entrar na `main`, e
    dizia de si mesma: "correção de texto de aviso, não de comportamento". Deixou de
    ser verdade quando o PR #321 mergeou.
    
    Isso importa porque a TAG aponta para o commit, não para a seção: cortar a
    v1.4.1 com aquele texto entregaria ao dono da VPS uma release que muda o
    primeiro acesso do produto anunciando-se como conserto de redação. A tela de
    atualização mostra exatamente essa seção (`git show <tag>:CHANGELOG.md`), então
    o texto É o que ele vai ler antes de decidir atualizar.
    
    O que entra:
    
    - **Adicionado** — o passo do telefone perguntando como a pessoa já usa o
      número (as três formas, cada uma levando ao formulário certo), e o aviso de
      que o caminho oficial precisa de duas configurações de servidor que a
      instalação não cria: sem elas o número envia e nunca recebe.
    - **Corrigido** — o guarda de instalação em cópia irmã, que estava em
      "[Não lançado]" e vai junto na mesma release.
    
    O conteúdo que já estava na seção fica VERBATIM: os dois avisos da 1.4.0 que
    precisavam de correção (domingo e `-conflito-`), o `salir`, a planilha e o
    rótulo do orçamento. Só a abertura foi reescrita, porque ela descrevia o escopo
    antigo.
    
    Fora do changelog de propósito: o conserto do relógio de parede (#319) não tem
    efeito em produção — lá `new Date()` É a hora certa; ele reprovava só o CI. E a
    sonda de canal oficial que media metade da condição (`24041b8d`) corrigiu código
    que nunca chegou a ser publicado.
    
    Verificado pelo caminho de produção: `changelog-cabe-na-tela-da-vps` verde, e a
    extração real sobre os 30.000 bytes que o agente envia devolve a seção inteira —
    com o onboarding, o instalador, o `salir` e os dois avisos, terminando em frase
    completa. Seção com 5.188 bytes; o corte é em 30.000.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0126ezJHDNKTi7J1EM2GpfvJ
    melgarafael and claude committed Aug 25, 2026
    Configuration menu
    Copy the full SHA
    d453ac3 View commit details
    Browse the repository at this point in the history
  16. Merge PR #330 — a 1.4.1 passa a descrever o que ela realmente contém

    A seção existia escrita antes de o onboarding entrar. A tag aponta para o commit, não para a seção: cortar assim entregaria uma release que muda o primeiro acesso anunciando-se como conserto de redação.
    melgarafael authored Aug 25, 2026
    Configuration menu
    Copy the full SHA
    bea0d50 View commit details
    Browse the repository at this point in the history
Loading