Varredura de "afirmações de estado": toda frase que afirma como o mundo ESTÁ e
que portanto pode ter envelhecido. 393 afirmações medidas contra a fonte em 11
grupos de documento, cada uma com o comando que a responde. 166 confirmadas, 227
com problema, 4 vereditos VERDADEIRA derrubados por um passe adversarial.
E a primeira coisa que a varredura derrubou foi uma frase que EU escrevi hoje.
## O pior achado, e é meu
O runbook dizia "**Desde a 1.3.0**, o agente completa o pin sozinho — em até 5
minutos". Falso. O `completar_pin_ausente` entrou em `81b3bd5d` (2026-08-14),
POSTERIOR à v1.3.0 (2026-08-13), e a v1.3.0 continua sendo a tag mais recente:
$ git show v1.3.0:hostgator-setup-kit/_common.sh | grep -c completar_pin_ausente
0
Nenhuma instalação existente tem esse comportamento. E o `update.sh` faz
`git checkout "$TARGET_TAG"` — o kit que roda na VPS é o da TAG, não o da `main`.
Quem atendesse um cliente lendo aquela linha diria "espere cinco minutos que se
resolve", e nada aconteceria, para sempre.
O erro é exatamente o que esta sessão passou o dia caçando: **provei presença na
main e afirmei comportamento na versão publicada.** A mesma frase estava no
CHANGELOG, e pior — dentro da seção `## [1.3.0]`, não em `[Não lançado]`. As duas
corrigidas, e a entrada foi para onde pertence.
## O segundo: o aviso que declarava não-provado o que o documento prova
O topo do runbook dizia "ainda não coberto: app contra Supabase real, sessão de
WhatsApp pareada". O §P4 do MESMO arquivo, 340 linhas abaixo, documenta as duas
coisas na produção. Faltava uma palavra — "pelo ENSAIO" — e sem ela o primeiro
parágrafo que qualquer leitor vê mandava refazer trabalho já feito.
## Três documentos mandavam o leitor agir errado
- **`triagem/TRIAGEM.md`**: "Obrigatórios no merge: verify, build-and-size,
invariants" — três de cinco, faltando `e2e` e `imagens-ok`. É o arquivo que
DEFINE a triagem, e o CLAUDE.md registra que medir contra a régua errada é o
modo de falha número um dela. Agora traz o comando, não a lista.
- **`docs/deploy-hostgator/README.md`**: mandava o comprador leigo instalar um
autenticador e esperar um QR de MFA "no primeiro login". Com o MFA opcional, o
`bootstrap-owner.ts` grava `mfa_required: false` e essa tela não aparece — o
leigo concluiria que a instalação falhou. É o guia P0 de primeira impressão.
- **`ARCHITECTURE.md`**: "MFA TOTP forçado pra admin/super-admin", a regra antiga.
O CLAUDE.md já tinha a nova; a porta de entrada linkada pelo README, não.
## A régua de RAM: duas parcelas medidas, uma herdada
"~150 MB por número de WhatsApp" aparece em SETE documentos que se citam entre si
e **nunca foi medido neste projeto** — vem da síntese do curso WAHA (2026-05). Fui
medir na produção: o contêiner `waha` inteiro em **304,5 MiB com uma sessão
pareada**, contra `mem_limit` de 1280. Um ponto não decompõe baseline e sessão.
A parcela agora está marcada como herdada, com o comando para quem quiser medir.
**O tier recomendado não muda** — a régua dos 4 GB é a soma da stack em operação,
não o WAHA isolado, e os materiais comerciais não foram tocados.
## Segurança: corrigi o fato, não rebaixei o risco
O `docs/threat-model.md` afirma que não há limite de tentativa em login, signup e
aceite de convite. `lib/auth/rate-limit.ts` existe desde 13/08 e cobre os quatro
pontos, com limites nomeados. **Não rebaixei o T1**: isso pede reauditoria com
teste contra instância viva, que o próprio `confidence` do documento diz nunca ter
havido. Corrigi os fatos e marquei a reauditoria como devida.
Uma sub-afirmação sobrevive à letra e morre no espírito, e ficou registrada: a
sonda do doc (`grep lockout|failed_attempts`) devolve ZERO ainda hoje — mas
`rate-limit.ts:158` conta falha de login POR CONTA. A sonda é cega para a defesa
que existe.
## O gate novo, e ele nasceu vermelho pelo motivo certo
`documentacao-aponta-para-o-que-existe.test.ts` vigia a classe inteira nos 36
documentos de AUTORIDADE: link relativo morto, path em crase que não existe, e
frase de pendência que sobreviveu à pendência.
Escopo deliberado: `docs/stories/`, `docs/handoffs/` e `docs/specs/` ficam de fora
— são planejamento, e incluí-los faria o gate nascer com 384 violações e ser
desligado na primeira semana. Nos 36 de autoridade havia UMA, e ela é honesta (o
runbook cita o script e declara que ele não existe): congelada com justificativa.
Ele nasceu vermelho e estava certo — pegou sozinho duas notas de pendência que a
varredura tinha achado e eu ainda não corrigira, em CONTRIBUTING.md e na doutrina.
Quatro sabotagens, cada uma derrubando exatamente 1 dos 4 — inclusive a do escopo
vazio, porque um gate que varre zero arquivo passa verde. Sabotei DEPOIS de deixar
o controle verde: na primeira tentativa o controle já falhava, e as sabotagens não
provavam nada. E `git add -N` antes, porque `git checkout` não restaura arquivo
que o git ainda não conhece — a sabotagem do escopo ficou aplicada e só apareceu
no run seguinte.
## O que NÃO entra aqui
227 achados; apliquei os de gravidade alta cuja consequência é alguém agir errado.
Os de gravidade média e baixa — sobretudo contagens que envelheceram em
`docs/current-state.md` e `docs/harness-audit.md` — ficam listados no relatório,
não corrigidos. Aplicar 227 correções de texto num commit seria trocar prosa velha
por prosa não verificada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
6.0 KiB
Runbook — Deploy em produção (VPS)
O caminho normal de deploy não constrói nada na VPS: o CI publica a imagem no GHCR e a VPS só puxa. Construir localmente é exceção de emergência, e tem custo — está documentado no fim.
1. O comando
cd /var/www/crm
docker compose -f docker-compose.prod.yml -f docker-compose.traefik.yml --env-file .env up -d app
Os DOIS -f são obrigatórios. Sempre.
Esta é a pegadinha que já derrubou o site inteiro em produção (2026-08-05).
A VPS (Hostinger) vem com um Traefik próprio ocupando as portas 80/443.
docker-compose.traefik.yml é o ÚNICO lugar que:
- coloca no contêiner
appas labels de roteamento (traefik.http.routers.deskcomm.rule=Host(...)); - associa o contêiner à rede que o Traefik enxerga (
TRAEFIK_DOCKER_NETWORK); - desliga o
caddydo compose base por profile (senão dois processos brigam pela mesma porta).
Rodar só com -f docker-compose.prod.yml recria o contêiner sem labels
nenhuma. O Traefik deixa de enxergá-lo e o domínio inteiro passa a responder
404 page not found — não é erro do Next, é o 404 genérico do Traefik. A app
está no ar, saudável, e inalcançável.
2. Verificação pós-deploy (não pule)
healthy no docker ps não prova que o site está acessível — o healthcheck
é um probe TCP interno e passa mesmo com o roteamento quebrado. Verifique as
duas coisas:
# 1) as labels do Traefik existem?
# O nome do contêiner é <pasta-do-projeto>-app-1, então pergunte ao compose
# em vez de chutar. Aqui um -f só basta: o `ps -q` resolve pelo nome do
# projeto + serviço, não pelo conteúdo do arquivo (medido: com um -f ou com
# os dois, devolve o MESMO contêiner). Quem precisa dos dois é o `up -d`.
docker inspect "$(docker compose -f docker-compose.prod.yml ps -q app)" \
--format '{{.Config.Labels}}' | grep -o 'traefik.enable:[^ ]*'
# esperado: traefik.enable:true (vazio = roteamento quebrado)
# 2) o domínio responde?
curl -s -o /dev/null -w "%{http_code}\n" https://<DOMAIN>/
# esperado: 307 (redireciona pro login)
# 404 = labels perdidas, refaça o deploy com os dois -f
3. Fluxo completo (do código à produção)
commit → push → PR → merge na main → CI publica imagem → VPS puxa
- Commit + push numa branch de feature. Trabalho que fica só no disco da VPS não existe: o CI não o vê, some se a VPS for reconstruída, e é invisível pra qualquer outra pessoa.
- PR e merge na
main.publish-image.ymldispara em push namain(ou tagv*) e publica três imagens —deskcommcrm,deskcomm-workeredeskcomm-scheduler— sempre na mesma versão. O build pesado roda nos runners do GitHub, nunca na VPS do usuário. - Deploy na VPS. Numa instalação real isto é
bash hostgator-setup-kit/update.sh, não umup -dna mão: ele puxa a tag publicada, re-aplica obaseline.sql, faz backup antes e grava as três imagens no.env.
latestnão é a última release. Ele é publicado a partir da branch default, então segue o topo damain— código ainda não lançado. Quem quer a última release usastable; quem opera um cliente usa o número da versão. Ver../doctrine/packaging.md.
4. Exceção: imagem construída na VPS
Só quando é preciso validar algo em produção antes de a imagem oficial existir (ex.: CI ainda rodando e um bug bloqueando o usuário).
APP_IMAGE=deskcomm-app:local docker compose \
-f docker-compose.prod.yml -f docker-compose.build.yml --env-file .env build app
APP_IMAGE=deskcomm-app:local APP_PULL_POLICY=never docker compose \
-f docker-compose.prod.yml -f docker-compose.traefik.yml --env-file .env up -d app
O docker-compose.build.yml também cobre worker e scheduler — troque
app pelo serviço que você precisa construir. Eles têm build: no próprio
compose de produção (é o escape que faz a instalação sobreviver a um registry
fora do ar), mas é o override que traz o pull_policy: never; sem ele o
up -d volta a buscar a imagem publicada.
Isto é dívida, não um caminho paralelo. A imagem existe só no disco daquela
VPS: não está no registry, não está no git, e qualquer docker compose up -d
sem APP_PULL_POLICY=never a substitui pela do GHCR — silenciosamente, sem erro
nenhum, revertendo o que você acabou de subir.
Requisitos: >= 4 GB de RAM ou swap (medido: ~4min num VPS de 3.8 GB com 4 GB de swap) — e isto é o requisito deste caminho de exceção, não da operação normal. A régua de operação é outra, e não mudou. Ela tem três parcelas, e duas são medidas e uma é herdada — a distinção importa porque a herdada é a que costuma ser citada como se fosse nossa:
| parcela | estado | como conferir |
|---|---|---|
| 7 contêineres | medido | docker compose -f docker-compose.prod.yml config --services | wc -l |
mem_limit somando 2560m (app 768 + worker 512 + waha 1280) |
medido | grep -n 'mem_limit' docker-compose.prod.yml |
| ~150 MB por número de WhatsApp | herdado do upstream WAHA, nunca medido neste projeto | docker stats --no-stream na sua VPS |
O terceiro número vem de docs/research/reference-synthesis.md (síntese do curso
WAHA, 2026-05), não de uma medição nossa — e circula em sete documentos que se
citam entre si. Uma medição pontual na produção do projeto (2026-08-14, uma
sessão pareada, VPS compartilhada com outras stacks) deu 304,5 MiB no contêiner
waha inteiro, contra o mem_limit de 1280 MiB. Um ponto não decompõe baseline
e sessão: para isso seriam necessários dois números pareados, e não é ensaio que
se faça numa instalação viva.
Nada disso mexe no tier recomendado. A régua que sustenta os 4 GB é a soma da stack em operação, não o WAHA isolado — e a folga existe justamente porque a parcela por sessão não é conhecida com precisão.
Ao terminar, feche o ciclo — merge na main e volte a VPS pra imagem oficial.