mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 09:34:46 +08:00
main
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d37cd5ca3b |
fix(kit): a atualização instala a release publicada, nunca a maior tag
update.sh (alvo) e agent.sh (o que a tela anuncia) escolhiam git tag -l 'v*' --sort=-v:refname | head -1. Em 2026-09-13 a v1.20.0 existia como tag manual sem release, com /releases/latest devolvendo v1.19.0: todo clone teria instalado código que ninguém lançou. ultima_release_estavel (_common.sh) pergunta /releases/latest do repositório da origin. API sem resposta = "não sei" (update.sh recusa; agent.sh marca COMPARE_FAILED), nunca a maior tag. Origin fora do GitHub (espelho, caminho local) não tem API de release e segue com a maior tag, como antes; o endereço é trocável por DESKCOMM_RELEASES_LATEST_URL (forks e testes, via file://). Prova: tests/shell/release-publicada-nao-maior-tag.test.sh, 7/7; sabotado (função devolvendo a maior tag), 3 vermelhos, os previstos. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
befedad6db |
docs(triagem): a afirmacao de estado sobre o gate do changelog acompanha o conserto
DoD 16. Duas afirmacoes ficaram falsas quando a medicao do acervo saiu do `pull_request`: - `triagem/TRIAGEM.md` dizia que `changelog-cabe-na-tela-da-vps.test.ts` reprova quando o acervo passa de 30.000 bytes. Ele nao reprova mais por isso, e a consequencia e operacional: o PR de LOTE deixa de ficar vermelho por acervo cheio, e o vermelho so chega depois, no push da main. Quem integra passa a rodar `pnpm release:acervo-cabe` ANTES de mesclar o lote — o comando entrou no bloco de conferencia que ja estava la. O numero saiu do texto: o teto e lido do `agent.sh`, e frase com numero envelhece; - `hostgator-setup-kit/agent.sh` mandava manter a linha do corte inteira porque o TESTE a lia por regex. Quem a le agora e `lib/release/cabe-na-tela.ts`, e os leitores dela sao dois, com atores diferentes. O comentario passa a nomear o leitor real — quem quebrar a linha precisa saber o que explode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHzAhMNW4yRWfgR5Ack89m |
||
|
|
edc5f8c638 |
merge: trazer a main para dentro de triagem/803-frente-a-atualizacao-segura
A base desta branch estava atrasada em relacao a origin/main, e por isso as medicoes de CI do PR #1198 falavam de um mundo que nao existe mais (e, enquanto o PR fica CONFLICTING, o GitHub nem chega a criar o merge ref, entao nenhum run de pull_request nasce). Este merge so traz a main para dentro. Um conflito, TRIVIAL: `package.json`, script `test:shell` — as duas pontas ACRESCENTARAM testes de shell distintos ao mesmo encadeamento, e nenhuma removeu nada (medido contra a merge-base: base 12 itens, a branch somou `atualizacao-para-quem-fala-com-o-banco.test.sh`, a main somou `senha-do-cron-vazada.test.sh` e `hooks-nao-acusam-a-main.test.sh`). A resolucao e a uniao dos tres, 15 itens, cada um na posicao em que seu autor o pos. Nenhum outro conserto embutido. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6f79c0fe54 |
fix(kit): a senha das rotinas que ficou no log do sistema é trocada sozinha
O #1054 (@rafaeskytrabalho) parou de escrever INTERNAL_CRON_SECRET na linha do crontab, mas a senha que o syslog/journal já guardou seguia valendo. Decisão do dono (18/09, opção a): o kit troca sozinho, uma vez, sem edição de .env. - trocar_segredo_do_cron_vazado (_common.sh): gera → .env → dc up -d → arquivo do cron → marca. Troca só o segredo que ia na linha (CRON, ou INTERNAL_SECRET quando o CRON está vazio). Falhou o up: volta a senha velha, sem marca, tenta de novo depois. Cadeado no .update.lock do agent.sh. - Chamada de dentro de setup_event_log_drain_cron só no update.sh do terminal: é a função que o corpo de todo update.sh publicado já chama depois de reler o _common.sh, então a troca chega na mesma atualização que a traz. - Pelo botão da tela NÃO troca no update.sh: o agent.sh que o dirige guarda a senha velha e o run_result levaria 401. O agent.sh seguinte (≤5 min, lido do disco a cada vez) faz a troca e já anuncia com a senha nova. - install.sh nasce marcado, salvo se o crontab ainda tem a linha antiga. Teste: tests/shell/senha-do-cron-vazada.test.sh (no test:shell), com o _common.sh e o agent.sh reais e dublês de docker/crontab/curl. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DeeqfKfpL79Sw79XG9X8g1 |
||
|
|
90572a9fd6 |
feat(kit): a atualização para o CRM enquanto mexe no banco, e avisa quem estiver na tela
Frente A do PR #803, de @paulolimajr77 — atualização segura da VPS. A frente de
agenda do mesmo PR não entra aqui: ela é independente e segue pelo caminho normal.
O que muda para quem opera uma VPS: durante a parte do banco o CRM sai do ar por
alguns segundos, de propósito, e quem estiver com a tela aberta vê uma página
"Estamos atualizando o sistema" em vez do erro do navegador. A página volta
sozinha. Nenhum passo manual foi acrescentado — o botão é o mesmo.
Os cinco consertos, todos medidos por ele numa instalação real:
- O CRM, o worker, o agendador e três peças do Supabase param antes do DDL.
Medido no mesmo dia e com o mesmo arquivo: 113 travamentos com tudo de pé, 60
com o CRM parado, 0 com as três peças também paradas. Travamento aqui não é
lentidão: quando o `create policy` trava, o `drop` que veio antes já valeu — a
regra some e o banco passa a negar leitura em silêncio.
- As regras de isolamento são conferidas uma a uma no fim, contra o pg_policy. Se
faltar alguma, as que faltam são recriadas a partir do PRÓPRIO baseline (não
reaplicando o arquivo inteiro, que foi medido e NÃO converge); se ainda faltar,
a atualização PARA e o CRM não volta ao ar. Um CRM fora do ar é um problema
visível; um CRM no ar sem isolamento mostra tela vazia e parece normal.
- A volta das peças deixa de ser muda: confere uma a uma, tenta de novo, e nomeia
em vermelho quem não voltou. Numa instalação real elas não voltaram e a
atualização disse "concluída com sucesso".
- A página não responde pela API: `/api/` recebe 503 + JSON curto, e só o
navegador recebe HTML. Com a página de pé, o agente da atualização recebia 18 KB
de HTML no próprio registro de erro e ficava cego na janela que precisa narrar.
- O aviso de versão nova só acende quando a imagem existe no registro. A etiqueta
sai uns seis minutos antes do pacote, e clicar nessa janela parava a atualização
no meio. Registro fora do ar NÃO apaga o botão — a sonda de controle é a versão
instalada.
Portado sobre a `main` de hoje, 1291 commits à frente do PR. O que mudou ao
reconciliar, e por quê:
- O quinto conserto dele ("a atualização roda pela versão que instala, não pela
velha") JÁ ESTÁ na `main` desde
|
||
|
|
ad7da221f1 |
fix(#1040): a rodada do banco chega na rota — e não afirma fechamento que não houve
Os três consertos do review, todos com prova: - O fio. `ler_rodada_do_banco` passa a imprimir as três chaves PLANAS e com os nomes que a rota lê (`disputa_de_banco`, `retentativas_do_banco`, `passada_do_banco`), e o `agent.sh` as manda no corpo do `run_result`. Antes ele aninhava um objeto em `rodada_do_banco` com `disputa`/`retentativas`/ `passada`: o `z.object` descarta chave desconhecida em SILÊNCIO, os três campos opcionais chegavam `undefined` e as colunas eram gravadas NULAS em toda rodada — a tela ficava calada, que é exatamente o silêncio que este PR veio tirar. Trava: caso em `route.test.ts` com o corpo literal do kit, e caso de unidade que amarra os mesmos nomes nos três arquivos (kit, agent.sh e rota). - O desfecho. Rodada que NÃO fechou o banco não registra mais nada: o esgotamento gravava os MESMOS números do sucesso, e o erro fatal gravava um `0 0 1` que ninguém mediu. As frases da tela são todas escritas como "…até a atualização do banco fechar", então silêncio é o degrau certo. Trava: seção 10 da prova de shell — o arquivo fica vazio, com controle positivo de que a rodada aconteceu. - O ramo morto. `disputa: false` com `retentativas >= 1` é impossível por construção (os dois saem do mesmo contador de passadas: `disputa = passadas > 1` e `retentativas = passadas - 1`), então virou `null` em vez de uma frase para um estado que ninguém produz — e o teste que o cobria saiu junto. A coluna da rodada também deixa de poder derrubar o desfecho: ela sai numa escrita própria, antes da que fecha o run, e best-effort. Na escrita única de antes, um 42703 (banco anterior à migration da rodada) ou a CHECK recusando o número virava 500, o `post` do agente voltava vazio e o run ficava preso em `dispatched` para sempre — o desfecho que ninguém detecta. |
||
|
|
1bf7c8762d |
fix(system): rodada de atualização conta a disputa do banco, as retentativas e a passada
O PR #997 mostrou que o baseline reaplicado sobrevive a uma disputa de lock com o sistema no ar: o kit tenta de novo e fecha. Até aqui essa parte da história morria no log do servidor — quem clicou via "terminou" sem saber que a base estava ocupada, nem quanto custou. Agora a rodada carrega isso e a tela conta ao terminar, em português de gente: - migration 0275 + apêndice idempotente no baseline.sql + linha no MANIFEST: três colunas em system_update_runs (disputa_de_banco, retentativas_do_banco, passada_do_banco), com CHECK de coerência e comentários de coluna; - o kit registra a rodada em cada saída do reaplicar_baseline (_common.sh) e o agent.sh manda o campo no run_result; - o relatório da versão lê as colunas e expõe rodada_do_banco, e o painel de atualização mostra o texto no fim da rodada (deu certo e falhou com volta atrás); - rodada que não passou pelo banco não afirma nada: sem medição o campo chega ausente e a tela fica calada, em vez de dizer "zero disputas". |
||
|
|
eed657ddcf |
fix(atualizacao): a tela mostra TODAS as versoes entre a sua e a nova, com os avisos de cada uma
O dano nao e hipotetico e esta escrito no commit
|
||
|
|
c861cbdcd8 |
fix(kit): duas cópias do repo disputavam os mesmos contêineres e trocavam as credenciais
`/root/DeskcommCRM` e `/root/apagar6/DeskcommCRM` têm o mesmo basename, logo o mesmo nome de projeto compose. O cron rodava o `agent.sh` das duas a cada 5 minutos, e cada `up -d` recriava o parque com o `.env` da sua árvore. Medido na VPS de produção: 21/08 13:30 o clone de teste recriou o contêiner do WAHA com a chave dele; 14:47 o app foi recriado da árvore de produção, com outra. Por três dias toda chamada ao WhatsApp respondeu 401 — nenhum número conectava — e as sessões caíam a cada recriação (STOPPED em 20/08 e 21/08). O `flock` do agent.sh não pega isso: tranca por DIRETÓRIO, e as duas árvores pegam locks diferentes enquanto disputam os mesmos contêineres. A trava tem de ser pelo que elas compartilham — o projeto Docker —, e o sinal já existe: o label `com.docker.compose.project.working_dir` de cada contêiner. `recusar_projeto_de_outra_arvore` falha fechada na ação e aberta na informação: nomeia as duas árvores e ensina a saída. `DESKCOMM_ASSUMIR_PROJETO=1` cobre o caso legítimo de instalação que mudou de pasta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L4p9H7rGReKzkLuwtZ1ABB |
||
|
|
81b3bd5d83 |
feat(kit): o agente completa o pin sozinho — só a lacuna, nunca a decisão
O estado que a primeira atualização deixa (worker seguindo canal móvel) durava até alguém rodar o update de novo — e ninguém roda, porque a tela diz "Atualização concluída". Agora dura no máximo um ciclo de cron. A regra que torna isso seguro é a que o Rafael aprovou: **preenche lacuna, nunca sobrescreve valor explícito.** Chave ausente é omissão do script antigo; chave presente é decisão de quem opera — inclusive a de seguir um canal de propósito. Um cron que corrigisse escolha alheia seria pior que o defeito. E grava a versão que o contêiner JÁ ESTÁ RODANDO (label image.version da imagem em uso), não a do app. A diferença importa: se o worker estiver numa versão diferente, gravar a do app MUDARIA o que roda no próximo `up -d` — possivelmente um downgrade. Assim é congelamento puro. Ensaiado na VPS com CRON DE VERDADE, não com a função chamada à mão: instalei a linha, esperei disparar, e o .env ganhou as quatro chaves em 1.3.0 com a customização do operador intacta. Cron removido e VPS varrida depois. Duas sabotagens, ambas derrubando o veredito: remover a regra do "só lacuna" reprova 4 provas (incluindo a que protege o `:stable` explícito); inventar a versão em vez de ler da imagem reprova 2. **Um campo decorativo que eu mesma tinha acabado de criar, removido.** No commit anterior pus `pin_incompleto` no heartbeat. Fui verificar: o schema da rota é `z.object` sem `.strict()`, então o Zod DESCARTA chave desconhecida em silêncio — o campo viajaria e não chegaria a lugar nenhum. É o mesmo anti-pattern que esta sessão já corrigiu duas vezes (controle que existe e não controla). Fica só o log do host, que é real, até o app aprender o campo. E um comentário meu que mentia: escrevi que `[ -w ]` protege contra escrita sem permissão. Não protege — o cron roda como ROOT, e root ignora chmod. Medi com `chattr +i`, que barra até root: a escrita falha, a função sai 0 e o .env chega intacto do outro lado. Quem protege é a atomicidade do `set_env_var` (tmp + mv), não a guarda. Living System Checklist: entrada = .env + imagens em execução; saída = .env corrigido + log do host; registro = log_err com a frase inteira; anti-morte = o cron reexecuta a cada 5 min, e o update.sh manual cobre se o cron estiver parado; laço de retorno = se gravar errado, o `diagnostico.sh` e o `pin_incompleto` da execução seguinte acusam. Dívida declarada: não aparece na tela — o estado é transitório (≤5 min) e autocorrigido, e o que precisa aparecer (a versão) já aparece. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa |
||
|
|
9f8fdc314b |
fix(kit): o pin pela metade deixa de ser silencioso — e o aviso vai onde ele é visto
A primeira atualização de uma instalação legada termina com "Atualização
concluída — app no ar e saudável" e deixa o worker seguindo um canal móvel. Todo
mundo do parque para aí, porque a tela disse que acabou. É a mesma classe do bug
original: estado errado que nada denuncia.
**O lugar óbvio produziria código morto, e medi antes de escrever.** O pedido era
"o update.sh detecta ao final e diz o que falta" — mas `gravar_imagens` roda na
linha 175, antes do `dc pull` (182), do `up -d` (203) e do "Atualização concluída"
(234). Ao chegar no fim, o estado JÁ está pinado: um aviso ali nunca dispararia.
Então a detecção vai em dois lugares, e cada um cobre o que o outro não alcança:
- **update.sh**: lê o estado ANTES de `gravar_imagens` e conta no fim o que
corrigiu ("a versão de worker scheduler estava solta e foi fixada agora").
Dispara na 2ª execução, que é quando o estado ruim existe.
- **agent.sh**: é o único que roda DEPOIS da 1ª execução já com o kit novo em
disco (cron de 5 min, lido a cada vez). Publica `pin_incompleto` no heartbeat e
escreve a frase inteira no log do host — porque o campo do heartbeat só aparece
na tela quando o app aprender a exibi-lo, e quem parou na 1ª precisa ser
alcançado agora.
Ele AVISA, não corrige. Gravar no `.env` de uma instalação alheia é mudança de
comportamento e é decisão de quem opera, não de um cron — vai como proposta no
relatório, não como fato consumado.
Três defeitos meus no caminho, os três achados por medição e não por revisão:
1. `valor_do_env` sem `|| true`: sob o `set -euo pipefail` do _common.sh, um
`grep` que não casa mata a função — justamente no caso da chave AUSENTE, que é
o que interessa. Dois casos davam verde de mentira.
2. O bloco de teste foi anexado DEPOIS do `exit` do script: código inalcançável.
3. Depois de movido, ele rodava sem sourcear o _common.sh — e três dos cinco
casos passavam por VACUIDADE ("comando não encontrado" devolve string vazia,
que casa com o esperado vazio). A guarda `command -v pin_incompleto` existe por
causa disso.
E o veredito do arquivo era impresso ANTES do bloco novo: se ele falhasse, a
linha "OK — todas as provas passaram" sairia mesmo assim. Movido para o fim, e
sabotado para confirmar — quebrei a detecção e o veredito virou "FALHOU — 1".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
|
||
|
|
6eb0b2d2ee |
fix(packaging): os 11 defeitos de gravidade alta que a revisão adversarial achou
Rodei 6 lentes independentes sobre o próprio diff, cada achado passando por um
cético que tinha de REPRODUZIR o cenário. 27 confirmados de 38, e 11 altos. Os
que quebravam de verdade:
**A transição pareava o app de uma release com o worker do topo da main.** Numa
VPS que atualiza pela primeira vez, quem executa é o update.sh VELHO — o do
disco. Ele faz `git checkout` (que já troca o compose pelo novo) e só sabe
gravar APP_IMAGE. Worker e scheduler caíam no default do compose, que era
`:latest` = topo da main. Ou seja: a entrega que existe para consertar "o worker
nunca era atualizado" faria o parque rodar código NÃO LANÇADO no runtime do
agente de IA, sobre o banco da release. O default virou `:stable` (a última
release) — conserto de uma linha, sem tocar em script, para uma corrida que
nenhum script novo pode vencer.
**`dc pull` sem guarda abortava a instalação NOVA.** Dar `image:` a um serviço
que era build-only muda o `pull` de "Skipped" para FALHA quando a referência não
resolve — e há três motivos reais para não resolver logo após um release: pacote
novo no GHCR nasce PRIVADO, a tag git existe minutos antes das imagens, e o
registro pode cair. O install morria no passo 9, com banco provisionado e .env
escrito. Ganhou a mesma guarda que o update já tinha.
**O pin do WAHA não chegava a ninguém.** `install.sh:1338` gravava
`WAHA_IMAGE=devlikeapro/waha` — sem tag — por cima do default pinado do compose.
O pin existia e era inalcançável.
**Toda tag `v*` também movia `latest`.** `enable={{is_default_branch}}` é
verdadeiro num push de tag, então o canal oscilava entre "topo da main" e
"última release" — apagando a distinção de que a doutrina inteira depende.
`latest` agora exige `ref_type == 'branch'`, e `stable` exige tag `v*`.
**O rollback podia matar todos os crons.** Numa instalação legada, `dc images -q
scheduler` devolve o ID do `alpine:3.20` antigo, cujo crontab vinha de um
`command:` que este compose não tem mais. Gravar esse ID deixaria o contêiner
rodando o CMD do alpine puro: sai na hora, `restart: unless-stopped` recoloca,
crashloop silencioso. Agora só entram no rollback os serviços cujo .env já tem a
chave — isto é, instalações que já passaram pelo update novo.
**A doutrina afirmava o que não era.** Dizia que `build-and-push` já era check
obrigatório (a branch protection diz `verify, build-and-size, invariants, e2e`),
citava dois arquivos de teste que não existem, e creditava ao update-guard uma
prova que é do test-validators. Cometi, dentro da doutrina, exatamente o defeito
que ela existe para impedir. Agora a pendência está escrita como pendência, com
a medição e o motivo de a ativação não poder vir antes do merge.
**O único artefato executável novo não tinha gate** — e escondia uma injeção: o
heredoc interpolava o INTERNAL_SECRET dentro de aspas duplas, então uma crase no
segredo virava substituição de comando a cada minuto. Medido, com o valor real
que saía: `segrafaelmelgacoredo/Users/rafaelmelgaco…`, com `whoami` EXECUTADO.
O valor passou para aspas simples com escape, e tests/shell/scheduler-entrypoint.test.sh
monta o header com um `sh` de verdade e compara byte a byte — sabotar de volta
para aspas duplas reprova.
De quebra: `SCHEDULER_APP_ORIGIN` era controle decorativo (o entrypoint lia, o
compose não repassava, nenhum template documentava) e saiu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
|
||
|
|
1c9ead8586 |
feat(kit): a instalação nasce numa VERSÃO, não no topo da main
`install.sh` gravava `APP_IMAGE=…:latest`, e aqui `latest` não quer dizer "a
última release": a regra do CI é `enable={{is_default_branch}}`, então ela segue
o topo da `main` — código ainda não lançado. Quem instalava no dia 6 e quem
instalava no dia 20 rodavam software diferente, ambos dizendo "estou no latest".
A issue #184 chegou descrevendo o ambiente como "latest do dia 06/08/2026", que
é a admissão de que a versão não era nomeável.
Agora o install resolve a última tag publicada no REMOTO (`git ls-remote
--tags --sort=-v:refname`) — precisa ser no remoto porque o clone é `--depth 1`
e não traz tag nenhuma, então um `git tag -l` local devolveria vazio e a
instalação nasceria em `latest` de novo, sem ninguém perceber.
Falha ABERTA: sem rede, sem git ou sem tag no remoto, cai em `latest` como
antes e avisa. Travar a instalação de alguém por não resolver um número seria
trocar previsibilidade por disponibilidade.
O `update.sh` passa a gravar as TRÊS imagens juntas, na mesma versão — app numa
versão e worker em `latest` é uma matriz de compatibilidade que ninguém testou.
E o `dc pull` deixou de ser fatal: se alguma imagem ainda não existir no
registro (instalação subindo para a primeira versão publicada depois desta
mudança), o compose constrói pelo `build:` que ficou ao lado. Mais lento, mesmo
resultado — melhor que não atualizar.
O rollback do agent.sh voltava SÓ o app. Com worker e scheduler agora pinados,
isso deixaria app na versão antiga e os outros dois na nova — a mistura de
versões que a doutrina existe para impedir. Os três voltam juntos, e só entram
no rollback os que o `images -q` conhecia.
Uma prova do update-guard mudou de lado, e não por conveniência: ela exigia
`APP_PULL_POLICY=always`, com um motivo real (um rollback deixava `missing` com
ID local e ninguém desfazia). O que mudou foi a régua — medi que `always` com o
registro sem responder faz o `up -d` FALHAR e o contêiner não subir, mesmo com
a imagem no disco. Com tag imutável, `always` não protege de nada e amarra a
subida do CRM à disponibilidade do GHCR. O medo original segue coberto: o
`dc pull` do update é explícito e independe do pull_policy. A justificativa
ficou escrita ao lado da prova, não na mensagem de commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
|
||
|
|
107a978c9f |
feat(kit): suporte a VPS que já tem proxy reverso (Traefik)
Várias hospedagens entregam a VPS com um Traefik próprio ocupando as portas 80 e 443 — Hostinger, Coolify, Dokploy, CapRover. É esse Traefik que dá HTTPS automático a tudo que o painel delas instala. O kit sobe um Caddy que quer as MESMAS portas. Dois processos não podem ouvir a mesma porta, então nessas VPS o `up -d` falha no bind e a instalação morre no meio, com um erro de Docker que não diz nada a quem não é técnico. A saída óbvia — desligar o Traefik da hospedagem — quebra as automações do painel dela, presentes e futuras. O que entra: - docker-compose.traefik.yml (novo): override que põe o `caddy` num profile inativo e publica o `app` por labels do Traefik, reproduzindo uma a uma as regras do Caddyfile — rota do domínio, TLS, gzip e o bloqueio 403 do webhook global do WAHA. Esse bloqueio importa: quem soubesse o endereço injetaria mensagem falsa no CRM. O Traefik não tem "responda 403", então o equivalente é um ipallowlist com faixa 192.0.2.1/32 (TEST-NET-1, RFC 5737) — nenhum cliente real casa. - REVERSE_PROXY=caddy|traefik no .env, DETECTADO sozinho pelo install.sh (procura um contêiner Traefik rodando). Default 'caddy': toda instalação existente continua idêntica, sem passar a saber que este arquivo existe. - dc() / dc_files() no _common.sh e no install.sh: todo `docker compose` do kit passa a montar a lista de -f conforme o modo. Inclui as mensagens que ensinam comandos ao dono — antes elas mandariam um comando sem o override, e o próprio dono subiria o Caddy por cima do Traefik seguindo a instrução do kit. - update.sh: pula o `up -d --force-recreate --no-deps caddy` no modo traefik. O profile inativo NÃO bastava: nomear o serviço explicitamente ATIVA o profile dele no Compose e o contêiner sobe assim mesmo. Comprovado com `docker compose --dry-run` (v5.3.0), que reporta "Container crm-caddy-1 Starting/Started". Sem isso, toda atualização numa VPS com proxy externo terminava num "⚠ não consegui recriar o proxy" — alarme falso. Verificação: numa VPS Hostinger real, com o CRM instalado e no ar, a config resolvida por docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml config é IDÊNTICA à do compose adaptado à mão que já roda lá. No modo padrão a lista de serviços segue com o caddy, e os validadores do kit (test-validators.sh) passam. |
||
|
|
2df62aa2cb |
feat(update): atualizar o CRM pela propria tela, sem SSH (#61)
* docs(update): desenho do botao de atualizar pela propria tela O app roda em container sem acesso ao host, entao o botao nao executa nada: publica uma intencao que um agente no host (mesmo cron que ja drena o event_log) le e converte em `update.sh` da tag assinada. O que atravessa a fronteira e um booleano, nao um comando. Alvo passa a ser a ultima tag SemVer, nao o topo da main — assim a tela mostra a secao curada do CHANGELOG em vez de mensagem de commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * docs(update): a tela prometia progresso que o agente nao reportava A secao da tela mostrava backup/codigo/banco passo a passo, mas o agente so falava no fim — barra de progresso decorativa. Passa a reportar cada passo concluido enquanto o app ainda esta de pe (os passos lentos sao justamente os que acontecem antes do reinicio). Tambem: `unknown` e derivado na leitura, nao reportado (agente morto nao anuncia a propria morte); e a instalacao fora de tag e levada a ultima release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * docs(update): plano de implementacao em 9 tasks Auto-revisao contra o codigo real corrigiu quatro divergencias: fail() tem o status POSICIONAL (nao dentro de opts), o tipo do GET tinha dois nomes diferentes entre as tasks 5 e 6, `system.update_finished` estava declarado e nunca emitido, e o E2E mandava "siga o helper vizinho" em vez de apontar o arquivo e as linhas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): tabelas de instancia da atualizacao self-service system_version (singleton) e system_update_runs descrevem o servidor, nao o inquilino — sem organization_id. RLS habilitada com ZERO policy: negar e o default estrutural, nao depende de a rota lembrar de checar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): extrator da secao do CHANGELOG O agente do host manda o arquivo cru; quem interpreta e o app, em TypeScript, porque aqui vira funcao pura testavel em vez de awk dentro do bash. O bloco "Requer atencao" sai separado do corpo: a tela o mostra ACIMA do botao, senao quem precisa agir a mao descobre rolando a pagina depois de ja ter clicado. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): corrigir extrator contra CHANGELOG real Correcoes empiricas encontradas pelo revisor rodando contra CHANGELOG.md real: 1. CRITICAL 1: regex ATTENTION_HEADING agora casa tanto com heading (### ⚠️ Requer atenção) quanto com negrito (**⚠️ Requer atenção**). O arquivo real usa heading de nível 3. 2. CRITICAL 2: teste "para no proximo heading" era placebo — testava versao ultima sem proxima secao seguinte. Reescrito para testar "Nao lancado" que tem "1.1.0" como proxima, garantindo que end=i é testado de verdade. Sabotagem prova que 2 testes quebram quando end=lines.length. 3. IMPORTANT 3: delimitador do bloco de atencao agora termina apenas em heading de verdade (^#{2,4}). Remover /^\*\*/ evita cortar em negrito aleatório no meio de um aviso multi-parágrafo. 4. IMPORTANT 4: funcao cleanBody remove referências de link do final (formato [algo]: http://...), evitando que rodapé do arquivo apareça na tela antes do botão "Atualizar". 5. Teste contra CHANGELOG.md real: valida que versao 1.0.0 é encontrada com requiresAttention não nulo, protegendo contra divergência entre fixture e arquivo real. Cobertura agora: 12 testes, incluindo sabotagem validada. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): corrigir testes placebo e vazamento em requiresAttention Correcoes criticas encontradas na rodada 2 de revisao: 1. CRITICAL: Teste que le CHANGELOG.md real era placebo com try/catch vazio. As asserções estavam dentro do try, logo erros eram engolidos e o teste passava mesmo com a implementacao quebrada (regex antigo não casa com heading). Fixes: remove try/catch, usa path relativo ao __dirname do teste (não cwd), restaura o regex quebrado e roda para provar que 3 testes falham pelo nome. Testes que quebram quando regex = antigo: - "extrai bloco de atencao em heading (### ⚠️ Requer atencao)" linha 58 - "le CHANGELOG.md real e encontra 1.0.0 com requiresAttention" linha 120 - "remove referencias de link do bloco de atencao..." linha 131 2. IMPORTANT: Rodape de links vazava em requiresAttention (não só em body). extractAttention usava linhas cruas. Contra arquivo real pedindo 1.0.0 (ultima versao com bloco de atencao), requiresAttention incluia: "[1.0.0]: https://github.com/..." Fixes: aplica cleanBody ao resultado de extractAttention. Novo teste cobre especificamente versao que eh ultima E tem bloco de atencao. Cobertura agora: 13 testes, sabotagem validada em 3 casos distintos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): vocabulario e transicoes do run de atualizacao Run que ja terminou e imutavel: o agente tenta reportar de novo apos o reinicio do app, e a segunda tentativa e recusada em vez de reescrever a historia. `unknown` nao existe como estado gravado — e derivado na leitura, porque um agente morto nao consegue anunciar a propria morte. Edita tests/invariants/vocabulario-banco-x-typescript.test.ts (congelado) para ACRESCENTAR dois pares novos ao array PARES (system_update_runs.status/last_step -> RunStatus/RunStep) — manutencao prevista pelo proprio cabecalho do arquivo, nao correcao de invariante existente. Sabotagem confirmou o morde: removendo "failed_rolled_back" de RunStatus e "banco" de RunStep, cada par reprovou isoladamente (1 teste vermelho por vez, os outros 376 verdes) e foi restaurado em seguida. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): endpoint unico do agente do host heartbeat / run_progress / run_result numa uniao discriminada. A resposta do heartbeat carrega um booleano — nunca um comando: mesmo com o app comprometido, o atacante nao escolhe O QUE roda no host, so quando o update.sh assinado roda. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): fecha os 3 buracos da rota do agente do host (review 1/5) Corrige 3 achados do revisor na Task 4: (1) o lookup do run pendente no heartbeat tinha perdido order/limit para acomodar um mock pobre — restaurado e o erro agora é checado (postgrest-js nao lanca com >1 linha "dispatched", so devolve PGRST116; ler isso como "ninguem pediu" apagava o pedido em silencio). (2) o invariante "no maximo 1 run dispatched" era so expectativa de aplicacao — virou indice unico parcial de banco (migration 0090: uniq_system_update_runs_dispatched), com dedup defensivo antes da constraint e invariante novo em tests/invariants/system-self-update.test.ts provando que a segunda insercao concorrente lanca. (3) os dois update() do run_result nao checavam erro e finalizavam na ordem errada (limpar o pedido ANTES de fechar o run) — se a segunda escrita falhasse sobrava um run "dispatched" orfao invisivel. Invertido: fecha o run primeiro (erro vira 500 sem auditoria), so depois limpa o pedido (erro so loga — o pior caso vira pedido pendente com run ja concluido, detectavel no proximo heartbeat). O run_progress tambem passou a checar erro. O mock do teste foi reescrito pra compor eq->order->limit->maybeSingle de verdade e simular falha de update por tabela, em vez de aceitar so o shape que o codigo antigo produzia. DESKCOMM_GOV_INVARIANTS_EDIT=1: acrescenta 1 caso a tests/invariants/system-self-update.test.ts (arquivo pre-existente da Task 1) provando o indice unico parcial novo da migration 0090 — nao remove nem enfraquece nenhum invariante existente, so soma cobertura para uma constraint de banco nova desta correcao. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): rotas de estado e de pedido de atualizacao Quem nao e dono do servidor recebe 200 com a versao instalada e nada mais — aviso sem acao disponivel e so ansiedade, e estado operacional nao e assunto de inquilino. O POST nao executa nada: cria o run em `dispatched` e marca o pedido. Essa e a UNICA transicao que o app escreve, porque e o unico instante em que ele sabe com certeza que a ordem saiu. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): unauthenticated no lugar de unauthorized + 500 em vez de 409 numa falha de leitura `unauthorized` no catalogo e reservado ao segredo interno das rotas host<->app; sessao ausente e `unauthenticated`, como as outras 38 rotas do projeto ja usam. Sem essa distincao um frontend que decide por error.code nao reage certo, e o log confunde "credencial de agente invalida" com "sessao expirada". Selects de system_version/system_update_runs que nao checavam o erro deixavam uma falha real de leitura (outage, rede) virar current/latest vazios — e no POST isso caia no 409 otimista "voce ja esta em dia", a pior mensagem possivel na hora errada. Agora viram 500 com logger.error, no mesmo padrao ja usado no INSERT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): heartbeat do agente do host bloqueado pelo proxy de sessao /api/v1/system/agent so aceita bearer token (INTERNAL_SECRET), sem cookie de sessao -- mas o proxy global exigia sessao antes mesmo da rota rodar seu proprio check, devolvendo 401 unauthenticated pra qualquer heartbeat real do agent.sh. Achado provando a Task 6 pela tela (curl do heartbeat batia 401 antes de chegar na rota). Registrado em PUBLIC_PATHS ao lado de /cron/, que tem a mesma forma de auth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): versao instalada no rodape da sidebar Vira aviso clicavel so para quem e dono do servidor E tem versao nova. Quem nao pode atualizar ve apenas o numero em cinza — alertar quem nao pode agir e so ansiedade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * test(update): prova que PUBLIC_PATHS morde isPublicPath decide quem atravessa o proxy sem sessao em toda a aplicacao e nao tinha nenhum teste. Cobre o heartbeat do agente (bearer, sem cookie), a ancora `$` que impede um sub-path de passar de carona, e confirma que /system/update e /system/version continuam exigindo sessao. Sabotado (removi o `$`) para confirmar vermelho pelo nome antes de restaurar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): tela de atualizacao com os quatro estados Pagina em vez de modal: um progresso de dois minutos com o servidor reiniciando no meio nao cabe num modal. Requisicao falhando enquanto ha run em andamento nao e erro — e o app reiniciando; a tela mostra "reiniciando" e reconecta sozinha. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): bloco de atencao nao duplica mais e ganha limpeza de markdown real O body do changelog incluia o proprio bloco de atencao, entao o mesmo aviso aparecia duas vezes na tela (caixa destacada + "O que muda"), e requires_attention nunca passava por limpeza de Markdown. Corrigido na causa em lib/system/changelog.ts: findAttentionRange() devolve o intervalo, nao so o texto, e quem chama exclui esse trecho do body. A limpeza de Markdown (agora markdownParaTextoSimples, testada contra o CHANGELOG.md real) cobre heading, negrito, crase e lista - nao so heading como antes. Achado rodando contra o arquivo real: italico de underscore sem guarda de fronteira de palavra comia os "_" de identificadores citados no changelog (fn_user_org_ids virava fnuserorg_ids), corrigido com a mesma regra do CommonMark. Estados "falhou"/"desfecho desconhecido" agora oferecem o botao de tentar de novo quando ha versao nova disponivel, em vez de deixar a pessoa presa numa tela sem saida. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * feat(update): agente do host e update.sh por tag publicada O agente puxa a ordem em vez de o app empurrar comando: nenhuma porta nova, nenhum socket do Docker, nenhum volume no compose. update.sh passa a instalar a TAG publicada (nao o topo da main) e a sair com codigo != 0 quando o app nao volta saudavel — e esse codigo que dispara a volta para a imagem anterior, guardada antes do pull. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): agente para de morrer calado sob pipefail e run travado expira set -e -o pipefail (herdado de _common.sh) matava o agent.sh em silencio em qualquer substituicao de comando que falhasse antes do relatorio (rede caindo no heartbeat, docker soluçando ao guardar a imagem anterior) — o pior caso: o app ja marcou o run como dispatched e nunca mais ouve falar do agente. Toda substituicao de risco agora tem "|| true" e trata vazio explicitamente; PREV_IMAGE vazio vira log, nao silencio. Escape de JSON passa a cobrir toda a faixa de controle C0 (inclusive ESC, que as proprias c_grn/c_ylw emitem em todo log_tail) e o corte do changelog agora mede o pior caso escapado, nao o bruto. POST /api/v1/system/update expira um run "dispatched" abandonado (>15min, isRunStale ja existente) em vez de recusar pra sempre — e a rede de protecao completa mesmo quando o agente morre por um motivo novo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): esc() vira byte-safe (LC_ALL=C), fecha o ultimo furo de morte silenciosa Sob locale UTF-8 (o padrao em Debian/Ubuntu, inclusive via cron), tr/sed validam a entrada como texto multibyte e abortam com "Illegal byte sequence" ao encontrar um byte invalido — e byte invalido e exatamente o que head -c produz cortando no meio de um caractere. Reproduzido de verdade: o agente morria silenciosamente sob UTF-8 e sobrevivia sob LC_ALL=C — dependia do locale do host, funcionava na maquina de quem testava e morria na do cliente. LC_ALL=C em cada estagio de esc() faz tr/sed tratarem tudo como bytes crus, nunca validando multibyte — a causa, nao o sintoma. BODY e TAIL (o ultimo ainda sem guarda) ganham "|| true" de cinto e suspensorio, e o comentario do topo do arquivo passa a descrever a garantia real em vez de prometer um mecanismo que nem todo lugar usava. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): rodape da sidebar ficava fora do viewport com muitos itens de menu Achado provando o clique real no aviso "Nova versao" (task 9): o <nav> da sidebar nao tinha scroll proprio, entao com todos os itens visiveis (caso do dono da plataforma, que enxerga mais paineis) o rodape era empurrado para fora da altura fixa do <aside>, inalcancavel por um clique de verdade - nao so no Playwright, em qualquer viewport curto. Fix: overflow-y-auto no <nav>, mantendo o rodape fixo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): comando manual fixava a marca e o clique usava clipboard cru Achado rodando pnpm test:unit (invariantes de branding.test.ts e clipboard.test.ts nunca tinham sido exercitados contra este arquivo nas tasks 6-8, que so rodaram vitest em arquivos especificos): 1. COMANDO_MANUAL hardcodava "cd DeskcommCRM" - alem de violar o guard de white-label (nenhuma superficie pode fixar a marca, revendedor usa a dele), o nome nem bate com o default real do instalador (REPO_DIR="deskcommcrm", minusculo, e configuravel). Corrigido removendo o cd: quem tem acesso ao servidor ja entra na pasta do projeto antes de rodar o script. 2. O botao "Copiar" chamava navigator.clipboard.writeText direto - a regra do repo e todo client component passar por copyToClipboard() (lib/clipboard.ts), que tem fallback pra contexto nao-seguro (self-host servindo http://IP puro). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * test(update): prova pela tela e documentacao da atualizacao self-service O E2E mede a ordem visual por ferramenta (boundingBox), nao a olho: o bloco "Requer atencao" tem que vir ANTES do botao, senao quem precisa agir a mao descobre depois de ja ter clicado. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): recusa instalar versao anterior e grava a imagem no .env A ultima tag publicada era mais velha que o codigo de quem segue a main: `git describe --exact-match` vazio fazia a comparacao de tags passar batido, e o update.sh rebobinava a instalacao inteira para a tag antiga — apagando o proprio agente que faz o botao da tela existir. A guarda agora e de ancestralidade (merge-base --is-ancestor), roda ANTES do backup e de qualquer toque no banco, e --force e a saida explicita de quem quer mesmo voltar. O agent.sh aplica o MESMO teste e deixa de anunciar tag ja contida no HEAD, para a tela nunca prometer o que o host vai recusar. APP_IMAGE deixou de ser so `export`: o compose le do .env, entao um `up -d` rodado a mao semanas depois voltava para :latest e desfazia a atualizacao — o modo de falha pelo qual o Watchtower foi descartado no desenho. Novo helper set_env_var grava (sem duplicar chave, mantendo 600) na atualizacao e tambem no rollback, onde a politica de pull vai junto porque a imagem anterior e um ID local. O cron do agente subiu para antes da decisao de versao: ele vivia depois de dois `exit` possiveis, entao quem ja estava em dia — ou agora e recusado — nunca o ganhava, e o bootstrap pelo terminal nao terminava. A linha de cron ganhou `cd` no diretorio do projeto: o agent.sh resolve o projeto pelo CWD, e no cron o CWD e o home (funcionava por acidente so na instalacao padrao). Prova: tests/shell/update-guard.test.sh (pnpm test:shell, no CI) monta um repo git descartavel com docker/crontab dublados — 13 verificacoes, incluindo que o backup nao chega a rodar na recusa e que a chave do .env nao duplica em duas execucoes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): tela para de mentir na falha e passa a ter saida sempre current_version e o `git describe` do HOST, e o checkout ja aconteceu quando o app quebra: a tela dizia "Voltei para a versao anterior (1.1.0)" nomeando a versao que FALHOU, e como latest passava a ser igual a current o botao de tentar de novo sumia — beco sem saida no caso mais comum. A mesma copia ainda era servida para `failed`, onde o rollback NAO aconteceu. Agora a versao exibida e a do app que esta no ar (derivada de run.from_version num rollback), lida antes da bifurcacao por papel para que o rodape da sidebar e a tela nao divirjam. `failed` e `failed_rolled_back` viraram dois textos: um diz que voltou, o outro diz que nao conseguiu voltar. O log_tail — que o agent.sh gasta escape byte-safe e retry de 2 min para entregar e ninguem lia — aparece recolhido nos tres estados ruins. O botao de tentar de novo fica DESLIGADO nas falhas de proposito: o codigo do servidor ja esta na tag nova, entao um novo pedido faria o agente rodar `update.sh --to <mesma tag>`, que responde "ja esta na mais recente", sai com 0 e seria reportado como sucesso — mentira pior que a anterior. Quem resolve ali e --force, e o comando manual (sempre presente) leva a ele. Instalacao fora de uma versao publicada sem tag para oferecer ganhou tela propria: antes caia no bloco final e renderizava "Versao disponivel" com numero vazio e um botao que a API recusa com 409, mais um ponto pulsante permanente na sidebar. Prova pela tela em tests/e2e/system-update.spec.ts (2 casos novos, evidencia em .superpowers/evidence/final-{1,2,3}-*.png). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): o run e a unica fonte de "alguem pediu", e erro de leitura nao vira 404 O POST /update criava o run e marcava update_requested_at num segundo write, sem transacao e sem checar erro: falhando o segundo, a rota respondia 200, a tela mostrava a barra de passos por 15 minutos e o agente nunca via o pedido. Duas fontes de verdade para a mesma pergunta, e o heartbeat porteava justamente na frageil. Agora quem responde "ha pedido?" e o run em dispatched — quem pediu e quando ja vivem no proprio run (requested_by, dispatched_at) e no audit log. Nos ramos run_progress/run_result, o error do select().maybeSingle() passou a ser checado: erro de banco virava 404 e o agente desistiria de reportar um desfecho que aconteceu de verdade. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * docs(update): o bootstrap sao duas execucoes, nao uma O bloco "Requer atencao" prometia uma execucao. A primeira e sempre a do script antigo, que ainda esta no disco: ele baixa o novo (git pull) mas sourceou o _common.sh velho, que nao conhece o cron do agente (conferido em `git show v1.0.0:hostgator-setup-kit/_common.sh`). Quem liga o botao e a segunda. Registrado com o porque em uma frase de leigo, no CHANGELOG, no CLAUDE.md do kit e na propria tela do estado "sem agente". O mapa vivo ganhou os invariantes que a forma nao mostra: a guarda de retrocesso nas duas pontas, a imagem gravada no .env e a fonte unica do pedido. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): clone raso derrubava a guarda de retrocesso, e recusa nao e falha no meio O install.sh instala com `git clone --depth 1`, e num repositorio raso o `merge-base --is-ancestor` responde "nao e ancestral" para tudo que esta fora do unico commit baixado — inclusive para uma tag velha. Ou seja: a resposta que LIBERA o retrocesso era exatamente a que o raso dava de graca, e `git fetch --tags` nao desfaz o raso (medido: is-shallow-repository continua true depois do fetch). A guarda da onda anterior passava batido justamente na unica topologia que o produto entrega. is_already_in_head() passa a ter tres respostas: 0 retrocesso, 1 pode seguir, 2 NAO SEI. Completa a historia (fetch --unshallow) antes de perguntar e, se nao conseguir, devolve 2 — que o update.sh e o agent.sh tratam como recusa, cada um do seu lado. O risco aqui nao e o botao nao funcionar, e o botao quebrar o CRM de quem nao sabe consertar: na duvida, recusa e explica. Recusa ganhou codigo de saida proprio (REFUSED_RC=3). O agent.sh tratava qualquer RC != 0 como "quebrou no meio": reiniciava o container, reescrevia o .env e reportava "failed_rolled_back" — estrago inventado para um run que nao tocou em nada. Agora so ha rollback quando houve o que desfazer. APP_PULL_POLICY volta a "always" numa atualizacao bem-sucedida: depois de um rollback ela ficava "missing" para sempre e o `up -d` manual do dono nunca mais puxava imagem. Prova: tres casos novos no pnpm test:shell — clone raso de verdade (com o HEAD e o .env conferidos antes e depois), clone raso sem conseguir completar a historia, e o agent.sh de verdade contra dubles de curl/docker/flock provando que a recusa nao dispara rollback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): a saida da falha volta para a versao que funcionava, e "a frente" nao e defeito O estado `failed` prometia "para colocar o sistema de volta no ar" e entregava um comando que reinstala exatamente a release que acabou de quebrar: se o problema for a release, e caminho sem volta. A pagina ja tinha `run.from_version` na mao — o comando agora e `--to v<anterior> --force`, e a frase nomeia o destino. Cai no --force puro so quando a versao anterior nao e uma release publicada (nao existe imagem publicada com o nome de uma marca de desenvolvimento). `failed` sem nenhum passo concluido virou estado proprio: o servidor recusou ANTES de tocar em qualquer coisa, entao a tela diz "a atualizacao nao chegou a comecar / nada mudou" em vez de "o sistema pode estar rodando a versao X com defeito" — assustar por nada e o mesmo defeito de afirmar o que nao aconteceu. E "Instalacao fora de uma versao publicada" soava como defeito sendo que ninguem do nosso publico escolheu estar no topo da main: virou "Voce esta a frente da versao publicada — nao ha nada a atualizar", com o tom de quem nao sabe o que e uma tag. Prova pela tela: 4 casos no spec, evidencia em .superpowers/evidence/final-*.png. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * docs(update): registra o furo do clone raso e o codigo de recusa O mapa vivo ganhou os dois invariantes que a forma nao mostra: por que a pergunta de ancestralidade precisa completar a historia antes (e recusar quando nao consegue), e o que o codigo 3 significa na fronteira host-app. O CLAUDE.md do kit explica a recusa por clone raso para quem opera o servidor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(update): o fio host-app ganha "nao sei", e a tela para de adivinhar recusa Duas regressoes que eu introduzi na onda anterior, com a mesma raiz: o host aprendeu a dizer "recusei" e "nao sei", mas o fio host->app nao tinha campo para nenhum dos dois — entao a tela reconstruia essas informacoes a partir de sinais que tambem significam outra coisa. 1. `last_step` nulo NAO quer dizer "o host recusou". run_progress nao tem retry e engole falha; run_result insiste 12x por ~2 min. Uma atualizacao que mexeu em tudo e quebrou no fim chega ao banco com status='failed' e last_step nulo sempre que os POSTs de progresso nao passaram — e a tela nova lia isso como "nada mudou", sem oferecer saida nenhuma, justo no estado em que o dono mais precisa do comando de volta. Ramo removido; o comentario no lugar dele registra o porque. O caso do E2E que afirmava o comportamento errado virou o contrario. 2. A instalacao atrasada era informada de que estava em dia. Com o clone raso sem conseguir completar a historia, o agente deixava de anunciar a tag e a tela lia o silencio como boa noticia: "Voce esta na versao 0.9.0 — e a mais recente", enquanto uma v1.1.0 esperava. Recusar agir e defensavel; afirmar o que nao se conferiu nao e, e o custo e o cliente nunca receber a proxima correcao de seguranca. Agora o "nao sei" viaja explicito: compare_failed no heartbeat (Zod opcional com default — agente antigo nao pode passar a levar 422 e ficar mudo), coluna nova em system_version (migration 0093 + apendice no baseline + MANIFEST) e tela propria dizendo que nao da pra afirmar nem uma coisa nem outra. 3. Nit: no failed_rolled_back a frase prometia "tentar de novo" e o comando so rebobina. A frase agora descreve o que o comando faz. Prova: 2 casos novos no pnpm test:shell (o agente de verdade num clone raso atrasado, e a saida que nao pode sumir sem passo reportado), 3 testes de rota, e 5 specs de tela. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf * fix(atualizacao): distingue fork sem release de instalação à frente da publicada Sem release nenhuma conhecida (fork novo), a tela dizia "à frente da versão publicada" — afirmando a existência de algo que não existe. Novo campo `has_known_release` (contrato agent.sh -> heartbeat -> tela) resolve a ambiguidade; default `true` em toda camada (coluna, Zod, GET) preserva o comportamento anterior para agente antigo que ainda não envia o campo. Também isola as duas provas do heartbeat "não sei comparar" no shell test (CONTIDA=2 do unshallow vs. fallback sem tag nenhuma), que antes viviam misturadas no mesmo caso e não provavam cada linha isoladamente. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ng41aCYobTn37KrN2V7BZf --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |