Files
Paulo Lima JrandClaude Opus 5 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 42720a4bf, por outro caminho: ela relê o
  `_common.sh` depois do checkout em vez de fazer `exec`. O re-exec e o caso 12 de
  update-guard que o provava ficaram de fora — entregar o mesmo conserto duas
  vezes, em dois desenhos que brigam, é pior que não entregar.
- O log do banco sai pelo segundo argumento de `reaplicar_baseline`, que a `main`
  já tinha e que guarda TODAS as passadas, em vez da captura crua da última.
- Duas sondas foram reancoradas porque o alvo se moveu na `main`, não porque
  afrouxaram: `dc up -d` hoje vive sob a guarda que reconstrói em VPS de outra
  arquitetura, e o `psql -f /b.sql` mudou-se para dentro de `reaplicar_baseline`.
  As duas estavam medindo -1 contra um update.sh correto.

Prova: `tests/shell/atualizacao-para-quem-fala-com-o-banco.test.sh` (46 casos,
com dublê de docker — nada sobe nem para de verdade), ligado ao `pnpm test:shell`,
e `tests/unit/atualizacao-confere-regras-de-isolamento.test.ts` (19 casos, que
executam o AWK extraído do próprio update.sh, nunca uma cópia).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 11:25:27 -03:00

125 lines
6.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# O aviso que assume a porta enquanto o CRM esta parado para mexer no banco.
#
# ## Por que um conteiner separado, e nao um "modo manutencao" dentro do app
#
# Porque o app e justamente quem precisa PARAR. Medido na instalacao real: com
# ele de pe, 60 travamentos ao aplicar o baseline; com ele parado e mais tres
# pecas do Supabase, zero. Um modo dentro dele exigiria mante-lo no ar, que e o
# oposto do conserto.
#
# E nao ha `middleware.ts` no projeto (medido em 2026-09-12). Criar um so para
# isto poria uma peca no caminho de TODA requisicao do produto para resolver uma
# janela de poucos minutos por atualizacao.
#
# ## As DUAS hospedagens, e por que o caminho e diferente em cada uma
#
# O kit atende dois arranjos de proxy, e eles nao se parecem:
#
# traefik → o proxy da hospedagem LE LABELS dos conteineres. O aviso sobe com
# as mesmas regras de roteamento do app e prioridade maior, e o
# proxy passa a entregar o aviso sem ninguem editar nada.
#
# caddy → o proxy e NOSSO, e a regra dele mora num arquivo dentro do
# conteiner (`reverse_proxy app:3000` no Caddyfile). Editar esse
# arquivo no meio de uma atualizacao e mexer em configuracao que o
# dono pode ter customizado. O caminho aqui e outro: o aviso entra
# na rede interna com o APELIDO `app` enquanto o CRM esta parado, e
# atende em 3000. O Caddy procura no mesmo lugar de sempre e acha
# quem responder.
#
# ⚠️ O apelido e o motivo de `manutencao_desce` vir ANTES de `dc up -d app`, e
# nao depois. Com os dois de pe ao mesmo tempo, o Docker faz rodizio entre eles:
# metade das pessoas veria "estamos atualizando" com o CRM ja no ar. A janela
# que se abre por vir antes dura o `docker rm` — e, nela, o que aparece e o
# mesmo 502 que aparecia o tempo todo antes desta pagina existir.
#
# ## O que foi MEDIDO no compose, contra o que o plano dizia
#
# O plano desta onda escrevia `${APP_DOMAIN}` e `tls=true`. As duas estao
# erradas e as duas quebrariam a instalacao:
#
# - a variavel do dominio e `DOMAIN` (docker-compose.traefik.yml). `APP_DOMAIN`
# nao existe em lugar nenhum do kit, e sob `set -u` a atualizacao morreria
# na linha — com o CRM ja parado;
# - `tls=true` sem `certresolver` faz o Traefik servir o certificado interno
# dele. Quem abrisse o CRM durante a atualizacao veria um aviso de site
# inseguro, que e pior que o erro de conexao que ja via.
# ## O TERCEIRO ARRANJO — medido na nossa VPS, depois de a pagina NAO aparecer
#
# A primeira versao disto cobria dois arranjos e falhou no unico que a gente tem.
# A atualizacao para a v1.17.21 rodou, a pagina subiu, os rotulos estavam certos,
# a rede estava certa — e quem abrisse o CRM viu o erro do navegador do mesmo
# jeito, por 87 segundos. Medido com sonda de 3 em 3 segundos.
#
# A causa: NAO EXISTE TRAEFIK nesta VPS. O `.env` diz `REVERSE_PROXY=traefik`,
# mas quem atende 80/443 e o nginx DO HOST
# (/etc/nginx/sites-enabled/thoth-crm.conf), com `proxy_pass 127.0.0.1:3000`; e
# quem atende ali e uma ponte `socat tcp-listen:3000,fork tcp-connect:app:3000`.
# Ninguem le rotulo de conteiner. O `REVERSE_PROXY` do .env descreve com qual
# compose subir, NAO quem roteia de fora — e eu tratei os dois como a mesma coisa.
#
# O conserto e o apelido, e ele serve aos TRES arranjos: quem roteia por rotulo
# ignora o apelido; quem procura por NOME (o Caddy do kit, a ponte socat,
# qualquer proxy_pass para um nome de conteiner) acha o aviso onde ja procurava.
# E o `fork` do socat resolve o nome A CADA CONEXAO, entao a troca vale na hora.
#
set -euo pipefail
NOME_DA_MANUTENCAO="deskcomm-manutencao"
manutencao_sobe() {
local html="$KIT_DIR/manutencao"
[ -d "$html" ] || return 0
docker rm -f "$NOME_DA_MANUTENCAO" >/dev/null 2>&1 || true
# ⚠️ O APELIDO `app` VEM SEMPRE, em qualquer arranjo — e foi uma medicao na VPS
# real que ensinou isso. Ver "O TERCEIRO ARRANJO", no cabecalho.
local args=(
-d --name "$NOME_DA_MANUTENCAO"
--network-alias app
-v "$html/index.html:/usr/share/nginx/html/index.html:ro"
-v "$html/nginx.conf:/etc/nginx/conf.d/default.conf:ro"
)
if [ "${REVERSE_PROXY:-caddy}" = "traefik" ]; then
local rede="${TRAEFIK_NETWORK:-traefik}"
local ep="${TRAEFIK_ENTRYPOINT:-websecure}"
local ep_http="${TRAEFIK_ENTRYPOINT_HTTP:-web}"
local cr="${TRAEFIK_CERTRESOLVER:-letsencrypt}"
local dom="${DOMAIN:-}"
# Sem dominio nao ha rota possivel. Calar aqui e melhor que subir um
# conteiner que nunca sera alcancado e que alguem tera de limpar a mao.
[ -n "$dom" ] || return 0
args+=(
--network "$rede"
--label "traefik.enable=true"
--label "traefik.docker.network=$rede"
# Prioridade 500: acima de qualquer regra `Host(...)` (a prioridade padrao
# do Traefik e o TAMANHO da regra) e abaixo do 1000 do bloqueio do webhook
# global do WAHA, que continua tendo de barrar durante a atualizacao.
--label "traefik.http.routers.deskcomm-manutencao.rule=Host(\`$dom\`)"
--label "traefik.http.routers.deskcomm-manutencao.entrypoints=$ep"
--label "traefik.http.routers.deskcomm-manutencao.tls.certresolver=$cr"
--label "traefik.http.routers.deskcomm-manutencao.priority=500"
--label "traefik.http.routers.deskcomm-manutencao.service=deskcomm-manutencao"
# O redirecionamento http→https vive nos labels do APP, e o app esta
# parado — o Traefik so enxerga conteiner em execucao, entao aquela regra
# some junto. Por isso o aviso atende nas duas portas em vez de redirecionar.
--label "traefik.http.routers.deskcomm-manutencao-http.rule=Host(\`$dom\`)"
--label "traefik.http.routers.deskcomm-manutencao-http.entrypoints=$ep_http"
--label "traefik.http.routers.deskcomm-manutencao-http.priority=500"
--label "traefik.http.routers.deskcomm-manutencao-http.service=deskcomm-manutencao"
--label "traefik.http.services.deskcomm-manutencao.loadbalancer.server.port=3000"
)
else
args+=(--network "$(nome_do_projeto_atual)_internal")
fi
docker run "${args[@]}" nginx:alpine >/dev/null 2>&1 || true
}
manutencao_desce() {
docker rm -f "$NOME_DA_MANUTENCAO" >/dev/null 2>&1 || true
}