mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
docs: a Vercel deixa de aparecer como onde o CRM roda e valida
O projeto do CRM na Vercel foi desvinculado do GitHub por decisao do dono: a plataforma fica so com a landing page, em outro repositorio. Medido com a mesma sonda nos dois lados — `gh api repos/.../commits/<sha>/status` devolve `Vercel` no commitf65d04667(antes) e vazio ema8c9e1ad3,7168961ade no head do #1081 (depois). Com isso, dezenas de afirmacoes do repositorio publico viraram falsas. As que falavam no presente sairam: - mensagem automatica ao contribuinte, molde de PR, CONTRIBUTING e os espelhos na skill de contribuir prometiam um check "Vercel" vermelho que nao aparece mais. No lugar, a mensagem passa a dizer onde olhar o que trava o merge — os checks marcados Required no proprio PR, com a ressalva de que `--required` so lista os que ja reportaram; - runbooks de operacao mandavam abrir o painel da plataforma e redeployar a main; passam a descrever o `.env` da instalacao e a recriacao dos conteineres; - `lib/env.ts` mandava, no boot, ajustar variavel "na Vercel"; - specs e PRDs descreviam topologia, crons e guarda de segredo na plataforma; - comentarios de codigo ancoravam decisoes vivas em premissa morta. Registros datados (pesquisa, pitch, epicos, handoffs) nao foram reescritos: ganharam uma linha dizendo que sao registro da fase hospedada. O `vercel.ts` fica: ele serve quem hospeda um fork, e o gate `tests/unit/cron-routes-scheduled.test.ts` continua reprovando divergencia de rotas entre ele e o crontab do scheduler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: deskcomm-contribuir
|
||||
description: 'Guia de contribuição ao DeskcommCRM para quem vai mexer no código e abrir um pull request, sobretudo de um fork. Use SEMPRE que a pessoa disser que vai contribuir, corrigir um bug, implementar algo, abrir ou atualizar um PR, criar uma migration, resolver conflito com a main, ou perguntar "como eu testo isso", "minha branch está atrasada?", "por que o CI ficou vermelho", "o Vercel falhou" — e antes de qualquer commit em clone que não seja do mantenedor. É o espelho da triagem: mede ANTES do PR o que o mantenedor mede depois (branch atrasada, tripla de migration, marca do fork no diff, fragmento de release, teste que falta, prova em tela), arma os hooks de git e evita retrabalho e PR recusado.'
|
||||
description: 'Guia de contribuição ao DeskcommCRM para quem vai mexer no código e abrir um pull request, sobretudo de um fork. Use SEMPRE que a pessoa disser que vai contribuir, corrigir um bug, implementar algo, abrir ou atualizar um PR, criar uma migration, resolver conflito com a main, ou perguntar "como eu testo isso", "minha branch está atrasada?", "por que o CI ficou vermelho" — e antes de qualquer commit em clone que não seja do mantenedor. É o espelho da triagem: mede ANTES do PR o que o mantenedor mede depois (branch atrasada, tripla de migration, marca do fork no diff, fragmento de release, teste que falta, prova em tela), arma os hooks de git e evita retrabalho e PR recusado.'
|
||||
metadata:
|
||||
publico: contribuidor externo, dev de agência, fork
|
||||
espelho-de: triagem/TRIAGEM.md
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
## O que vai parecer erro e não é
|
||||
|
||||
- **`Vercel` vermelho** — "Authorization required to deploy". A `main` faz deploy de produção e a
|
||||
Vercel recusa construir PR de fork. **Não entra no gate de merge.** Ignore.
|
||||
- **Workflows parados "esperando aprovação"** — política do GitHub no primeiro PR de quem nunca
|
||||
contribuiu. Um mantenedor libera; do segundo PR em diante roda sozinho. Se demorar mais que um
|
||||
dia útil, comente no PR.
|
||||
|
||||
@@ -9,7 +9,7 @@ histórico dos PRs de fork (10 dos ~40 mais recentes fechados sem merge). Ordem
|
||||
| 2 | **Config ou marca do próprio fork dentro do PR** — nome do cliente, logo, `.env`, compose editado, imagens publicadas no namespace do fork | #465 (7 arquivos com a marca de um cliente mergeando sem conflito), #518, #596, #600, #605 | branch nomeada a partir de `origin/main`, nunca do `main` do fork; item "marca/config" do pré-voo; a marca é tela, não código |
|
||||
| 3 | **Seção de versão `## [x.y.z]` escrita à mão no CHANGELOG** — o corte é automático e a seção manual quase publicou uma versão pelo merge | #354, #518, #588 | fragmento em `.changes/`; item "CHANGELOG" do pré-voo |
|
||||
| 4 | **PR aberto do `main` do fork** (traz tudo que a instalação da pessoa tem) | #418, #465 | `git switch -c fix/… origin/main` |
|
||||
| 5 | **Autor fecha o próprio PR** achando que "fez ruído" ou "abriu no lugar errado" (6 vezes; um fechou 36 segundos depois de abrir, levando junto um bug real) | #515, #547, #588 | nunca feche; o Vercel vermelho e os workflows parados são esperados (`depois-do-pr.md`) |
|
||||
| 5 | **Autor fecha o próprio PR** achando que "fez ruído" ou "abriu no lugar errado" (6 vezes; um fechou 36 segundos depois de abrir, levando junto um bug real) | #515, #547, #588 | nunca feche; workflow parado esperando liberação é o normal no primeiro PR (`depois-do-pr.md`) |
|
||||
| 6 | **Commit assinado como `root@vps` ou com nome alheio** — o trabalho some do perfil | #569-#571 (`root@vpsbr-…`), #352 ("SoftIA Backup Agent") | `git config user.email`; hook avisa; `.mailmap` só credita com prova |
|
||||
| 7 | **Mudança de comportamento sem teste**, ou teste que não fica vermelho quando o conserto sai (uma segunda porta sem guarda) | #474 e vários | passe 5 do guia: teste + sabotagem com contagem prevista |
|
||||
| 8 | **Funciona em instalação nova e quebra em quem já usa** — constraint nova sobre dados existentes; env obrigatória nova sem default | relato 13 da triagem | corrigir dados antes da constraint; env com default; fragmento `exige_acao` quando não há saída |
|
||||
@@ -22,7 +22,6 @@ histórico dos PRs de fork (10 dos ~40 mais recentes fechados sem merge). Ordem
|
||||
|
||||
## O que NÃO é erro seu (e já assustou gente)
|
||||
|
||||
- `Vercel` vermelho com "Authorization required to deploy" — esperado em fork; não entra no gate.
|
||||
- Workflows "aguardando aprovação" no primeiro PR — política do GitHub; um mantenedor libera.
|
||||
- Vermelho em arquivo que você não tocou: `Test timed out in 15000ms` em dezenas de arquivos é
|
||||
saturação da máquina (`uptime`); `address already in use` é o runner; leia o log do **passo**,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
impacto: nada_mudou
|
||||
secao: corrigido
|
||||
titulo: A documentação deixou de dizer que o sistema roda numa plataforma que ele não usa
|
||||
---
|
||||
|
||||
Vários textos do projeto — o guia de quem contribui, os runbooks de operação, as especificações e até uma mensagem de erro do próprio sistema — afirmavam que o CRM era publicado e testado numa plataforma de hospedagem gerenciada. Isso deixou de ser verdade: o produto é instalado na sua própria infraestrutura, e é lá que ele opera.
|
||||
|
||||
Nada muda no que você roda hoje. O que muda é o que você lê: a mensagem que aparece quando falta uma variável agora manda ajustar o `.env` da instalação, em vez de um painel que você não tem; os procedimentos de trocar chave do WhatsApp e de rotacionar credenciais passam a descrever o `.env` e a recriação dos contêineres; e o texto que quem contribui recebe ao abrir um pedido de mudança deixa de anunciar um resultado de verificação que não existe mais, e diz onde olhar o que de fato trava a entrada do código.
|
||||
|
||||
O agendador de tarefas para quem instala sem cron próprio continua existindo e funcionando igual — só deixou de ser descrito como coisa de uma plataforma específica.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: deskcomm-contribuir
|
||||
description: 'Guia de contribuição ao DeskcommCRM para quem vai mexer no código e abrir um pull request, sobretudo de um fork. Use SEMPRE que a pessoa disser que vai contribuir, corrigir um bug, implementar algo, abrir ou atualizar um PR, criar uma migration, resolver conflito com a main, ou perguntar "como eu testo isso", "minha branch está atrasada?", "por que o CI ficou vermelho", "o Vercel falhou" — e antes de qualquer commit em clone que não seja do mantenedor. É o espelho da triagem: mede ANTES do PR o que o mantenedor mede depois (branch atrasada, tripla de migration, marca do fork no diff, fragmento de release, teste que falta, prova em tela), arma os hooks de git e evita retrabalho e PR recusado.'
|
||||
description: 'Guia de contribuição ao DeskcommCRM para quem vai mexer no código e abrir um pull request, sobretudo de um fork. Use SEMPRE que a pessoa disser que vai contribuir, corrigir um bug, implementar algo, abrir ou atualizar um PR, criar uma migration, resolver conflito com a main, ou perguntar "como eu testo isso", "minha branch está atrasada?", "por que o CI ficou vermelho" — e antes de qualquer commit em clone que não seja do mantenedor. É o espelho da triagem: mede ANTES do PR o que o mantenedor mede depois (branch atrasada, tripla de migration, marca do fork no diff, fragmento de release, teste que falta, prova em tela), arma os hooks de git e evita retrabalho e PR recusado.'
|
||||
metadata:
|
||||
publico: contribuidor externo, dev de agência, fork
|
||||
espelho-de: triagem/TRIAGEM.md
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
## O que vai parecer erro e não é
|
||||
|
||||
- **`Vercel` vermelho** — "Authorization required to deploy". A `main` faz deploy de produção e a
|
||||
Vercel recusa construir PR de fork. **Não entra no gate de merge.** Ignore.
|
||||
- **Workflows parados "esperando aprovação"** — política do GitHub no primeiro PR de quem nunca
|
||||
contribuiu. Um mantenedor libera; do segundo PR em diante roda sozinho. Se demorar mais que um
|
||||
dia útil, comente no PR.
|
||||
|
||||
@@ -9,7 +9,7 @@ histórico dos PRs de fork (10 dos ~40 mais recentes fechados sem merge). Ordem
|
||||
| 2 | **Config ou marca do próprio fork dentro do PR** — nome do cliente, logo, `.env`, compose editado, imagens publicadas no namespace do fork | #465 (7 arquivos com a marca de um cliente mergeando sem conflito), #518, #596, #600, #605 | branch nomeada a partir de `origin/main`, nunca do `main` do fork; item "marca/config" do pré-voo; a marca é tela, não código |
|
||||
| 3 | **Seção de versão `## [x.y.z]` escrita à mão no CHANGELOG** — o corte é automático e a seção manual quase publicou uma versão pelo merge | #354, #518, #588 | fragmento em `.changes/`; item "CHANGELOG" do pré-voo |
|
||||
| 4 | **PR aberto do `main` do fork** (traz tudo que a instalação da pessoa tem) | #418, #465 | `git switch -c fix/… origin/main` |
|
||||
| 5 | **Autor fecha o próprio PR** achando que "fez ruído" ou "abriu no lugar errado" (6 vezes; um fechou 36 segundos depois de abrir, levando junto um bug real) | #515, #547, #588 | nunca feche; o Vercel vermelho e os workflows parados são esperados (`depois-do-pr.md`) |
|
||||
| 5 | **Autor fecha o próprio PR** achando que "fez ruído" ou "abriu no lugar errado" (6 vezes; um fechou 36 segundos depois de abrir, levando junto um bug real) | #515, #547, #588 | nunca feche; workflow parado esperando liberação é o normal no primeiro PR (`depois-do-pr.md`) |
|
||||
| 6 | **Commit assinado como `root@vps` ou com nome alheio** — o trabalho some do perfil | #569-#571 (`root@vpsbr-…`), #352 ("SoftIA Backup Agent") | `git config user.email`; hook avisa; `.mailmap` só credita com prova |
|
||||
| 7 | **Mudança de comportamento sem teste**, ou teste que não fica vermelho quando o conserto sai (uma segunda porta sem guarda) | #474 e vários | passe 5 do guia: teste + sabotagem com contagem prevista |
|
||||
| 8 | **Funciona em instalação nova e quebra em quem já usa** — constraint nova sobre dados existentes; env obrigatória nova sem default | relato 13 da triagem | corrigir dados antes da constraint; env com default; fragmento `exige_acao` quando não há saída |
|
||||
@@ -22,7 +22,6 @@ histórico dos PRs de fork (10 dos ~40 mais recentes fechados sem merge). Ordem
|
||||
|
||||
## O que NÃO é erro seu (e já assustou gente)
|
||||
|
||||
- `Vercel` vermelho com "Authorization required to deploy" — esperado em fork; não entra no gate.
|
||||
- Workflows "aguardando aprovação" no primeiro PR — política do GitHub; um mantenedor libera.
|
||||
- Vermelho em arquivo que você não tocou: `Test timed out in 15000ms` em dezenas de arquivos é
|
||||
saturação da máquina (`uptime`); `address already in use` é o runner; leia o log do **passo**,
|
||||
|
||||
+5
-3
@@ -18,9 +18,11 @@ SUPABASE_SERVICE_ROLE_KEY=
|
||||
INTERNAL_SECRET=
|
||||
# Secret dedicado opcional pros endpoints de cron. Se vazio, cai pra INTERNAL_SECRET.
|
||||
INTERNAL_CRON_SECRET=
|
||||
# Só Vercel Cron (plano Pro). A Vercel manda Bearer CRON_SECRET; em produção o
|
||||
# app copia esse valor para INTERNAL_CRON_SECRET. Sem a variável, o cron nativo
|
||||
# chega sem Authorization e as rotas respondem 403. Self-host ignora.
|
||||
# CRON_SECRET não é chave deste arquivo: é o nome com que o Vercel Cron injeta o
|
||||
# Bearer, e vale para quem hospeda um fork lá. Em produção, se ela existir,
|
||||
# lib/env.ts copia o valor para INTERNAL_CRON_SECRET. Quem usa o serviço
|
||||
# `scheduler` do docker-compose NÃO precisa dela: lá o Bearer é o INTERNAL_SECRET
|
||||
# (docker/scheduler/entrypoint.sh).
|
||||
# Relógio local (`pnpm dev:crons`). Só bate em NEXT_PUBLIC_APP_URL; não instala
|
||||
# crontab. NÃO rode contra o mesmo Supabase da VPS — disputa a fila com o prod.
|
||||
# DEV_CRON_INTERVAL_MS=15000
|
||||
|
||||
@@ -18,9 +18,8 @@
|
||||
|
||||
### 📌 Contribuindo de um fork? Você está no lugar certo.
|
||||
|
||||
**Duas coisas vão parecer erro seu e não são** — e nenhuma é motivo para fechar o PR:
|
||||
**Uma coisa vai parecer erro seu e não é** — e ela não é motivo para fechar o PR:
|
||||
|
||||
- **`Vercel` vermelho** (`Authorization required to deploy`): esperado em PR de fork, porque a `main` faz deploy de produção. **Não entra no gate de merge.**
|
||||
- **Workflows parados** esperando aprovação: política do GitHub no primeiro PR de quem nunca contribuiu. Um mantenedor libera.
|
||||
|
||||
<details>
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
# projeto dizia a quem contribui era um check `Vercel` vermelho que não é culpa
|
||||
# dela.
|
||||
#
|
||||
# Este workflow diz três coisas, e nada além: o `Vercel` vermelho é esperado em
|
||||
# fork; os workflows podem ficar parados esperando liberação; e quando vem o
|
||||
# Este workflow diz três coisas, e nada além: os workflows podem ficar parados
|
||||
# esperando liberação; o que trava o merge são os checks marcados obrigatórios no
|
||||
# próprio PR de quem lê; e quando vem o veredito humano.
|
||||
#
|
||||
# O PRAZO PROMETIDO NÃO É CHUTE — foi medido antes de entrar no texto.
|
||||
#
|
||||
# A doutrina proíbe prometer o que não se cumpre, então "em até um dia útil"
|
||||
@@ -65,6 +67,23 @@
|
||||
# 4. O texto fixo vai por heredoc COM ASPAS (`<<'MD'`). Sem as aspas, as crases
|
||||
# da própria prosa (`` `gh pr checks` ``) viram substituição de comando.
|
||||
#
|
||||
# ## Por que o texto apresenta `--required` como atalho PARCIAL
|
||||
#
|
||||
# `gh pr checks <n> --required` mostra os obrigatórios que JÁ REPORTARAM, e só
|
||||
# esses: o que ainda não rodou não aparece na saída. Medido em 2026-09-17 — a
|
||||
# proteção da `main` exigia cinco contextos, e o comando listou DOIS no PR #1069
|
||||
# (`invariants`, `verify`), contra os cinco que a proteção da `main` exigia.
|
||||
# Escrever que ele "mostra os mesmos" da tela do PR faria alguém ler três verdes
|
||||
# no terminal e concluir que passou. Refaça a comparação em vez de confiar nesta
|
||||
# linha:
|
||||
#
|
||||
# gh api repos/$REPO/branches/main/protection --jq '.required_status_checks.contexts'
|
||||
# gh pr checks <n> --required --json name --jq '[.[].name]'
|
||||
#
|
||||
# O primeiro só responde a quem administra o repositório, e para quem não
|
||||
# administra o GitHub devolve 404 — que lê como "não há proteção nenhuma", e não
|
||||
# é isso. Medido em `gh api repos/vercel/next.js/branches/canary/protection`.
|
||||
#
|
||||
# ## O que este workflow DELIBERADAMENTE não faz: liberar o CI sozinho
|
||||
#
|
||||
# No primeiro PR de quem nunca contribuiu, os runs ficam em `action_required` e
|
||||
@@ -98,8 +117,8 @@ jobs:
|
||||
# `github.repository_owner` guarda o fork: este arquivo vai para a `main` de um
|
||||
# produto self-host, e o fork de cada pessoa herda o workflow. Sem a guarda, um
|
||||
# bot falaria pela gente no repositório de outra pessoa — prometendo o prazo de
|
||||
# mantenedores que nunca concordaram com ele, e explicando um deploy da Vercel
|
||||
# que não é o dela. É `repository_owner` e não `repository` porque renomear o
|
||||
# mantenedores que nunca concordaram com ele, e apontando um gate de merge que
|
||||
# não é o dela. É `repository_owner` e não `repository` porque renomear o
|
||||
# repositório não deve calar a acolhida; mudar de dono deve.
|
||||
acolher:
|
||||
if: github.repository_owner == 'melgarafael' && github.event.pull_request.head.repo.full_name != github.repository
|
||||
@@ -147,16 +166,19 @@ jobs:
|
||||
# Heredoc COM aspas: nada aqui dentro é expandido pelo shell, e as
|
||||
# crases da prosa continuam sendo crases.
|
||||
cat <<'MD'
|
||||
Duas coisas que vão parecer erro seu e não são:
|
||||
Uma coisa vai parecer erro seu e não é:
|
||||
|
||||
- O check **Vercel** vermelho ("Authorization required to deploy") é esperado em PR de fork. A
|
||||
`main` faz deploy de produção e a Vercel se recusa a construir código de fora, o que está
|
||||
certo. **Ele não entra no gate de merge.**
|
||||
- No primeiro PR de quem nunca contribuiu aqui, os workflows ficam **parados esperando
|
||||
liberação** — política do GitHub, não sua. Enquanto isso o PR parece não ter check nenhum
|
||||
(nem o `gh pr checks` mostra os que estão nesse estado). Quem tria libera; você não precisa
|
||||
fazer nada.
|
||||
|
||||
Quando eles rodarem, o que trava o merge são só os checks marcados **Required** aqui no seu
|
||||
próprio PR — é essa lista que vale, não qualquer outro vermelho que apareça. No terminal,
|
||||
`gh pr checks <número> --required` mostra os obrigatórios que **já reportaram**, e só esses:
|
||||
enquanto um deles não rodou, ele não aparece ali. Se um reprovar, a saída dele diz o que
|
||||
falta; se não estiver claro, comente aqui — não feche o PR.
|
||||
|
||||
Um mantenedor vai revisar de verdade — rodando os gates e reproduzindo o comportamento, não só
|
||||
lendo o diff — e responde aqui **em até um dia útil**, com a medição junto, nunca com um "acho
|
||||
que".
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# O RELÓGIO DE QUEM NÃO TEM AGENDADOR.
|
||||
#
|
||||
# Existe para instalação sem cron de verdade — o caso comum é o plano gratuito
|
||||
# da Vercel, onde o `vercel.json` só permite cron DIÁRIO. Sem alguém batendo,
|
||||
# follow-up agendado e `event_log` ficam parados até uma pessoa abrir o sistema,
|
||||
# e um lead que responde de madrugada espera até de manhã.
|
||||
# Existe para instalação sem agendador próprio — hospedagem que não oferece cron
|
||||
# de minuto, ou instalação em que o serviço `scheduler` do compose não está de
|
||||
# pé. Sem alguém batendo, follow-up agendado e `event_log` ficam parados até uma
|
||||
# pessoa abrir o sistema, e um lead que responde de madrugada espera até de manhã.
|
||||
#
|
||||
# ⚠️ NASCE DESLIGADO, e isto não é cautela decorativa: este arquivo vai para a
|
||||
# `main` de um produto self-host, e o fork de cada pessoa herda o agendamento.
|
||||
|
||||
+1
-4
@@ -121,11 +121,8 @@ abaixo são para que isso não se repita.
|
||||
|
||||
### Se você está contribuindo de fora (fork) — leia isto
|
||||
|
||||
Duas coisas vão parecer erro seu e não são:
|
||||
Uma coisa vai parecer erro seu e não é:
|
||||
|
||||
- **O check `Vercel` fica vermelho** com "Authorization required to deploy". A `main` deste
|
||||
repositório faz deploy de produção, e a Vercel se recusa a construir PR de fork por
|
||||
segurança — o que está certo. **Ignore esse check**; ele não entra no gate de merge.
|
||||
- **Os workflows ficam parados esperando aprovação** no seu primeiro PR. É política do
|
||||
GitHub para quem nunca contribuiu antes. Um mantenedor libera; do segundo PR em diante
|
||||
roda sozinho. Se demorar, comente no PR.
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \
|
||||
|
||||
# Turbopack (`pnpm build`): ~4min vs ~34min do webpack num VPS. O bloco `webpack:`
|
||||
# do Sentry (tree-shake + upload de sourcemap em build-time) é ignorado, mas o
|
||||
# Sentry RUNTIME segue ativo (DSN hardcoded nas configs). Sourcemap upload é
|
||||
# concern só da Vercel; aqui o ganho de tempo de build é o que importa pro leigo.
|
||||
# Sentry RUNTIME segue ativo (DSN hardcoded nas configs); aqui o ganho de tempo
|
||||
# de build é o que importa pro leigo.
|
||||
RUN pnpm build
|
||||
|
||||
# ---- runner: imagem slim de produção ----
|
||||
|
||||
@@ -35,8 +35,9 @@ parte da primeira e mais nada):
|
||||
|
||||
**Âncora de arquitetura:** o público-alvo principal instala o CRM numa **VPS da
|
||||
HostGator** pelo `hostgator-setup-kit/`, com **Supabase cloud**. Toda decisão se
|
||||
avalia contra isso primeiro — não contra a Vercel, não contra o laptop. E a pergunta
|
||||
que quase sempre é esquecida: **como isto chega a um clone que JÁ RODA e vai atualizar?**
|
||||
avalia contra isso primeiro — não contra uma hospedagem gerenciada, não contra o
|
||||
laptop. E a pergunta que quase sempre é esquecida: **como isto chega a um clone
|
||||
que JÁ RODA e vai atualizar?**
|
||||
|
||||
---
|
||||
|
||||
@@ -57,11 +58,11 @@ que quase sempre é esquecida: **como isto chega a um clone que JÁ RODA e vai a
|
||||
|
||||
## Estado das fases
|
||||
|
||||
**A ordem mudou depois da reancoragem.** A antiga fazia sentido para a Vercel, onde nada
|
||||
chega ao usuário sem deploy. Sob a âncora VPS, o valor chega antes por outro caminho: o
|
||||
que o comprador percebe primeiro não é upload de logo, é **a interface não parecer a
|
||||
nossa** — e cor é o eixo mais barato, mais visível, e o único que a doc de venda declara
|
||||
impossível hoje.
|
||||
**A ordem mudou depois da reancoragem.** A antiga fazia sentido para uma hospedagem
|
||||
gerenciada, onde nada chega ao usuário sem deploy. Sob a âncora VPS, o valor chega
|
||||
antes por outro caminho: o que o comprador percebe primeiro não é upload de logo, é
|
||||
**a interface não parecer a nossa** — e cor é o eixo mais barato, mais visível, e o
|
||||
único que a doc de venda declara impossível hoje.
|
||||
|
||||
> **A numeração divergiu no meio do épico, e esta tabela é a que vale.** A versão anterior
|
||||
> reservava a **Fase 3** para "upload de logo" e chamava a marca por organização de Fase 2 —
|
||||
@@ -117,8 +118,10 @@ de um agente que rodou o cálculo — e está marcado de propósito.
|
||||
- **Supabase é cloud**, provisionado pela Management API (`hostgator-setup-kit/supabase-provision.sh`).
|
||||
Storage não consome disco da VPS, mas consome **cota do plano do cliente**,
|
||||
competindo com `whatsapp-media`. Plano grátis: 2 projetos por usuário.
|
||||
- **O scheduler da VPS já roda 16 crons** (`docker-compose.prod.yml:145-172`) — o
|
||||
anti-morte por cron **existe** para o público principal. A Vercel é o caso degradado.
|
||||
- **O scheduler da VPS agenda toda rota de `app/api/v1/cron/`** — o crontab mora em
|
||||
`docker/scheduler/entrypoint.sh`, e quem mantém as duas listas iguais nas duas direções
|
||||
é `tests/unit/cron-routes-scheduled.test.ts`. O anti-morte por cron **existe** para o
|
||||
público principal.
|
||||
- Serviços do compose: `app`, `worker`, `waha`, `redis`, `srh`, `scheduler`, `caddy`.
|
||||
**Dois processos Node** (app + worker), não N lambdas — isso muda a escolha de cache.
|
||||
- A única policy de escrita em `organizations` é `orgs_write_platform_admin`
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
> nunca a olho. COMMITAR este arquivo a cada atualização — mudança só no
|
||||
> working tree se perde quando um subagent limpa a árvore (já aconteceu 1x).
|
||||
|
||||
> **Sobre o diário abaixo:** as menções à Vercel são registro do dia em que cada
|
||||
> entrada foi escrita. O CRM é self-host em VPS, e o deploy que vale está em
|
||||
> [`docs/runbooks/deploy.md`](docs/runbooks/deploy.md).
|
||||
|
||||
## Contexto fixo
|
||||
|
||||
- **Feature:** sistema de follow-up inteligente — grafo versionado + enrollment + relógio único; builder visual React Flow; fila UI; seletor no agente.
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
* Body: { run_id: uuid, sample_message?, sample_contact? }
|
||||
* sample_message/sample_contact only honored when the run row is_dry_run=true.
|
||||
*
|
||||
* Configured for `maxDuration=300` in vercel.ts — agent loops with multiple
|
||||
* tool calls can stretch close to that budget.
|
||||
* `maxDuration=300` is declared below (`export const maxDuration`) and mirrored
|
||||
* in `vercel.ts` for forks hosted on Vercel — agent loops with multiple tool
|
||||
* calls can stretch close to that budget.
|
||||
*/
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { type NextRequest } from "next/server";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* GET/POST /api/v1/cron/event-log-drain
|
||||
*
|
||||
* Vercel cron entry point for the generic event_log drain (Task 2, spec
|
||||
* webhooks/automação 2026-07-17). Each tick drains up to 50 `pending` rows
|
||||
* Cron entry point for the generic event_log drain (Task 2, spec
|
||||
* webhooks/automação 2026-07-17), scheduled by the `scheduler` service
|
||||
* (`docker/scheduler/entrypoint.sh`). Each tick drains up to 50 `pending` rows
|
||||
* whose `event_type` has a handler registered via `ensureHandlersRegistered()`
|
||||
* — types drained by a dedicated cron (e.g. `ai_agent.dispatch_requested` →
|
||||
* agent-dispatcher) have no handler here and are left untouched.
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
* isolado, só loga) — o cron sempre devolve o resultado de `runFollowupTick`.
|
||||
*
|
||||
* No fim, drena texto fixo pendente (`enviarTextoFixoPendente`) — o mesmo
|
||||
* atalho do relógio HTTP. Na Vercel não há `agent-worker`; sem isto o job
|
||||
* `followup_turn` fica `pending` e o no_reply nunca vira mensagem. O ledger
|
||||
* (job_id, seq) impede envio em dobro no self-host, onde o worker também
|
||||
* consome a fila.
|
||||
* atalho do relógio HTTP. Onde não há `agent-worker` (instalação sem o
|
||||
* contêiner `worker`), sem isto o job `followup_turn` fica `pending` e o
|
||||
* no_reply nunca vira mensagem. O ledger (job_id, seq) impede envio em dobro no
|
||||
* self-host, onde o worker também consome a fila.
|
||||
*
|
||||
* Auth: Bearer INTERNAL_CRON_SECRET|INTERNAL_SECRET, fail-closed. Audit
|
||||
* agregada por tick (`followup.worker_run` + `followup.silence_sweep_run`),
|
||||
@@ -140,9 +140,10 @@ async function handle(req: NextRequest): Promise<Response> {
|
||||
logger.error("[followup-flow-worker.cron] runSilenceSweep threw", { error: detail, requestId });
|
||||
}
|
||||
|
||||
// ponytail: o cron nativo da Vercel não tem agent-worker. Sem este dreno o
|
||||
// no_reply avança o grafo e a mensagem seguinte fica pending. Teto: jobs
|
||||
// sem fixed_body (mode ai_message) continuam precisando do worker.
|
||||
// ponytail: instalação sem `agent-worker` (relógio HTTP, cron puro) não tem
|
||||
// quem consuma a fila. Sem este dreno o no_reply avança o grafo e a mensagem
|
||||
// seguinte fica pending. Teto: jobs sem fixed_body (mode ai_message) continuam
|
||||
// precisando do worker.
|
||||
try {
|
||||
await enviarTextoFixoPendente(admin);
|
||||
} catch (err) {
|
||||
|
||||
@@ -672,7 +672,7 @@ export async function POST(req: NextRequest, ctx: RouteCtx): Promise<NextRespons
|
||||
}
|
||||
|
||||
// Captação: drena lead.created e inscreve no fluxo neste mesmo request.
|
||||
// Sem isto, em prod (Vercel Hobby sem cron de 1 min) o gatilho fica pending.
|
||||
// Sem isto, numa instalação sem cron de 1 min (relógio HTTP), o gatilho fica pending.
|
||||
await kickLocalPipeline(
|
||||
admin,
|
||||
contactId
|
||||
|
||||
@@ -202,7 +202,8 @@ services:
|
||||
|
||||
scheduler:
|
||||
# Cron sem docker.sock: crond interno batendo curl na rede interna.
|
||||
# Resolução de 1 min (= paridade com os crons da Vercel).
|
||||
# Resolução de 1 min — o menor passo que o crond tem, e o piso de latência
|
||||
# de tudo que depende de cron (a lista tem rotas de minuto em minuto).
|
||||
#
|
||||
# A lista de crons mora em docker/scheduler/entrypoint.sh, não mais aqui. O
|
||||
# `command:` inline instalava curl e tzdata (`apk add`) a CADA start — num
|
||||
|
||||
+28
-18
@@ -1,14 +1,15 @@
|
||||
# Checklist de deploy
|
||||
|
||||
Este projeto tem **dois destinos de deploy com bancos diferentes**, e confundi-los já custou um
|
||||
incidente. Escolha a seção certa antes de marcar qualquer caixa.
|
||||
Este projeto já teve **dois destinos de deploy com bancos diferentes**, e confundi-los custou um
|
||||
incidente — é por isso que a distinção segue escrita aqui. Hoje há **um** procedimento vivo: a
|
||||
seção A. A seção B é registro, e não tem caixa a marcar.
|
||||
|
||||
| Destino | O que é | Banco |
|
||||
|---|---|---|
|
||||
| **A. VPS self-host** | **o produto** — o que o cliente compra e o que a doutrina rege | Supabase cloud provisionado pelo `install.sh` |
|
||||
| **B. Vercel** | ambiente do mantenedor, deploy a cada push na `main` | outro projeto Supabase |
|
||||
| **B. Vercel** | ambiente do mantenedor, **congelado em 2026-09-17**: o projeto foi desvinculado do GitHub e nenhum push publica código novo lá | outro projeto Supabase |
|
||||
|
||||
> Este arquivo esteve congelado de 2026-04-28 a 2026-08-13 — escrito **dois meses antes de o
|
||||
> Este arquivo ficou sem revisão de 2026-04-28 a 2026-08-13 — escrito **dois meses antes de o
|
||||
> Dockerfile existir**. Ele cobria só a Vercel e mandava aplicar `supabase/migrations/`, que
|
||||
> não é o caminho de nenhum self-hoster (o kit aplica **só** `supabase/baseline.sql`). Quem o
|
||||
> seguisse para um deploy de VPS não fazia nada do que o produto exige.
|
||||
@@ -27,6 +28,10 @@ O procedimento completo está em [`doctrine/packaging.md`](doctrine/packaging.md
|
||||
- [ ] Mudança de schema saiu como **tripla**: `supabase/migrations/` + apêndice idempotente no
|
||||
`supabase/baseline.sql` + linha no `MANIFEST.md`. **O kit aplica só o baseline** — o que
|
||||
não chegar lá não chega a ninguém
|
||||
|
||||
Schema antes de código, e não o contrário: em 2026-08-04 o ambiente do mantenedor
|
||||
subiu código à frente do banco e o inbox devolveu 500 (`42703`). O build passa — quem
|
||||
descobre é o usuário, na tela.
|
||||
- [ ] `pnpm test:db` verde localmente (aplica o baseline em install **e** update num Postgres limpo)
|
||||
- [ ] `pnpm test:shell` verde (é o único gate que exercita o kit)
|
||||
- [ ] O número da versão nunca foi publicado: `git tag --list 'vX.Y.Z'` vazio **e**
|
||||
@@ -66,21 +71,26 @@ O procedimento completo está em [`doctrine/packaging.md`](doctrine/packaging.md
|
||||
|
||||
---
|
||||
|
||||
## B. Vercel — deploy do mantenedor
|
||||
## B. Vercel — ambiente do mantenedor, congelado em 2026-09-17
|
||||
|
||||
- [ ] Envs do projeto na Vercel espelham o `.env.local`
|
||||
- [ ] `SENTRY_DSN` definido; `SENTRY_AUTH_TOKEN` configurado para upload de sourcemap
|
||||
- [ ] `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`
|
||||
- [ ] `WAHA_*` (URL, api key em plaintext no cliente, hash SHA512 no servidor)
|
||||
- [ ] `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN`
|
||||
- [ ] `INTERNAL_SECRET` (rotacionado ao menos uma vez)
|
||||
- [ ] **Migrations aplicadas no banco da Vercel** — `supabase/migrations/`, e **confira**: o
|
||||
pipeline da Vercel faz deploy a cada push na `main` e **não aplica migration nenhuma**.
|
||||
Em 2026-08-04 a produção rodou código à frente do banco e o inbox devolveu 500 (`42703`)
|
||||
- [ ] RLS verificada nas tabelas tenant-aware (smoke cross-tenant)
|
||||
- [ ] Evento de teste do Sentry capturado no ambiente de produção
|
||||
- [ ] `pnpm typecheck`, `pnpm lint`, `pnpm test:unit` limpos no commit da release
|
||||
- [ ] LCP/CLS/INP dentro do orçamento (Vercel Analytics RUM)
|
||||
> O projeto do CRM na Vercel foi desvinculado do GitHub: push e PR neste repositório não geram
|
||||
> mais deploy, e o que estava no ar não recebe mais código. **Nada abaixo é acionável**: fica
|
||||
> como registro do que aquele ambiente exigia enquanto recebia código. O destino vivo é a
|
||||
> seção A.
|
||||
|
||||
- Envs do projeto na Vercel espelham o `.env.local`
|
||||
- `SENTRY_DSN` definido; `SENTRY_AUTH_TOKEN` configurado para upload de sourcemap
|
||||
- `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`
|
||||
- `WAHA_*` (URL, api key em plaintext no cliente, hash SHA512 no servidor)
|
||||
- `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN`
|
||||
- `INTERNAL_SECRET` (rotacionado ao menos uma vez)
|
||||
- Migrations aplicadas naquele banco, porque o deploy de lá nunca aplicou nenhuma — **não
|
||||
aplique**: o código que as leria não avança mais. A lição que sobrou disso está na seção A,
|
||||
no item da tripla de schema, e vale igual na VPS
|
||||
- RLS verificada nas tabelas tenant-aware (smoke cross-tenant)
|
||||
- Evento de teste do Sentry capturado no ambiente de produção
|
||||
- `pnpm typecheck`, `pnpm lint`, `pnpm test:unit` limpos no commit da release
|
||||
- LCP/CLS/INP dentro do orçamento (Vercel Analytics RUM)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+16
-9
@@ -284,15 +284,22 @@ consequência natural de trabalho em branches paralelas, mas ilustra a regra:
|
||||
5. **`lib/agent-engine/agent/inbound-turn.ts` com 1789 linhas** — 2,4× o segundo maior arquivo
|
||||
de lógica (`AgentForm.tsx`, 746), e é o hot path do produto. Cresceu ~200 linhas desde a
|
||||
primeira medição desta auditoria.
|
||||
6. **NENHUM cron roda no deploy Vercel.** Os 14 crons do produto são agendados exclusivamente
|
||||
pelo `crond` do serviço `scheduler` do `docker-compose.prod.yml` — e **não existe
|
||||
`vercel.json` neste repo** (medido: `ls vercel.json` → ausente). Como a Vercel é a produção
|
||||
real de quem mantém (ver `project_dois_ambientes_de_producao`), tudo que depende de
|
||||
agendamento está **dormente lá**: `event-log-drain`, `agent-dispatcher`, `followup-flow-worker`,
|
||||
`recover-stuck-messages`, `sync-model-catalog`, `contact-proposals-watcher`, etc. O sintoma
|
||||
nunca é um erro — a tela só fica velha, a fila só não anda. Está escrito aqui porque cada
|
||||
frente nova repetia a suposição de que "o cron roda"; a decisão (portar para Vercel Cron ou
|
||||
assumir que a Vercel é vitrine e a VPS é a operação) é do dono do repo.
|
||||
6. **Cron parado não dá erro — a tela só fica velha e a fila só não anda.** Está escrito aqui
|
||||
porque cada frente nova repete a suposição de que "o cron roda" sem olhar quem bate. No
|
||||
self-host quem bate é o `crond` do serviço `scheduler`, e a lista de rotas mora em
|
||||
`docker/scheduler/entrypoint.sh` — não no `docker-compose.prod.yml` (a de hoje sai de
|
||||
`grep -oE 'api/v1/cron/[a-z0-9-]+' docker/scheduler/entrypoint.sh | sort -u`). Cada linha
|
||||
descarta a saída do `curl` (`>/dev/null 2>&1`), então o resultado da batida não chega ao
|
||||
log do scheduler. Quem instala sem agendador próprio tem um segundo caminho, que roda
|
||||
**parte** dessas tarefas dentro do processo do app (`lib/relogio/executar.ts`).
|
||||
A cerca contra rota que nasce sem agendamento é
|
||||
`tests/unit/cron-routes-scheduled.test.ts`: ele compara o diretório `app/api/v1/cron/` com
|
||||
o crontab nas duas direções, e cobre também `vercel.ts`, o inventário de quem hospeda um
|
||||
fork na Vercel. **Decidido em 2026-09-17, e por isso não há mais decisão em aberto aqui:**
|
||||
a operação é a instalação em VPS e a Vercel ficou só com a landing page; o projeto Vercel
|
||||
do CRM foi desvinculado do GitHub. Para conferir, olhe os status do último commit da `main` —
|
||||
enquanto o projeto esteve ligado, aparecia ali um check `Vercel`:
|
||||
`gh api repos/melgarafael/DeskcommCRM/commits/main/status --jq '[.statuses[].context]'`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1179,8 +1179,8 @@ aplica.
|
||||
`scripts/smoke-llm.ts:168`, um smoke de dev, o que **sugere** zero; sugerir não é medir, e
|
||||
o campo é jsonb livre, editável por qualquer acesso privilegiado ao banco.
|
||||
`select count(*) from organizations where jsonb_typeof(settings->'llm'->'monthly_budget_cents') = 'number'`
|
||||
responde em um segundo. Há **dois** ambientes de produção com bancos diferentes (Vercel e
|
||||
VPS) e a resposta pode divergir entre eles.
|
||||
responde em um segundo. Rode-o contra o banco da instalação em VPS — o produto é self-host,
|
||||
e é lá que ele opera.
|
||||
2. Quantas têm `monthly_limit_cents <> 5000` — dimensiona quantas pessoas vão ler "você
|
||||
definiu X, mas nunca foi aplicado".
|
||||
3. Quantas orgs estão no recorte que alcança o guard de `ai-response-worker.ts:408-413`
|
||||
|
||||
@@ -40,7 +40,7 @@ Implicações de leitura deste documento e dos sub-PRDs:
|
||||
3. **MCP-ready**: arquitetura inclui MCP server (interno hoje; contrato público na Fase 2) com 19 tools canônicas pra LLMs operarem o sistema.
|
||||
4. **LGPD nativa**: redact e data_request como contrato de primeira-classe (incluindo os webhooks da Nuvemshop no vertical e-commerce), não afterthought.
|
||||
|
||||
**Restrições principais.** MVP-B em produção em **8–12 semanas**. Stack obrigatória: bundle adotado (Next.js 14+ App Router + Supabase + WAHA Plus + Vercel + MCP server separado em Node ESM). LGPD desde o dia 1. Arquitetura multi-tenant com RLS Postgres em toda tabela tenant-aware.
|
||||
**Restrições principais.** MVP-B em produção em **8–12 semanas**. Stack obrigatória: bundle adotado (Next.js 14+ App Router + Supabase + WAHA Plus + MCP server separado em Node ESM). LGPD desde o dia 1. Arquitetura multi-tenant com RLS Postgres em toda tabela tenant-aware.
|
||||
|
||||
---
|
||||
|
||||
@@ -174,7 +174,7 @@ Em três anos: ser a resposta padrão pra "melhor CRM open source com agentes de
|
||||
DeskcommCRM **adota integralmente** a doutrina arquitetural extraída do material da *Aula CRM Nichado com WhatsApp (WAHA)*. Síntese completa em `docs/research/reference-synthesis.md`.
|
||||
|
||||
**Pontos não negociáveis herdados:**
|
||||
- Stack Next.js + Supabase + WAHA Plus + Vercel
|
||||
- Stack Next.js + Supabase + WAHA Plus
|
||||
- Multi-tenant via RLS com helper `fn_user_org_ids()`
|
||||
- 5 tabelas core CRM (`crm_pipelines`, `crm_stages`, `crm_leads`, `crm_lead_activities`, `crm_lead_links`)
|
||||
- Polimorfismo explícito em timeline e vínculos
|
||||
|
||||
@@ -226,7 +226,7 @@ Contrato completo e limites OAuth: [`docs/support-sessions.md`](../support-sessi
|
||||
- p95 de tenant resolution + RLS query simples: <100ms
|
||||
- p95 de mutação simples (POST/PATCH lead): <300ms
|
||||
- p95 de audit log write: <500ms (fire-and-forget)
|
||||
- Suporte concorrente: 100 RPS por tenant no MVP, escalável horizontalmente via Vercel
|
||||
- Suporte concorrente: 100 RPS por tenant no MVP
|
||||
|
||||
### 4.3 Compliance
|
||||
- LGPD desde o dia 1 (vide §3.6)
|
||||
@@ -265,7 +265,7 @@ A Plataforma Base é considerada **MVP-completa** quando:
|
||||
|
||||
### Externas
|
||||
- **Supabase** (Auth, Postgres, Storage) — projeto provisionado, plano Pro mínimo pra produção
|
||||
- **Vercel** — projeto + domínio + AI Gateway (pra Sub-PRDs futuros)
|
||||
- **Vercel AI Gateway** — só o gateway de LLM, via `AI_GATEWAY_API_KEY` (pra Sub-PRDs futuros). Hospedagem não entra aqui: quem instala roda o CRM na própria infraestrutura
|
||||
- **Upstash Redis** — instância de produção pra rate limit
|
||||
- **Sentry** — projeto criado, com regras de sanitização aprovadas
|
||||
- **Domínio + subdomínio** `admin.deskcomm.com` (pra UI super-admin)
|
||||
|
||||
@@ -68,7 +68,7 @@ A janela de 24h da Meta (envio proativo só com template aprovado fora da janela
|
||||
- **Engine NOWEB por default** (mais leve, sem Chromium); **WEBJS apenas pra features específicas** (stickers animados, listas/botões interativos) — decisão por feature, não por sessão inteira; revisitar na Spec
|
||||
- `webhook_secret` é **único por sessão** (não global) — facilita revogação e rotação
|
||||
- 1 tenant pode ter N sessões (MVP-B: 1-2 por tenant; arquitetura suporta mais)
|
||||
- Auth WAHA: `WAHA_API_KEY` armazenada como **SHA512 do plaintext** no servidor WAHA; o backend DeskcommCRM guarda o plaintext em variável de ambiente segura (Vercel Encrypted Env Var)
|
||||
- Auth WAHA: `WAHA_API_KEY` armazenada como **SHA512 do plaintext** no servidor WAHA; o backend DeskcommCRM guarda o plaintext no `.env` da instalação (permissão 600)
|
||||
- Mudança de status é evento de timeline + audit (`channel_session.status_changed`)
|
||||
|
||||
**ACs principais.**
|
||||
@@ -283,7 +283,7 @@ A janela de 24h da Meta (envio proativo só com template aprovado fora da janela
|
||||
|
||||
### 3.10 Crons obrigatórios
|
||||
|
||||
**O que provê.** Tarefas agendadas (Vercel Cron) que sustentam consistência e auto-recovery.
|
||||
**O que provê.** Tarefas agendadas — no self-host, pelo serviço `scheduler` do `docker-compose.prod.yml` — que sustentam consistência e auto-recovery. A tabela abaixo é o requisito deste PRD, não o crontab em vigor: esse sai de `grep -oE 'api/v1/cron/[a-z0-9-]+' docker/scheduler/entrypoint.sh | sort -u`.
|
||||
|
||||
| Cron | Frequência | Responsabilidade |
|
||||
|---|---|---|
|
||||
@@ -320,7 +320,7 @@ A janela de 24h da Meta (envio proativo só com template aprovado fora da janela
|
||||
- SLA WAHA upstream: 99% (Railway/Hostgator não dão SLA forte; aceito como tradeoff)
|
||||
|
||||
### 4.3 Segurança
|
||||
- `WAHA_API_KEY` plaintext armazenada apenas em Vercel Encrypted Env Vars; SHA512 no servidor WAHA
|
||||
- `WAHA_API_KEY` plaintext armazenada apenas no `.env` da instalação (permissão 600); SHA512 no servidor WAHA
|
||||
- `webhook_secret` por sessão (não global); rotação suportada
|
||||
- HMAC-SHA512 com timing-safe compare; nunca comparação ingênua de strings
|
||||
- Mídia em Supabase Storage com RLS por bucket; URLs assinadas com TTL ≤30min
|
||||
@@ -373,7 +373,7 @@ O canal WhatsApp é considerado **MVP-completo** quando:
|
||||
### Externas
|
||||
- **WAHA Plus** — instância hospedada (Railway $5-10/mês no MVP; VPS Hostgator plano Turing ~R$140/mês em produção com Nginx + Let's Encrypt; datacenter São Paulo)
|
||||
- **Supabase Storage** (bucket por tenant pra mídia)
|
||||
- **Vercel Cron** (3 jobs: sync-sessions, recover-stuck-messages, process-pending-webhooks)
|
||||
- **Agendamento de crons** — 3 jobs previstos (sync-sessions, recover-stuck-messages, process-pending-webhooks) no serviço `scheduler` do `docker-compose.prod.yml`. Previsto: nem todo job desta linha está no crontab de hoje, que sai de `grep -oE 'api/v1/cron/[a-z0-9-]+' docker/scheduler/entrypoint.sh | sort -u`
|
||||
- **Fila de envio**: Inngest, Trigger.dev, ou pg_boss (decisão na Spec)
|
||||
|
||||
### Decisões deferidas pra Spec
|
||||
@@ -399,7 +399,7 @@ O canal WhatsApp é considerado **MVP-completo** quando:
|
||||
| W3 | **Falha de webhook** (WAHA down, network partition, handler crash) | Alto | `webhook_events_log` raw como fonte de verdade; cron `process-pending-webhooks` re-processa; WAHA Plus tem retry nativo (Core não); dead-letter com alerta após 3 tentativas |
|
||||
| W4 | **Inconsistência multi-device** (mensagem enviada por celular não aparece no CRM, ou aparece duplicada) | Médio | Assinar `message.any` (não `message`); idempotência por `(org, external_id)`; teste de regressão simulando envio cross-device |
|
||||
| W5 | **Abuso de envio em campanha** (atendente faz blast de 1000 msgs sem warm-up) | Alto | Hard-cap diário por sessão; validação de min 5 variações de copy; bloqueio de campanha durante warm-up; revisão manual de campanhas >500 msgs no MVP |
|
||||
| W6 | **Vazamento de credentials WAHA** (`WAHA_API_KEY` em log, repo, ou env exposto) | Crítico | Plaintext apenas em Vercel Encrypted Env Vars; sanitização agressiva em logs; `gitleaks` pre-commit (Sub-PRD 01 §4.1); rotação trimestral; SHA512 no servidor WAHA garante que comprometimento do servidor não vaza o plaintext |
|
||||
| W6 | **Vazamento de credentials WAHA** (`WAHA_API_KEY` em log, repo, ou env exposto) | Crítico | Plaintext apenas no `.env` da instalação (permissão 600); sanitização agressiva em logs; `gitleaks` pre-commit (Sub-PRD 01 §4.1); rotação trimestral; SHA512 no servidor WAHA garante que comprometimento do servidor não vaza o plaintext |
|
||||
| W7 | **Dependência de upstream WAHA Plus** (mudança de política, deprecação, ban da WAHA pelo WhatsApp) | Alto | Variante BYO documentada (cliente roda WAHA próprio) como Fase 2; consideração futura de migração pra Cloud API oficial Meta como Fase 2.5; monitoramento de release notes WAHA; contrato de suporte explícito com mantenedor da WAHA Plus |
|
||||
| W8 | **Mensagem fora de ordem** (webhook chega depois de mensagem mais nova) | Médio | Ordenar timeline por `sent_at` (do payload), não `created_at` do DB; UI re-renderiza ao receber out-of-order |
|
||||
| W9 | **Mídia >50MB inviável** (WhatsApp aceita até 100MB pra alguns tipos, mas WAHA pode falhar) | Médio | UI rejeita >16MB no outbound (limite WhatsApp para a maioria dos tipos); inbound >50MB usa S3 do WAHA Plus ou stream em chunks; fallback de download |
|
||||
|
||||
@@ -337,7 +337,7 @@ A integração Nuvemshop + LGPD é considerada **MVP-completa** quando:
|
||||
- **Conta Nuvemshop developer** + app registrado (production) com scopes mínimos definidos
|
||||
- **Acesso aos endpoints OAuth da Nuvemshop** (production e sandbox)
|
||||
- **`pgcrypto` ativo** no Postgres (Supabase) — vide Sub-PRD 01 §4.1
|
||||
- **Worker runtime** pra background jobs (Vercel Functions / serviço externo — decisão Spec)
|
||||
- **Worker runtime** pra background jobs — decidido depois deste PRD; ver o item 13 de §9
|
||||
|
||||
### Decisões deferidas pra Spec
|
||||
- App embedded vs External da Nuvemshop
|
||||
@@ -364,7 +364,7 @@ A integração Nuvemshop + LGPD é considerada **MVP-completa** quando:
|
||||
| N6 | **Sync inicial demora dias e bloqueia admin** | Worker em background; UI mostra progresso e ETA; chunking + cursor para resume após interrupção; throttle pra respeitar rate limit Nuvemshop |
|
||||
| N7 | **Conflito identity resolution gera `merge_queue` enorme** (loja com 50k clientes histórico vs WhatsApp existente) | UI de merge em batch; opção de "auto-aceitar" merges com confiança alta após review humano dos primeiros 100; documentação clara pro lojista no onboarding |
|
||||
| N8 | **Re-sync corrompe estado** (idempotência falha por bug) | Idempotência testada com fixture de produção (re-rodar sync 3x deve resultar em zero linhas novas); rollback procedure documentado |
|
||||
| N9 | **`pgcrypto` encryption key vazada** → todos os tokens OAuth expostos | Chave separada (`NUVEMSHOP_OAUTH_ENCRYPTION_KEY`) das demais; rotação trimestral planejada; processo de re-encrypt em background; secret manager (Vercel Encrypted env) com audit de acesso |
|
||||
| N9 | **`pgcrypto` encryption key vazada** → todos os tokens OAuth expostos | Chave separada (`NUVEMSHOP_OAUTH_ENCRYPTION_KEY`) das demais; rotação trimestral planejada; processo de re-encrypt em background; chave no `.env` da instalação (permissão 600) — **sem auditoria de leitura** |
|
||||
| N10 | **Rate limit Nuvemshop estourado** durante incident bloqueia outros tenants | Worker pool por tenant (não global); circuit breaker se 429 persistente; alarme em 3+ tentativas em 1h |
|
||||
|
||||
---
|
||||
@@ -399,7 +399,7 @@ A serem decididas no spec correspondente (`docs/specs/06-spec-nuvemshop-lgpd.md`
|
||||
10. **Backoff específico do rate limit Nuvemshop**: parâmetros exatos baseados em testes
|
||||
11. **Política de retenção de `webhook_events_log`**: hot 90 dias, cold após (S3?)
|
||||
12. **Estrutura de notificação ao admin** sobre token expirado, store/redact, DLQ (in-app, email, ambos)
|
||||
13. **Worker runtime**: Vercel Functions (timeout 5min na Hobby, 15min Pro), serviço externo (Render, Railway), ou fila gerenciada (Inngest, Trigger.dev)
|
||||
13. ~~**Worker runtime**: Vercel Functions, serviço externo (Render, Railway), ou fila gerenciada (Inngest, Trigger.dev)~~ — **decidido, e fora desta lista desde então**: runtime próprio, no serviço `worker` do `docker-compose.prod.yml`, na infraestrutura de quem instala. Os dois caminhos de execução (o entrypoint do `worker` e os handlers de `event_log`) estão em [`docs/specs/07-spec-events-workers.md`](../specs/07-spec-events-workers.md) §6
|
||||
14. **Estratégia de teste de contrato** com sandbox Nuvemshop no CI
|
||||
|
||||
---
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
**Data**: 2026-04-28 (entrega overnight pra apresentação SP 2026-04-29 cedo)
|
||||
**Sessão**: autônoma via Claude Opus 4.7 + 11 subagentes paralelos
|
||||
|
||||
> **Registro de 2026-04-28.** Foi escrito quando o alvo de deploy era a Vercel — inclusive o próximo passo de conectar o projeto à plataforma. Hoje o CRM é self-host em VPS, e o deploy que vale está em [`docs/runbooks/deploy.md`](../runbooks/deploy.md).
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
@@ -8,6 +8,8 @@ description: CRM operacional com IA pra e-commerce brasileiro
|
||||
date: 2026-04-29
|
||||
---
|
||||
|
||||
> **Registro de 2026-04-29.** Este deck foi montado quando o alvo de deploy era a Vercel — daí os slides de arquitetura e de custo. Hoje o CRM é self-host em VPS, e o deploy que vale está em [`docs/runbooks/deploy.md`](../runbooks/deploy.md).
|
||||
|
||||
# DeskcommCRM
|
||||
|
||||
### O CRM operacional onde **IA e humanos atendem juntos** os clientes finais de PMEs de e-commerce no WhatsApp.
|
||||
|
||||
@@ -10,6 +10,8 @@ formato: Mermaid
|
||||
|
||||
# DeskcommCRM — Diagramas de Arquitetura
|
||||
|
||||
> **Registro de 2026-04-28.** Os diagramas abaixo foram desenhados quando o alvo de deploy era a Vercel. Hoje o CRM é self-host em VPS, e o deploy que vale está em [`docs/runbooks/deploy.md`](../runbooks/deploy.md). O corpo não foi atualizado: é o desenho daquele dia, não o estado do sistema.
|
||||
|
||||
Este documento consolida os diagramas canônicos do DeskcommCRM em sintaxe Mermaid. Serve como referência visual única para discussões de arquitetura, onboarding técnico, revisão de PRs estruturais e auditoria LGPD. Os diagramas aderem ao modelo C4 (níveis 1, 2 e 3), complementados por ER, sequência, deployment, fluxo de dados e máquinas de estado. Toda decisão arquitetural representada aqui foi herdada do bundle de referência (`reference-synthesis.md`) ou explicitada nos sub-PRDs `01` a `06`.
|
||||
|
||||
---
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
**Status:** Adotada integralmente como linha de base arquitetural do DeskcommCRM (decisão registrada em memória do projeto).
|
||||
|
||||
> **O corpo abaixo é registro, não estado — este arquivo não traz data.** Foi escrito quando o alvo de deploy era a Vercel; hoje o CRM é self-host em VPS, e o deploy que vale está em [`docs/runbooks/deploy.md`](../runbooks/deploy.md).
|
||||
|
||||
Esse documento extrai apenas as decisões e padrões da referência que o DeskcommCRM herda. Para citações literais, schema SQL completo e edge cases detalhados, consultar a fonte original.
|
||||
|
||||
---
|
||||
|
||||
@@ -7,10 +7,10 @@ o que rotacionamos aqui é a **master key** que cifra essas BYO keys.
|
||||
## Quando rotacionar
|
||||
- Suspeita de exposição da `AI_CRED_AES_KEY` (vazamento de env var, leak de logs).
|
||||
- Política de rotação anual (default).
|
||||
- Saída de engenheiro com acesso ao Vercel project secrets.
|
||||
- Saída de engenheiro com acesso ao `.env` da instalação.
|
||||
|
||||
## Pré-requisitos
|
||||
- Acesso ao Vercel project (env vars Production + Preview).
|
||||
- Acesso ao `.env` da instalação e à possibilidade de recriar os contêineres do CRM.
|
||||
- `psql` direto ou Supabase Studio com service role.
|
||||
- Janela de manutenção curta (≤5 min) ou estratégia online (descrita abaixo).
|
||||
|
||||
@@ -24,7 +24,9 @@ v1. Backfill é feito por job que decifra com a key antiga e recifra com a nova.
|
||||
|
||||
## Estratégia offline (manutenção curta — usar em emergência)
|
||||
|
||||
1. Coloque a app em modo manutenção (Vercel maintenance redirect ou flag).
|
||||
1. Tire a app do ar durante a janela, parando os contêineres do CRM. **Não há chave de
|
||||
manutenção no código:** a página `app/503/page.tsx` existe e é pública
|
||||
(`lib/auth/public-paths.ts`), mas nada no repositório coloca a instalação nela.
|
||||
2. Gere a nova key local: `openssl rand -base64 32` → `NEW_AES_KEY`.
|
||||
3. Mantenha a antiga em mãos como `OLD_AES_KEY`.
|
||||
4. Rode o script de rotação (a ser implementado em
|
||||
@@ -39,8 +41,12 @@ v1. Backfill é feito por job que decifra com a key antiga e recifra com a nova.
|
||||
- Decifra com `OLD`
|
||||
- Cifra com `NEW`
|
||||
- UPDATE ai_provider_credentials SET ... WHERE id = $1
|
||||
5. Atualize `AI_CRED_AES_KEY` no Vercel (Production + Preview).
|
||||
6. Tire o modo manutenção.
|
||||
5. Atualize `AI_CRED_AES_KEY` no `.env` da instalação e recrie os contêineres — env var
|
||||
é lida no boot, então editar o arquivo sozinho não troca chave nenhuma. Quem recria
|
||||
tudo é `bash hostgator-setup-kit/update.sh`, que roda o `up -d` sem nomear serviço;
|
||||
à mão, o comando (e a armadilha dos **dois** `-f` num host com proxy reverso próprio)
|
||||
está em [`deploy.md`](deploy.md).
|
||||
6. Suba os contêineres do CRM de volta.
|
||||
7. Apague `AI_CRED_AES_KEY_OLD` de qualquer lugar persistido.
|
||||
|
||||
## Smoke test pós-rotação
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
# Relógio no Vercel Hobby (follow-up não fica preso)
|
||||
# Relógio para instalação sem agendador (follow-up não fica preso)
|
||||
|
||||
## Plano Pro
|
||||
## Quando este runbook se aplica
|
||||
|
||||
No Pro o relógio nativo é o `vercel.ts` da raiz — a mesma cadência do
|
||||
O caminho do produto é o self-host, e lá o agendador já vem junto: o serviço
|
||||
`scheduler` do compose bate cada rota na cadência do
|
||||
`docker/scheduler/entrypoint.sh` (follow-up, dreno, dispatcher, agenda…).
|
||||
Expressões de minuto **quebram o deploy Hobby**. Este runbook é só para quem
|
||||
está no plano gratuito e precisa de um cron de fora. Não crie `vercel.json`
|
||||
ao lado do `vercel.ts`: a Vercel recusa os dois.
|
||||
|
||||
## Por que existe (Hobby)
|
||||
Este runbook é para a instalação que **não** tem esse serviço — hospedagem
|
||||
gerenciada sem cron de minuto, ou um deploy em que o `scheduler` não está de pé.
|
||||
Aí o relógio precisa vir de fora.
|
||||
|
||||
No plano Hobby a Vercel só agenda **1 cron por dia**. Sem um relógio externo:
|
||||
### O caso concreto: um FORK num plano que só agenda 1 cron por dia
|
||||
|
||||
Este repositório não é hospedado na Vercel, mas **um fork pode ser** — e o plano
|
||||
gratuito de lá é o exemplo canônico de agendador que dispara uma vez ao dia. Duas
|
||||
regras valem para quem hospeda assim:
|
||||
|
||||
- **Expressões de minuto derrubam o deploy no plano gratuito.** É o que o cabeçalho
|
||||
do `vercel.ts` da raiz já avisa: nesse plano cabe uma entrada diária só. Deixe uma
|
||||
(o `lgpd-sla-watcher` é a sugestão de lá) e traga o resto do relógio de fora, pelas
|
||||
opções A e B abaixo.
|
||||
- **Não crie um `vercel.json` ao lado do `vercel.ts`: a Vercel recusa os dois.** Este
|
||||
repositório traz só o `.ts` — `ls vercel.json` não acha nada.
|
||||
|
||||
O `vercel.ts` é o inventário de crons do fork, com as mesmas rotas e a mesma cadência
|
||||
do `docker/scheduler/entrypoint.sh`; `tests/unit/cron-routes-scheduled.test.ts` reprova
|
||||
quando as duas listas de **rotas** divergem — a cadência não está sob gate —, então rota de
|
||||
cron nova entra nos dois arquivos.
|
||||
|
||||
## Por que existe
|
||||
|
||||
Sem nada batendo de poucos em poucos minutos:
|
||||
|
||||
1. o lead responde "SIM" no WhatsApp;
|
||||
2. a mensagem entra no banco (inbox OK);
|
||||
@@ -20,27 +40,22 @@ O endpoint `POST /api/v1/system/relogio/tick` drena eventos, aplica respostas
|
||||
inbound nos follow-ups e envia textos fixos pendentes. Quem precisa chamar
|
||||
esse endpoint a cada poucos minutos é um **cron de fora** — grátis.
|
||||
|
||||
## Pré-requisito: um só deploy no domínio do WAHA
|
||||
## Pré-requisito: um só endereço para a UI e para o webhook
|
||||
|
||||
O webhook WAHA tem que bater no **mesmo** deployment que a UI/`webhooks/in`.
|
||||
|
||||
```bash
|
||||
# Ver para onde o domínio aponta hoje
|
||||
npx vercel alias ls | findstr /i "crm-gabrielle deskcomm-crm"
|
||||
|
||||
# Se ainda apontar para um deploy CLI antigo, reaponte para o da branch develop:
|
||||
npx vercel alias set <url-do-deploy-develop> crm-gabrielle.vercel.app
|
||||
```
|
||||
O webhook do WAHA tem que bater na **mesma** instalação que serve a UI e
|
||||
`webhooks/in`. Se o domínio do webhook apontar para um deploy e a UI para
|
||||
outro, o tick anda numa instalação e o "SIM" chega na outra — o follow-up fica
|
||||
preso com a mensagem visível na inbox.
|
||||
|
||||
Confirme nos logs: `POST /api/v1/webhooks/waha` e `POST /api/v1/webhooks/in`
|
||||
devem compartilhar o **mesmo** `dep=dpl_…`.
|
||||
têm de chegar no mesmo lugar em que o tick bate.
|
||||
|
||||
## Opção A — GitHub Actions (grátis em repo público)
|
||||
|
||||
Arquivo: [`.github/workflows/relogio.yml`](../../.github/workflows/relogio.yml).
|
||||
|
||||
**Limitação:** o `schedule:` do Actions **só roda na branch default (`main`)**.
|
||||
Se o workflow existir só em `develop`, o cron **nunca** dispara.
|
||||
Se o workflow existir só numa branch de trabalho, o cron **nunca** dispara.
|
||||
|
||||
### Ligar
|
||||
|
||||
@@ -50,8 +65,8 @@ Se o workflow existir só em `develop`, o cron **nunca** dispara.
|
||||
| Tipo | Nome | Valor |
|
||||
|------|------|--------|
|
||||
| Variable | `RELOGIO_LIGADO` | `1` |
|
||||
| Secret | `RELOGIO_APP_URL` | `https://crm-gabrielle.vercel.app` (sem barra no fim) |
|
||||
| Secret | `RELOGIO_SECRET` | o mesmo `INTERNAL_SECRET` do projeto na Vercel |
|
||||
| Secret | `RELOGIO_APP_URL` | `https://SEU-DOMINIO` (sem barra no fim) |
|
||||
| Secret | `RELOGIO_SECRET` | o mesmo `INTERNAL_SECRET` do `.env` da sua instalação |
|
||||
|
||||
3. Actions → **relogio** → Run workflow (teste manual).
|
||||
4. Espere o schedule `*/5` (o GitHub atrasa; 5–15 min é normal).
|
||||
@@ -59,7 +74,7 @@ Se o workflow existir só em `develop`, o cron **nunca** dispara.
|
||||
```bash
|
||||
# Via CLI (com permissão de secrets no repo)
|
||||
gh variable set RELOGIO_LIGADO -R SEU_USER/DeskcommCRM -b 1
|
||||
gh secret set RELOGIO_APP_URL -R SEU_USER/DeskcommCRM -b "https://crm-gabrielle.vercel.app"
|
||||
gh secret set RELOGIO_APP_URL -R SEU_USER/DeskcommCRM -b "https://SEU-DOMINIO"
|
||||
gh secret set RELOGIO_SECRET -R SEU_USER/DeskcommCRM -b "$INTERNAL_SECRET"
|
||||
```
|
||||
|
||||
@@ -69,7 +84,7 @@ Melhor latência que o Actions. Conta free permite job a cada minuto.
|
||||
|
||||
1. Crie conta em [https://cron-job.org](https://cron-job.org).
|
||||
2. Create cronjob:
|
||||
- **URL:** `https://crm-gabrielle.vercel.app/api/v1/system/relogio/tick`
|
||||
- **URL:** `https://SEU-DOMINIO/api/v1/system/relogio/tick`
|
||||
- **Schedule:** every 1 minute
|
||||
- **Request method:** POST
|
||||
- **Header:** `Authorization` = `Bearer <INTERNAL_SECRET>`
|
||||
@@ -80,12 +95,12 @@ O curl equivalente:
|
||||
```bash
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: Bearer $INTERNAL_SECRET" \
|
||||
"https://crm-gabrielle.vercel.app/api/v1/system/relogio/tick"
|
||||
"https://SEU-DOMINIO/api/v1/system/relogio/tick"
|
||||
```
|
||||
|
||||
## Como saber que está funcionando
|
||||
|
||||
Nos logs da Vercel (produção), a cada batida:
|
||||
Nos logs da sua instalação, a cada batida:
|
||||
|
||||
- `POST /api/v1/system/relogio/tick` → 200
|
||||
- quando há "SIM" preso: `[relogio] follow-up avancou por resposta inbound`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Runbook — WAHA em produção (VPS Hostgator)
|
||||
status: canônico
|
||||
last_review: 2026-05-04
|
||||
last_review: 2026-09-17
|
||||
owner: Rafael Melgaço
|
||||
---
|
||||
|
||||
@@ -32,7 +32,7 @@ owner: Rafael Melgaço
|
||||
2. Domínio com DNS gerenciado em Cloudflare (ou outro provider) — ex.: `waha.deskcomm.com.br`.
|
||||
3. Conta Backblaze B2 com bucket `deskcomm-waha-backup` (R$0,06/GB/mês ≈ $0.005/GB).
|
||||
4. Licença ativa **WAHA Plus** (`https://waha.devlike.pro` — ~$30/mês).
|
||||
5. Vercel project com env vars `WAHA_API_BASE_URL`, `WAHA_API_KEY`, `WAHA_WEBHOOK_BASE_URL`, `WAHA_HMAC_SECRET` configurados (ainda apontando pra dev — atualizamos no fim).
|
||||
5. Uma instalação do CRM já de pé, com `WAHA_API_BASE_URL`, `WAHA_API_KEY`, `WAHA_WEBHOOK_BASE_URL` e `WAHA_HMAC_SECRET` no `.env` dela (ainda apontando pra dev — atualizamos no fim).
|
||||
|
||||
---
|
||||
|
||||
@@ -152,7 +152,7 @@ services:
|
||||
|
||||
```bash
|
||||
WAHA_API_KEY=<plaintext gerado novo, 64 chars hex>
|
||||
WAHA_WEBHOOK_BASE_URL=https://app.deskcomm.com.br
|
||||
WAHA_WEBHOOK_BASE_URL=https://<dominio-da-sua-instalacao-do-crm>
|
||||
WAHA_HMAC_SECRET=<32 bytes random distinto da api key>
|
||||
```
|
||||
|
||||
@@ -190,8 +190,8 @@ server {
|
||||
ssl_certificate /etc/letsencrypt/live/waha.deskcomm.com.br/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/waha.deskcomm.com.br/privkey.pem;
|
||||
|
||||
# Egress allowlist — só Vercel pode chamar.
|
||||
include /etc/nginx/conf.d/vercel-egress-allowlist.conf;
|
||||
# Egress allowlist — só o servidor onde o CRM roda pode chamar.
|
||||
include /etc/nginx/conf.d/crm-egress-allowlist.conf;
|
||||
deny all;
|
||||
|
||||
proxy_buffering off; # SSE / streaming WAHA
|
||||
@@ -215,23 +215,14 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
`/etc/nginx/conf.d/vercel-egress-allowlist.conf` — atualizado por cron diário:
|
||||
`/etc/nginx/conf.d/crm-egress-allowlist.conf` — o IP público do servidor onde o CRM roda:
|
||||
|
||||
```bash
|
||||
sudo tee /usr/local/bin/refresh-vercel-cidrs.sh > /dev/null <<'EOF'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
TMP=$(mktemp)
|
||||
curl -s https://api.vercel.com/v1/edge/cidrs | jq -r '.cidrs[]' | sed 's/^/allow /;s/$/;/' > "$TMP"
|
||||
sudo mv "$TMP" /etc/nginx/conf.d/vercel-egress-allowlist.conf
|
||||
echo "allow <IP_DO_SERVIDOR_DO_CRM>;" | sudo tee /etc/nginx/conf.d/crm-egress-allowlist.conf
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
EOF
|
||||
sudo chmod +x /usr/local/bin/refresh-vercel-cidrs.sh
|
||||
sudo /usr/local/bin/refresh-vercel-cidrs.sh
|
||||
echo "0 4 * * * deskcomm /usr/local/bin/refresh-vercel-cidrs.sh" | sudo tee /etc/cron.d/vercel-cidrs
|
||||
```
|
||||
|
||||
> Endpoint da Vercel pode mudar. Se a API responder 404, fallback é colar manualmente os ranges de https://vercel.com/docs/limits e revisar trimestralmente.
|
||||
> É um endereço só, escrito à mão: não há lista para sincronizar nem cron para manter. Quem o muda é você, ao mover o CRM de servidor — quando isso acontecer, reescreva este arquivo e recarregue o Nginx. Para descobrir o IP de saída, rode de dentro do próprio servidor do CRM: `curl -s https://ifconfig.me`.
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/waha /etc/nginx/sites-enabled/waha
|
||||
@@ -316,18 +307,28 @@ Better Stack ou Datadog Agent → `docker logs deskcomm-waha`. Sem isso, logs fi
|
||||
|
||||
---
|
||||
|
||||
## 9. Atualizar Vercel envs
|
||||
## 9. Apontar o CRM para este WAHA
|
||||
|
||||
No painel Vercel → Project Settings → Environment Variables (escopo: **Production** apenas):
|
||||
As variáveis do CRM vivem no `.env` da instalação dele — não num painel. Edite lá:
|
||||
|
||||
```
|
||||
WAHA_API_BASE_URL=https://waha.deskcomm.com.br
|
||||
WAHA_API_KEY=<mesmo plaintext do .env do VPS>
|
||||
WAHA_WEBHOOK_BASE_URL=https://app.deskcomm.com.br
|
||||
WAHA_WEBHOOK_BASE_URL=https://<dominio-da-sua-instalacao-do-crm>
|
||||
WAHA_HMAC_SECRET=<mesmo do VPS>
|
||||
```
|
||||
|
||||
Redeploy da branch `main` aplica.
|
||||
Env var é lida no boot: editar o `.env` não muda nada até os contêineres serem recriados.
|
||||
|
||||
Não tente descobrir quais são eles com um `grep env_file`: esse grep não enxerga o
|
||||
serviço `waha` do compose, que recebe `WAHA_WEBHOOK_BASE_URL` e `WAHA_HMAC_SECRET` por
|
||||
interpolação (`${...}` dentro do bloco `environment:`), sem `env_file` nenhum — a
|
||||
resposta sai incompleta com cara de completa.
|
||||
|
||||
Numa instalação real, quem recria tudo é `bash hostgator-setup-kit/update.sh`: ele roda
|
||||
o `up -d` sem nomear serviço. Subindo à mão, o comando está em [`deploy.md`](deploy.md) —
|
||||
numa VPS com proxy reverso próprio o `up -d` precisa dos **dois** arquivos de compose,
|
||||
senão o domínio passa a responder 404.
|
||||
|
||||
---
|
||||
|
||||
@@ -338,13 +339,13 @@ Redeploy da branch `main` aplica.
|
||||
- [ ] UFW ativo, só 22/80/443
|
||||
- [ ] SSH password disabled, root login disabled
|
||||
- [ ] fail2ban com jails de SSH + nginx-http-auth
|
||||
- [ ] Egress allowlist Nginx atualizando via cron
|
||||
- [ ] Egress allowlist Nginx com o IP do servidor do CRM
|
||||
- [ ] TLS válido (testar `https://www.ssllabs.com/ssltest/` ≥ A)
|
||||
- [ ] Backup `restic` rodando + restore drill executado uma vez
|
||||
- [ ] UptimeRobot configurado
|
||||
- [ ] Watchdog cron de 1min ativo
|
||||
- [ ] Sentry release tagging do app capturando erros do `lib/waha/client`
|
||||
- [ ] Vercel envs apontando pro domínio público
|
||||
- [ ] `.env` do CRM apontando pro domínio público do WAHA
|
||||
- [ ] Webhook entrante funcionando (mensagem de teste WhatsApp → aparece na Inbox)
|
||||
- [ ] API key WAHA documentada em 1Password com data de rotação +90d
|
||||
|
||||
@@ -369,7 +370,7 @@ Redeploy da branch `main` aplica.
|
||||
|
||||
| Sintoma | Diagnóstico | Fix |
|
||||
|---|---|---|
|
||||
| App recebe 401 do WAHA | Env var `WAHA_API_KEY` desalinhada (Vercel vs VPS) | Confirmar plaintext idêntico nos dois lados |
|
||||
| App recebe 401 do WAHA | Env var `WAHA_API_KEY` desalinhada (`.env` do CRM vs `.env` do WAHA) | Confirmar plaintext idêntico nos dois lados |
|
||||
| WAHA cria session mas não inicia | `start: true` ignorado em algumas versões | Chamar `POST /api/sessions/:name/start` explicitamente |
|
||||
| Webhook não chega | Nginx allowlist bloqueando, ou Cloudflare Proxy ON | `tail -f /var/log/nginx/access.log` + desligar proxy CF |
|
||||
| QR expira sempre | RTT alto, ou clock drift no VPS | `timedatectl set-ntp true`, conferir RTT pro `web.whatsapp.com` |
|
||||
@@ -380,4 +381,5 @@ Redeploy da branch `main` aplica.
|
||||
|
||||
## 13. Histórico de decisão
|
||||
|
||||
- **2026-09-17** — O passo 9 deixou de mandar editar env var em painel de hospedagem: o CRM é self-host, e as variáveis dele vivem no `.env` da instalação. A allowlist de egress do Nginx passou a liberar o IP do servidor onde o CRM roda, no lugar dos ranges da plataforma — que eram atualizados por cron diário e sobrescreveriam qualquer ajuste manual.
|
||||
- **2026-05-04** — Trocamos referências Hetzner→Hostgator nos docs por parceria comercial existente. Custo subiu (~$5 → ~$28) mas latência BR melhora pareamento e suporte ticketing fica em PT-BR. Hetzner mantido como plano B documentado em §1.
|
||||
|
||||
@@ -1603,7 +1603,7 @@ export function logWithCtx(orgId: string, requestId: string) {
|
||||
|
||||
### 11.3 Métricas custom
|
||||
|
||||
Emitidas via OpenTelemetry → Vercel Observability ou Grafana Cloud:
|
||||
Emitidas via OpenTelemetry → o coletor da instalação (Grafana Cloud, Sentry ou equivalente):
|
||||
|
||||
| Métrica | Tipo | Tags |
|
||||
|---|---|---|
|
||||
|
||||
@@ -152,10 +152,10 @@ volumes:
|
||||
|
||||
### 2.2 Variáveis de ambiente
|
||||
|
||||
A `WAHA_API_KEY` do servidor é o **hash SHA512 hex (lowercase) do plaintext**. O backend DeskcommCRM guarda **só** o plaintext em Vercel Encrypted Env Var; nunca a hash duplicada. Geração:
|
||||
A `WAHA_API_KEY` do servidor é o **hash SHA512 hex (lowercase) do plaintext**. O backend DeskcommCRM guarda **só** o plaintext no `.env` da instalação (modo 0600, como o `install.sh` o grava); nunca a hash duplicada. Geração:
|
||||
|
||||
```bash
|
||||
# Gerar plaintext seguro (nunca commitar; armazenar em 1Password/Vercel)
|
||||
# Gerar plaintext seguro (nunca commitar; guardar num gerenciador de senhas)
|
||||
PLAINTEXT=$(openssl rand -hex 32)
|
||||
echo "Plaintext (env do app): $PLAINTEXT"
|
||||
|
||||
@@ -167,7 +167,7 @@ echo -n "$PLAINTEXT" | sha512sum | awk '{print $1}'
|
||||
`.env.production.example` (não commitar valores reais):
|
||||
|
||||
```dotenv
|
||||
# === WAHA server (no host do WAHA, NÃO no Vercel) ===
|
||||
# === WAHA server (no host do WAHA, NÃO no `.env` do app) ===
|
||||
WAHA_API_KEY_SHA512=<sha512 hex do plaintext>
|
||||
WAHA_DASHBOARD_USERNAME=admin_deskcomm
|
||||
WAHA_DASHBOARD_PASSWORD=<senha forte gerada>
|
||||
@@ -176,7 +176,7 @@ WAHA_S3_BUCKET=deskcomm-waha-media
|
||||
WAHA_S3_ACCESS_KEY_ID=<r2/s3 access key>
|
||||
WAHA_S3_SECRET_ACCESS_KEY=<r2/s3 secret>
|
||||
|
||||
# === Backend Vercel (Encrypted Env) ===
|
||||
# === Backend DeskcommCRM (`.env` da instalação) ===
|
||||
WAHA_API_KEY=<plaintext — só aqui>
|
||||
WAHA_BASE_URL=https://waha.deskcomm.internal
|
||||
WAHA_WEBHOOK_PUBLIC_BASE_URL=https://api.deskcomm.com
|
||||
@@ -233,8 +233,9 @@ server {
|
||||
|
||||
client_max_body_size 64M; # mídia até 50MB + overhead
|
||||
|
||||
# Allowlist do Vercel (egress IPs) — atualizar via cron
|
||||
include /etc/nginx/conf.d/vercel-egress-allowlist.conf;
|
||||
# Allowlist de egress: só o servidor onde o CRM roda chama este WAHA
|
||||
# (receita viva em docs/runbooks/waha-hostgator.md)
|
||||
include /etc/nginx/conf.d/crm-egress-allowlist.conf;
|
||||
deny all;
|
||||
|
||||
location / {
|
||||
@@ -1679,21 +1680,16 @@ Pra distinguir múltiplos celulares vinculados, fora do escopo MVP (WAHA não ex
|
||||
|
||||
---
|
||||
|
||||
## 10. Crons (Vercel Cron)
|
||||
## 10. Crons
|
||||
|
||||
`vercel.json`:
|
||||
A cadência vigente não mora neste documento: ela vive em duas fontes espelhadas — `docker/scheduler/entrypoint.sh` (o serviço `scheduler` do compose, que é o caminho self-host) e `vercel.ts` na raiz (para quem hospeda a própria instalação na Vercel). `tests/unit/cron-routes-scheduled.test.ts` confere as duas uma contra a outra e contra o diretório `app/api/v1/cron/`, e reprova divergência. Para ver a lista de hoje:
|
||||
|
||||
```json
|
||||
{
|
||||
"crons": [
|
||||
{ "path": "/api/cron/wa/sync-sessions", "schedule": "* * * * *" },
|
||||
{ "path": "/api/cron/wa/recover-stuck-messages", "schedule": "* * * * *" },
|
||||
{ "path": "/api/cron/wa/process-pending-webhooks", "schedule": "* * * * *" }
|
||||
]
|
||||
}
|
||||
```bash
|
||||
grep -oE 'api/v1/cron/[a-z0-9-]+' docker/scheduler/entrypoint.sh | sort -u
|
||||
```
|
||||
|
||||
Auth via header `Authorization: Bearer ${INTERNAL_CRON_SECRET}` + `x-vercel-cron: 1`.
|
||||
O que esse comando devolve são os crons em vigor. **As subseções abaixo são planejamento, e nem toda
|
||||
rota nomeada nelas existe** — confira cada nome contra `ls app/api/v1/cron`. Toda rota de cron aceita `Authorization: Bearer <segredo>`, conferido contra `INTERNAL_CRON_SECRET` e `INTERNAL_SECRET`; parte delas usa o helper `autorizaCron()` (`lib/auth/cron-auth.ts`), que também aceita `x-cron-secret` — para ver quais, `grep -rl autorizaCron app/api/v1/cron/`.
|
||||
|
||||
### 10.1 `sync-sessions`
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ Conectar o DeskcommCRM ao backend de e-commerce do tenant (Nuvemshop no MVP) de
|
||||
|---|---|---|
|
||||
| App embedded vs External | **External** | Mais flexibilidade de UI custom (admin DeskcommCRM tem UX própria); evita iframe sandbox; consent renderizado no domínio Nuvemshop é suficiente |
|
||||
| Lib Nuvemshop | **Wrapper próprio em `lib/nuvemshop/`** | SDK oficial PT-BR é incompleto pra webhooks LGPD; wrapper fino sobre `fetch` permite tipagem rigorosa e telemetria custom |
|
||||
| Worker runtime | **Vercel Cron + Upstash QStash** pra jobs longos (>30s); Edge Functions pros receivers | Hobby/Pro têm limite de 5/15 min; QStash dá retry e dedupe de fila; mantém stack Vercel-only |
|
||||
| Worker runtime | ~~**Vercel Cron + Upstash QStash** pra jobs longos (>30s); Edge Functions pros receivers~~ — **superado**: os serviços `worker` e `scheduler` do `docker-compose.prod.yml` (Spec 07) | A justificativa original ("mantém stack Vercel-only") caiu junto com a premissa: o produto é distribuído como self-host, sem teto de plano serverless; retry e dedupe vivem no `event_log` |
|
||||
| Particionamento `webhook_events_log` | **Por mês (`PARTITION BY RANGE (received_at)`)** | Hot 90 dias acessível; partições antigas detacháveis pra cold S3 |
|
||||
| Roteamento receiver | **`/api/v1/webhooks/nuvemshop/<event>?t=<webhook_path_token>`** | Token por tenant gerado no onboarding (Spec 01); evita subdomínio dinâmico |
|
||||
| Mapeamento status | Tabela canônica + override em `tenant_integrations.store_metadata.stage_mapping` | Configurável por tenant (PRD §3.10) |
|
||||
@@ -795,7 +795,8 @@ export async function postConnect({ integrationId, organizationId }: PostConnect
|
||||
|
||||
### 4.7 Refresh token rotation worker
|
||||
|
||||
Cron `*/15 * * * *` (Vercel Cron) varre `tenant_integrations` com `expires_at < now() + interval '30 minutes'`:
|
||||
Cron `*/15 * * * *` **previsto** para o serviço `scheduler` — é planejamento: nenhuma rota de refresh
|
||||
existe hoje, confira com `ls app/api/v1/cron` — varrendo `tenant_integrations` com `expires_at < now() + interval '30 minutes'`:
|
||||
|
||||
```typescript
|
||||
export async function refreshExpiringTokens() {
|
||||
|
||||
@@ -19,7 +19,7 @@ related:
|
||||
|
||||
# Spec 07 — Event Log + Workers + Crons (transversal)
|
||||
|
||||
> Esta spec define o **bus interno** do DeskcommCRM: como módulos publicam eventos no banco, como workers consomem, quais crons rodam no Vercel, como tratamos retries, dead-letter, idempotência e observabilidade. É **transversal** — todos os outros sub-PRDs (02–06) emitem ou consomem deste bus.
|
||||
> Esta spec define o **bus interno** do DeskcommCRM: como módulos publicam eventos no banco, como workers consomem, quais crons rodam (no serviço `scheduler` do `docker-compose.prod.yml`, na infraestrutura de quem instala), como tratamos retries, dead-letter, idempotência e observabilidade. É **transversal** — todos os outros sub-PRDs (02–06) emitem ou consomem deste bus.
|
||||
|
||||
---
|
||||
|
||||
@@ -65,7 +65,7 @@ A solução adotada:
|
||||
LOCKED, batch) não crítico)
|
||||
│ │
|
||||
▼ ▼
|
||||
Vercel Cron / Background Side-effects:
|
||||
Cron / worker Side-effects:
|
||||
Functions - WAHA send
|
||||
- AI bot
|
||||
- Webhooks out
|
||||
@@ -485,7 +485,14 @@ await boss.start();
|
||||
|
||||
## 6. Worker Implementations
|
||||
|
||||
> Padrão: cada worker = arquivo TS em `workers/`, deploy como **Vercel Background Function** (long-running) OU **Edge Function Supabase agendada**. Para o MVP usamos Vercel Background Functions com **fluid compute**.
|
||||
> Padrão: cada worker = arquivo TS em `workers/`, e o cabeçalho do arquivo diz por qual caminho ele roda.
|
||||
> O `workers/agent-worker/main.ts` é o entrypoint do serviço `worker` do `docker-compose.prod.yml`
|
||||
> (é o `CMD` do `Dockerfile.worker`). Os `workers/*.handler.ts` são consumidores de `event_log`:
|
||||
> registrados em `lib/event-log/register-handlers.ts` e drenados por dois processos em paralelo — o
|
||||
> laço do próprio `worker` (`lib/event-log/drain-loop.ts`) e o cron `app/api/v1/cron/event-log-drain`,
|
||||
> no processo do `app`. Há ainda worker com rota de cron dedicada — `workers/storage-cleanup-worker.ts`
|
||||
> é drenado por `app/api/v1/cron/storage-redaction`. As imagens são publicadas pelo CI e os contêineres sobem na infraestrutura de
|
||||
> quem instala; ver [`docs/doctrine/packaging.md`](../doctrine/packaging.md).
|
||||
|
||||
### 6.1 `whatsapp-send-worker`
|
||||
|
||||
@@ -548,25 +555,18 @@ await boss.start();
|
||||
|
||||
---
|
||||
|
||||
## 7. Crons (Vercel Cron)
|
||||
## 7. Crons
|
||||
|
||||
Configuração em `vercel.json` (ou `app/api/cron/[name]/route.ts` com schedule). Todos os crons batem em `/api/cron/{name}` autenticados via `CRON_SECRET` (header `Authorization: Bearer ...`).
|
||||
A lista vigente não mora neste documento: ela vive em duas fontes espelhadas — `docker/scheduler/entrypoint.sh` (o serviço `scheduler` do compose, que é o caminho self-host) e `vercel.ts` na raiz (para quem hospeda a própria instalação na Vercel). `tests/unit/cron-routes-scheduled.test.ts` confere as duas uma contra a outra e contra o diretório `app/api/v1/cron/`, e reprova divergência. Para ver a de hoje:
|
||||
|
||||
```json
|
||||
{
|
||||
"crons": [
|
||||
{ "path": "/api/cron/sync-sessions", "schedule": "* * * * *" },
|
||||
{ "path": "/api/cron/recover-stuck-messages", "schedule": "* * * * *" },
|
||||
{ "path": "/api/cron/process-pending-webhooks", "schedule": "* * * * *" },
|
||||
{ "path": "/api/cron/health-check-integrations","schedule": "*/15 * * * *" },
|
||||
{ "path": "/api/cron/oauth-refresh-tokens", "schedule": "0 * * * *" },
|
||||
{ "path": "/api/cron/daily-budget-reset", "schedule": "0 3 * * *" },
|
||||
{ "path": "/api/cron/prune-old-media", "schedule": "30 3 * * *" }
|
||||
]
|
||||
}
|
||||
```bash
|
||||
grep -oE 'api/v1/cron/[a-z0-9-]+' docker/scheduler/entrypoint.sh | sort -u
|
||||
```
|
||||
|
||||
> Nota: Vercel cron usa **UTC**. `0 3 * * *` ≈ 00:00 BRT (UTC-3).
|
||||
O que esse comando devolve são os crons em vigor. **As subseções abaixo são planejamento, e nem toda
|
||||
rota nomeada nelas existe** — confira cada nome contra `ls app/api/v1/cron`. Toda rota de cron aceita `Authorization: Bearer <segredo>`, conferido contra `INTERNAL_CRON_SECRET` e `INTERNAL_SECRET`; parte delas usa o helper `autorizaCron()` (`lib/auth/cron-auth.ts`), que também aceita `x-cron-secret` — para ver quais, `grep -rl autorizaCron app/api/v1/cron/`.
|
||||
|
||||
> Nota: o `crond` do scheduler roda em **UTC** (`TZ=UTC` no `Dockerfile.scheduler`). `0 3 * * *` ≈ 00:00 BRT (UTC-3).
|
||||
|
||||
### 7.1 `sync-sessions` (1min)
|
||||
Verifica WAHA sessions ativas vs `whatsapp_sessions` no DB, sincroniza status (`WORKING`, `STOPPED`, `FAILED`), emite `system.session_changed` quando status diverge.
|
||||
@@ -804,7 +804,7 @@ Ordem recomendada (sob `supabase/migrations/`):
|
||||
7. `2026XX07_pgboss_schema.sql` — `create schema pgboss;` + grants (pg_boss cria tabelas no init runtime).
|
||||
8. `2026XX08_event_archive_storage_bucket.sql` — bucket Storage `event-archive` private.
|
||||
9. `2026XX09_webhook_subscriptions_columns.sql` — `consecutive_failures`, `enabled`.
|
||||
10. `2026XX10_event_log_reaper_cron.sql` — `pg_cron` (se disponível) ou Vercel cron equivalente.
|
||||
10. `2026XX10_event_log_reaper_cron.sql` — `pg_cron` (se disponível) ou rota de cron equivalente.
|
||||
|
||||
Cada migration tem **rollback** (`down.sql`) testado em branch Supabase antes de merge.
|
||||
|
||||
|
||||
@@ -21,6 +21,19 @@ referencias:
|
||||
|
||||
> Documento transversal que rege topologia de produção, ambiente local, secrets, CI/CD, observability, alertas, runbooks, performance targets, disaster recovery e custo. Toda decisão arquitetural conflitante com este documento exige justificativa explícita no PRD/Spec de origem.
|
||||
|
||||
> **Nota de 2026-09-17.** A topologia de infraestrutura descrita abaixo é a da fase em que o CRM
|
||||
> era hospedado num PaaS. O produto é self-host: `app`, `worker` e `scheduler` sobem como
|
||||
> contêineres na infraestrutura de quem instala (`docker-compose.prod.yml`), e o que o merge na
|
||||
> `main` dispara neste repositório é a publicação das imagens
|
||||
> (`.github/workflows/publish-image.yml`), não um deploy de aplicação. O que vale hoje para
|
||||
> produção está em [`docs/runbooks/deploy.md`](../runbooks/deploy.md) e na seção "Packaging e
|
||||
> distribuição" do [`CLAUDE.md`](../../CLAUDE.md), com a lei completa em
|
||||
> [`docs/doctrine/packaging.md`](../doctrine/packaging.md). Leia como registro daquela fase toda
|
||||
> seção de infraestrutura abaixo que não disser, no próprio texto, o que vale hoje — várias já
|
||||
> foram trazidas para o presente e dizem isso onde estão. Uma ressalva é de escolha, não de
|
||||
> hospedagem: o provedor de modelos do §5.4 (Vercel AI Gateway) segue na Stack canônica do
|
||||
> [`CLAUDE.md`](../../CLAUDE.md) — ele serve a IA, não hospeda o CRM.
|
||||
|
||||
---
|
||||
|
||||
## 1. Visão Geral
|
||||
@@ -33,21 +46,28 @@ referencias:
|
||||
- **Mean time to recovery (MTTR) ≤ 30 min** pros 6 incidentes mais frequentes documentados em runbooks.
|
||||
|
||||
### 1.2 Princípios não-negociáveis
|
||||
1. **Stateless app.** Nada de estado em filesystem do Vercel. Toda persistência vai pra Supabase, Storage, Upstash ou volume Docker do WAHA.
|
||||
1. **Stateless app.** Nada de estado no filesystem do app. Toda persistência vai pra Supabase, Storage, Upstash ou volume Docker do WAHA.
|
||||
2. **Trigger NUNCA faz HTTP.** Workers consomem `event_log` via Realtime/cron — herdado da referência.
|
||||
3. **Service role bypassa RLS — filtro manual obrigatório.** Em todo handler que usa admin client, `organization_id` é resolvido a partir de cookie/JWT/path token, NUNCA do body.
|
||||
4. **Encryption-at-rest separada por contexto.** Chaves distintas pra CPF, OAuth tokens Nuvemshop, e WAHA BYO API keys.
|
||||
5. **Backups verificados.** Restore drill trimestral em ambiente de staging; backup que não foi restaurado é teoria.
|
||||
6. **Logs estruturados em JSON.** Nada de `console.log("erro: " + e)`.
|
||||
7. **Deploy = git push.** Sem `vercel deploy` manual em produção. Tudo passa por main.
|
||||
7. **Nada de deploy manual.** Produção só recebe o que passou pela `main`.
|
||||
|
||||
### 1.3 Estados e ambientes
|
||||
| Ambiente | Branch | Domínio | Supabase | WAHA | Sentry env |
|
||||
|---|---|---|---|---|---|
|
||||
| Production | `main` | `app.deskcomm.com.br` + `admin.deskcomm.com.br` | projeto Pro `deskcomm-prod` | `waha.deskcomm.com.br` (Hostgator) | `production` |
|
||||
| Staging | `staging` | `staging.deskcomm.com.br` | projeto Free `deskcomm-staging` | `waha-staging.deskcomm.com.br` (Hostgator mesmo VPS, container separado) | `staging` |
|
||||
| Preview | qualquer PR | `*.vercel.app` | projeto Free `deskcomm-preview` (compartilhado) | mock/staging | `preview` |
|
||||
| Local dev | local | `localhost:3000` | `supabase start` (Docker) | `localhost:3000` (compose) | `development` |
|
||||
|
||||
A tabela original amarrava branch, domínio e deploy de preview por PR à hospedagem num PaaS (ver a nota do topo). **Não há ambiente compartilhado de staging nem deploy de preview por PR neste repositório:** um PR é validado pelos checks do CI e pelo ambiente local.
|
||||
|
||||
| Ambiente | Onde roda | WAHA | Sentry env |
|
||||
|---|---|---|---|
|
||||
| Produção | instalação em VPS (`install.sh` / `update.sh`; imagens publicadas pelo CI) | serviço `waha` do compose da instalação | `production` |
|
||||
| Local dev | `localhost:3000`; `supabase start` (Docker) | `localhost:3030` (serviço `waha` do `docker-compose.yml`, que publica `3030:3000`) | `development` |
|
||||
|
||||
Para saber quais checks a `main` exige, pergunte à fonte em vez de a uma lista escrita:
|
||||
|
||||
```bash
|
||||
gh api repos/<owner>/<repo>/branches/main/protection --jq '.required_status_checks.contexts|join(", ")'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -55,13 +75,15 @@ referencias:
|
||||
|
||||
### 2.1 Componentes
|
||||
|
||||
> **Onde isto roda hoje:** o Next.js (frontend, Route Handlers `/api/v1/*` e os webhook receivers) roda no serviço `app`; os workers, no serviço `worker`; os crons, no serviço `scheduler` — os três do `docker-compose.prod.yml`, na infraestrutura de quem instala. O bloco abaixo é o registro da fase hospedada.
|
||||
|
||||
#### Vercel (Next.js app + API + crons)
|
||||
- **Plano:** Pro ($20/mês/seat).
|
||||
- **Região:** `gru1` (São Paulo) prioritária; fallback `iad1`. Edge functions só onde latência precisa <50ms (rate-limit middleware).
|
||||
- **Hospeda:**
|
||||
- Next.js 14+ App Router (frontend `/`, super-admin `/admin`)
|
||||
- Route Handlers `/api/v1/*`
|
||||
- 7 Vercel Crons (lista exaustiva em §5.3)
|
||||
- Vercel Crons (ver §5.3)
|
||||
- Webhook receivers WAHA (`/api/webhooks/waha/[sessionId]`) e Nuvemshop (`/api/webhooks/nuvemshop/[event]`)
|
||||
- **Não hospeda:** workers de longa duração (>60s no Pro com Fluid Compute), processamento síncrono de mídia >25MB, base vetorial (vai pro Postgres com pgvector).
|
||||
|
||||
@@ -366,10 +388,9 @@ create extension if not exists btree_gin with schema extensions; -- tags i
|
||||
Configuração via Supabase Dashboard ou `supabase/config.toml`:
|
||||
```toml
|
||||
[auth]
|
||||
site_url = "https://app.deskcomm.com.br"
|
||||
site_url = "https://<dominio-da-sua-instalacao>"
|
||||
additional_redirect_urls = [
|
||||
"https://app.deskcomm.com.br/auth/callback",
|
||||
"https://admin.deskcomm.com.br/auth/callback",
|
||||
"https://<dominio-da-sua-instalacao>/auth/callback",
|
||||
]
|
||||
jwt_expiry = 3600 # 1h, refresh token rotation ativa
|
||||
refresh_token_rotation_enabled = true
|
||||
@@ -511,22 +532,25 @@ Estratégia:
|
||||
|
||||
Vars NUNCA commitadas: tudo prefixado `*_KEY`, `*_SECRET`, `*_TOKEN`, `DATABASE_URL`, `*_DSN`. Pre-commit gitleaks bloqueia (§7.5).
|
||||
|
||||
### 5.3 Vercel Cron schedules (lista exaustiva)
|
||||
### 5.3 Crons — a lista vigente não mora neste documento
|
||||
|
||||
| # | Path | Schedule (UTC) | Descrição | Owner PRD | Timeout |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `/api/cron/recover-stuck-messages` | `*/5 * * * *` | Mensagens em `status='sending'` há >5min viram `failed` + alerta | 03 | 60s |
|
||||
| 2 | `/api/cron/sync-waha-sessions` | `*/2 * * * *` | Pull `GET /api/sessions` do WAHA e atualiza `channel_sessions.status` | 03 | 60s |
|
||||
| 3 | `/api/cron/process-pending-webhooks` | `*/1 * * * *` | Processa webhooks deduplicados com `status='pending'` (Nuvemshop + WAHA) | 03/06 | 60s |
|
||||
| 4 | `/api/cron/lgpd-sla-warning` | `0 9 * * *` | Alerta em `data_request` ainda não respondidas em D+5 | 06 | 60s |
|
||||
| 5 | `/api/cron/event-log-cleanup` | `0 3 * * *` | Move events >90d pra `event_log_archive`, depois pra S3 cold storage | 01 | 300s |
|
||||
| 6 | `/api/cron/nuvemshop-sync-incremental` | `*/15 * * * *` | Pull diff de orders/products desde último checkpoint | 06 | 300s |
|
||||
| 7 | `/api/cron/audit-log-archive` | `0 4 * * 0` | Semanal: comprime audit log antigo +18m e move pra S3 | 01 | 300s |
|
||||
São duas fontes espelhadas: `docker/scheduler/entrypoint.sh` (o serviço `scheduler` do compose, que é o caminho self-host) e `vercel.ts` na raiz (para quem hospeda a própria instalação na Vercel). `tests/unit/cron-routes-scheduled.test.ts` confere as duas uma contra a outra e contra o diretório `app/api/v1/cron/`, e reprova divergência. Para ver a lista de hoje:
|
||||
|
||||
```bash
|
||||
grep -oE 'api/v1/cron/[a-z0-9-]+' docker/scheduler/entrypoint.sh | sort -u
|
||||
```
|
||||
|
||||
Toda rota de cron aceita `Authorization: Bearer <segredo>`, conferido contra `INTERNAL_CRON_SECRET` e
|
||||
`INTERNAL_SECRET` — qualquer um dos dois que esteja preenchido serve. Parte delas usa o helper
|
||||
`autorizaCron()` (`lib/auth/cron-auth.ts`), que também aceita `x-cron-secret`; para ver quais,
|
||||
`grep -rl autorizaCron app/api/v1/cron/`. Em produção, se houver `CRON_SECRET` no ambiente, `lib/env.ts` o copia para
|
||||
`INTERNAL_CRON_SECRET` — **não** para `INTERNAL_SECRET`, que nunca é sobrescrito; é nas duas primeiras
|
||||
que se depura um 403 de cron.
|
||||
|
||||
Cada cron handler:
|
||||
1. Valida header `Authorization: Bearer ${CRON_SECRET}` (Vercel injeta automático).
|
||||
1. Valida o header acima antes de qualquer trabalho — quem passa pelo `autorizaCron()` falha fechado quando nenhum segredo está configurado.
|
||||
2. Loga `event_log` row `cron.{name}.started` no início e `cron.{name}.completed` no fim, com `metadata.duration_ms` e `metadata.rows_processed`.
|
||||
3. Se falha: Sentry capture + `cron.{name}.failed` no event_log + retorno 500 (Vercel marca como failed, dashboard alerta).
|
||||
3. Se falha: Sentry capture + `cron.{name}.failed` no event_log + retorno 500.
|
||||
|
||||
### 5.4 AI Gateway integration
|
||||
|
||||
@@ -587,7 +611,7 @@ services:
|
||||
WAHA_DASHBOARD_ENABLED: "false" # desabilita em prod
|
||||
WAHA_SWAGGER_ENABLED: "false"
|
||||
WHATSAPP_DEFAULT_ENGINE: NOWEB
|
||||
WHATSAPP_HOOK_URL: https://app.deskcomm.com.br/api/webhooks/waha
|
||||
WHATSAPP_HOOK_URL: https://<dominio-da-sua-instalacao>/api/webhooks/waha
|
||||
WHATSAPP_HOOK_EVENTS: "message.any,session.status,message.ack,call.received,group.v2.join,group.v2.leave"
|
||||
WHATSAPP_HOOK_HMAC_ALGORITHM: SHA512
|
||||
WHATSAPP_HOOK_HMAC_KEY: ${WAHA_WEBHOOK_HMAC_KEY}
|
||||
@@ -886,20 +910,25 @@ repos:
|
||||
|
||||
## 8. CI/CD
|
||||
|
||||
### 8.1 Vercel Git integration
|
||||
- GitHub repo conectado ao project Vercel via OAuth.
|
||||
- Trigger: push em `main` → deploy `Production`. Push em qualquer outra branch → `Preview`.
|
||||
- Deploy protection: branch `main` requer (a) PR aprovado, (b) checks verdes, (c) 1 reviewer mínimo.
|
||||
### 8.1 Integração com o GitHub
|
||||
- **Não há projeto de PaaS ligado a este repositório:** push e PR não disparam deploy de aplicação.
|
||||
- O que o merge na `main` dispara é `.github/workflows/publish-image.yml`, que constrói e publica as imagens `app`, `worker` e `scheduler` no GHCR; cada instalação puxa a imagem no próprio servidor.
|
||||
- Deploy protection: a `main` exige PR com checks verdes. Para ver quais são obrigatórios hoje:
|
||||
|
||||
```bash
|
||||
gh api repos/<owner>/<repo>/branches/main/protection --jq '.required_status_checks.contexts'
|
||||
```
|
||||
|
||||
### 8.2 Preview deployments
|
||||
- 1 deploy por commit. URL `deskcomm-app-git-{branch}-{team}.vercel.app`.
|
||||
- Cada Preview cria uma branch Supabase (`supabase branch create --name preview-{pr}`) automaticamente via GitHub Action.
|
||||
- Comentário automático no PR com URL Preview + link Sentry environment.
|
||||
|
||||
### 8.3 Rolling releases
|
||||
Vercel Pro com Skew Protection ativado garante que clients antigos não chamem API nova quebrada durante deploy. Toggle no Dashboard: **Settings → Deployment → Skew Protection** (max age 12h).
|
||||
Não existem neste repositório: não há URL de preview por commit, nem branch Supabase por PR, nem comentário automático com o link. Um PR é validado pelos checks do CI e pelo ambiente local — a receita de ambiente fresco está no [`CLAUDE.md`](../../CLAUDE.md), na doutrina de QA Visual. (Um fork que ligue o próprio projeto de hospedagem volta a ter preview; isso é configuração dele, não deste repositório.)
|
||||
|
||||
Para rollback: `vercel rollback <previous-deployment-url>` ou via UI. Banco de dados não rollbacka — toda migration é forward-compatible (regra de ouro: nunca DROP COLUMN no mesmo PR que adiciona uso novo; espalhar em 2 PRs com 1 release no meio).
|
||||
### 8.3 Atualização e rollback
|
||||
|
||||
Atualizar troca o contêiner do app. **Não há proteção de skew no self-host:** nada neste repositório
|
||||
força o cliente a recarregar quando a imagem muda, então uma aba já aberta segue com o bundle antigo
|
||||
até o próximo carregamento — bundle antigo chamando API nova continua sendo um risco de release aqui,
|
||||
não uma hipótese coberta pela plataforma. O rollback é de **imagem**, não de deploy — voltar `APP_IMAGE` (e `WORKER_IMAGE`/`SCHEDULER_IMAGE`) para a versão anterior no `.env` e subir de novo; é o que o `agent.sh` do kit faz sozinho quando a versão nova não sobe, reescrevendo o `.env` para a volta não ser desfeita no `up -d` seguinte. Banco de dados não rollbacka — toda migration é forward-compatible (regra de ouro: nunca DROP COLUMN no mesmo PR que adiciona uso novo; espalhar em 2 PRs com 1 release no meio).
|
||||
|
||||
### 8.4 Migrations runner (Supabase CLI no CI)
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ create table public.ai_provider_credentials (
|
||||
provider text not null check (provider in ('anthropic', 'openai', 'google')),
|
||||
label text not null, -- "Produção", "Testes", etc
|
||||
|
||||
-- API key cifrada (AES-GCM, key em KMS/Vercel KV secret)
|
||||
-- API key cifrada (AES-GCM, chave no `.env` da instalação)
|
||||
api_key_encrypted bytea not null,
|
||||
api_key_iv bytea not null, -- 12 bytes IV
|
||||
api_key_tag bytea not null, -- 16 bytes auth tag
|
||||
@@ -535,7 +535,7 @@ if (!isGroup && !fromMe && message.kind === 'inbound') {
|
||||
### 5.1 Cron (Spec 07)
|
||||
|
||||
```
|
||||
schedule: "*/5 * * * * *" (a cada 5s; Vercel não suporta sub-minute, então roda a cada 1min e processa batch)
|
||||
schedule: "*/5 * * * * *" (a cada 5s; cron de minuto não faz sub-minute, então roda a cada 1min e processa batch)
|
||||
endpoint: POST /api/v1/cron/agent-dispatcher
|
||||
auth: header X-Cron-Secret
|
||||
```
|
||||
@@ -660,7 +660,7 @@ function triggerMatches(config: TriggerConfig, msg: Message): boolean {
|
||||
|
||||
## 6. Runtime — Endpoint `/api/internal/agents/run`
|
||||
|
||||
Não é parte de `/api/v1/`. É **internal-only**, autenticado por `X-Internal-Secret` (env var). Vercel function com `maxDuration = 300`.
|
||||
Não é parte de `/api/v1/`. É **internal-only**, autenticado por `X-Internal-Secret` (env var). Rota com `maxDuration = 300`.
|
||||
|
||||
### 6.1 Algoritmo
|
||||
|
||||
@@ -788,7 +788,7 @@ Preços vêm do seed `ai_models`. Sem hardcode.
|
||||
**Provider key handling — defesa em profundidade**:
|
||||
1. Plaintext recebido só no POST `/credentials` (HTTPS, body)
|
||||
2. Cifrado AES-GCM no servidor antes de bater no DB
|
||||
3. Key de criptografia em `process.env.AI_CRED_AES_KEY` (gerenciado por Vercel/KMS, rotacionado anualmente)
|
||||
3. Key de criptografia em `process.env.AI_CRED_AES_KEY` — no `.env` da instalação (modo 0600), exigida em `lib/env.ts`; rotação anual
|
||||
4. Decrypt apenas no `/api/internal/agents/run`, key fica só em variável de função
|
||||
5. Sentry `beforeSend` strip: `authorization`, `x-api-key`, `api_key`, `*_key`, `*_secret`
|
||||
6. Logs estruturados (lib/logger) já strippa esses campos
|
||||
|
||||
@@ -41,6 +41,8 @@ owner: Rafael Melgaço
|
||||
|
||||
> **Para o epic-executor**: leia este arquivo inteiro antes de qualquer wave. Stories em ordem estrita de dependência. As 3 webhooks LGPD (`customer/redact`, `customer/data_request`, `store/redact`) **NÃO** estão neste epic — ficam em EPIC-08. Aqui entregamos a infraestrutura OAuth + adapter + 5 webhooks operacionais + sync workers + UI de configuração. EPIC-08 reusa `processWebhook`, `NuvemshopAdapter`, `tenant_integrations` e `webhook_events_log`.
|
||||
|
||||
> **Registro de 2026-04-28.** Escrito quando o alvo de deploy era a Vercel; hoje o CRM é self-host em VPS (ver [`docs/runbooks/deploy.md`](../../runbooks/deploy.md)). O corpo não foi reescrito — mas `vercel.json` não existe neste repositório, e os crons são agendados pelo serviço `scheduler` do `docker-compose.prod.yml`.
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Conectar tenant DeskcommCRM a uma loja Nuvemshop via OAuth, ingerir pedidos/clientes/produtos via 5 webhooks operacionais + 3 sync workers iniciais, e materializar cada pedido como lead no pipeline "Pedidos" (criado por T-05). Resultado mensurável: ao final do epic, conectar uma loja real → ver pedidos abertos virando cards no Kanban em <30s do `order/created`.
|
||||
@@ -419,7 +421,7 @@ exposes:
|
||||
#### Definition of Done
|
||||
|
||||
- [ ] ACs passam
|
||||
- [ ] Vercel cron configurado em vercel.json
|
||||
- ~~Vercel cron configurado em vercel.json~~ — **superada**: `vercel.json` não existe neste repositório; o agendamento é do serviço `scheduler` do `docker-compose.prod.yml`
|
||||
- [ ] Commit `feat(EPIC-07): oauth-refresh cron worker [wave 3]`
|
||||
|
||||
---
|
||||
|
||||
@@ -42,6 +42,8 @@ owner: Rafael Melgaço
|
||||
|
||||
> **Para o epic-executor**: leia este arquivo inteiro antes de qualquer wave. As stories estão em ordem de dependência. Cada story = 1 wave. Não pular ordem mesmo que pareça independente — `Deps:` é lei. Este epic constrói a camada **cross-tenant** sob o sub-domínio `admin.deskcomm.com`. Toda query depende de `fn_is_platform_admin()` retornando `true` pra bypassar RLS, conforme Spec 01 §3.4 e §3.6 (T-04). Nada aqui pode vazar pra `/app` regular.
|
||||
|
||||
> **Registro de 2026-04-28.** Escrito quando o alvo de deploy era a Vercel; hoje o CRM é self-host em VPS (ver [`docs/runbooks/deploy.md`](../../runbooks/deploy.md)). O corpo não foi reescrito — onde ele conta com recursos da plataforma (multi-domain, password protection), é registro daquele alvo, não caminho disponível.
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Entregar o Super-Admin Platform completo: 14 rotas cross-tenant em `admin.deskcomm.com` que permitem ao operador BPO P2 (Spec 01 §3.4) triagem, observabilidade, impersonate, suspensão de tenants, audit cross-tenant, gestão de LGPD/incidents/usage cross-tenant e visibilidade read-only dos `platform_admins`. Inclui inbox unificada cross-tenant via `fn_is_platform_admin()` bypass de RLS e 3 canais realtime dedicados (`admin-inbox-{platform_admin_id}`, `tenant-health-{tenant_id}`, `alerts-platform`).
|
||||
|
||||
@@ -21,6 +21,8 @@ owner: Rafael Melgaço
|
||||
|
||||
> **Para o epic-executor**: leia este arquivo inteiro antes de qualquer wave. Última fase pré-produção. Stories em ordem de dependência. Nenhuma é skippable — performance budgets, error boundaries, observability e suite E2E são gates de go-live, não nice-to-have.
|
||||
|
||||
> **Registro de 2026-04-28.** Escrito quando o alvo de deploy era a Vercel; hoje o CRM é self-host em VPS (ver [`docs/runbooks/deploy.md`](../../runbooks/deploy.md)). O corpo não foi reescrito — onde ele cobra preview deploy, projeto de staging ou RUM da plataforma, o gancho não existe mais; o que sobrevive é a exigência (budget de Core Web Vitals, smoke antes do go-live), não o instrumento citado.
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Endurecer o produto pra produção: error boundaries em todos os layouts, páginas 404/403/500/503 com copy PT-BR canônico, catálogo de empty states reusáveis, loading orchestration, Core Web Vitals dentro de budget, Sentry com PII scrubbing, suite Playwright E2E cobrindo as 5 jornadas críticas, auditoria de acessibilidade keyboard-first, polish dos docs (README/ARCHITECTURE/CONTRIBUTING) e smoke test de deploy preview pré-go-live.
|
||||
|
||||
@@ -71,6 +71,8 @@ specs:
|
||||
> - `getSession()` no backend — sempre `getUser()`
|
||||
> - Tool MCP que faz query SQL custom — sempre via handler core extraído (`_handler.ts`)
|
||||
|
||||
> **Registro de 2026-05-05.** Escrito quando o alvo de deploy era a Vercel; hoje o CRM é self-host em VPS (ver [`docs/runbooks/deploy.md`](../../runbooks/deploy.md)). O corpo não foi reescrito — mas `vercel.json` não existe neste repositório, e os crons são agendados pelo serviço `scheduler` do `docker-compose.prod.yml`.
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Entregar o **módulo de agentes configuráveis de IA** do DeskcommCRM: cada tenant configura N agentes com prompt + provider/model + chave BYO + tools MCP + sessão WhatsApp + gatilhos + prioridade. Um webhook WAHA inbound dispara o dispatcher que seleciona o agente top-priority cujo gatilho match, executa o `ToolLoopAgent` (Vercel AI SDK v6) contra MCP server interno (que expõe os endpoints REST existentes do CRM), e devolve a resposta via `WAHA sendText` na mesma sessão. UI permite Save/Publish com versionamento atômico, test mode com trace, log de execuções em realtime.
|
||||
@@ -935,7 +937,7 @@ exposes:
|
||||
|
||||
#### Definition of Done
|
||||
- [ ] Todos os ACs passam
|
||||
- [ ] Cron registrado em `vercel.json`
|
||||
- ~~Cron registrado em `vercel.json`~~ — **superada**: `vercel.json` não existe neste repositório; o agendamento é do serviço `scheduler` do `docker-compose.prod.yml`
|
||||
- [ ] Sentry monitora `ai_dispatcher.no_match` se ratio > 30% (alarme info)
|
||||
- [ ] Commit `feat(EPIC-13): agent-dispatcher worker [wave 7]`
|
||||
|
||||
@@ -1075,7 +1077,7 @@ exposes:
|
||||
|
||||
#### Definition of Done
|
||||
- [ ] Todos os ACs passam
|
||||
- [ ] `vercel.json` configura `maxDuration: 300` para `/api/internal/agents/run`
|
||||
- ~~`vercel.json` configura `maxDuration: 300` para `/api/internal/agents/run`~~ — **superada**: `vercel.json` não existe neste repositório, e a rota roda dentro do contêiner `app` do compose, não numa função serverless com teto de duração
|
||||
- [ ] Sentry breadcrumbs estruturados (sem args completos das tools — só nome+latency+error)
|
||||
- [ ] Smoke test no CI: run real com fake provider mock → 0 plaintext leaks
|
||||
- [ ] Commit `feat(EPIC-13): agent runtime ToolLoopAgent [wave 8]`
|
||||
|
||||
@@ -861,12 +861,12 @@ esta jornada prende.
|
||||
| Seed antigo restaurado (`git show HEAD~1`) e re-semeado | `agenda-escopo` reprova como no CI | **reprovou** com `não terminou` + `element(s) not found`, literal |
|
||||
## J18 — O follow-up anda em hospedagem sem agendador `[P0]`
|
||||
|
||||
**Por que P0:** para quem **não tem** o `scheduler` da VPS — o plano gratuito da
|
||||
Vercel é o caso comum, e é o cenário inteiro do runbook
|
||||
[`vercel-hobby-relogio.md`](../runbooks/vercel-hobby-relogio.md) — o relógio
|
||||
externo não é conveniência: é o **único** motor do follow-up. E a falha dele é
|
||||
silenciosa: os follow-ups não andam, ninguém recebe erro, e a instalação parece
|
||||
saudável.
|
||||
**Por que P0:** para quem **não tem** o `scheduler` da VPS — hospedagem sem cron
|
||||
de minuto, ou instalação em que o serviço não subiu; é o cenário inteiro do
|
||||
runbook [`vercel-hobby-relogio.md`](../runbooks/vercel-hobby-relogio.md) — o
|
||||
relógio externo não é conveniência: é o **único** motor do follow-up. E a falha
|
||||
dele é silenciosa: os follow-ups não andam, ninguém recebe erro, e a instalação
|
||||
parece saudável.
|
||||
|
||||
**O que existia media TEXTO.** `tests/unit/relogio-hobby-workflow.test.ts`
|
||||
confere que o `.yml` cita o caminho do tick, a variável e o `exit 1` — ancora o
|
||||
|
||||
@@ -327,8 +327,8 @@ function avisarUmaVez(chave: string, mensagem: string, contexto: Record<string,
|
||||
* cai na camada do `.env`, que é uma instalação funcionando.
|
||||
*
|
||||
* O caso `42P01` (relation does not exist) é o rollback pela OUTRA ponta: código
|
||||
* novo sobre schema velho — o que acontece na Vercel, onde a `main` sobe sem
|
||||
* ninguém aplicar migration. Ele degrada para o `.env` igual, com um aviso.
|
||||
* novo sobre schema velho — o que acontece quando a imagem nova sobe antes de o
|
||||
* baseline ser aplicado. Ele degrada para o `.env` igual, com um aviso.
|
||||
*/
|
||||
export async function marcaDaInstalacao(): Promise<LinhaDaMarca | null> {
|
||||
const memoria = memoEmVigor();
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
*
|
||||
* ─── Por que existe um fallback, e por que ele não é paliativo ───────────────
|
||||
* Neste projeto o código chega ao clone/produção por deploy, e aplicar a
|
||||
* migration é passo SEPARADO e manual — já aconteceu de a `main` subir sem ela
|
||||
* (memória `project_vercel_deploy_sem_gate_de_schema`). Nesse estado, uma
|
||||
* consulta que filtra `archived_at` volta com SQLSTATE 42703 do Postgres,
|
||||
* repassado pelo PostgREST. Quem trata isso como "não achei" mostra "nenhum
|
||||
* número conectado" numa org que tem canal ligado, ou descarta a mensagem que
|
||||
* acabou de entrar.
|
||||
* migration é passo SEPARADO e manual — já aconteceu de a versão nova subir sem
|
||||
* ela. Nesse estado, uma consulta que filtra `archived_at` volta com SQLSTATE
|
||||
* 42703 do Postgres, repassado pelo PostgREST. Quem trata isso como "não achei"
|
||||
* mostra "nenhum número conectado" numa org que tem canal ligado, ou descarta a
|
||||
* mensagem que acabou de entrar.
|
||||
*
|
||||
* Repetir a consulta SEM o filtro não é degradar: se a coluna não existe, NADA
|
||||
* está arquivado, então o resultado sem filtro é o resultado exato. O que o
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Relógio do pipeline webhook → automação → follow-up → 1º envio.
|
||||
*
|
||||
* NÃO usa cron da Vercel. O Hobby só agenda 1×/dia e event-log-drain nem
|
||||
* entra na lista. Este código corre DENTRO do POST (captação ou inbound).
|
||||
* NÃO depende de agendador externo: onde não há cron de minuto, o dreno de
|
||||
* eventos não roda a tempo. Este código corre DENTRO do POST (captação ou
|
||||
* inbound).
|
||||
*
|
||||
* O crontab da VPS continua existindo como rede de segurança; não é requisito
|
||||
* desta jornada. Falha aqui nunca vira 5xx do webhook.
|
||||
|
||||
+1
-1
@@ -408,7 +408,7 @@ if (!parsed.success) {
|
||||
console.error("[env] Falha de validação de variáveis de ambiente:");
|
||||
console.error(parsed.error.flatten().fieldErrors);
|
||||
throw new Error(
|
||||
"Variáveis de ambiente inválidas. Veja o erro acima e ajuste .env.local / Vercel.",
|
||||
"Variáveis de ambiente inválidas. Veja o erro acima e ajuste o .env da instalação (ou .env.local, em dev).",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* sugeria um cursor próprio em `watchdog_cursors` drenado DENTRO do tick do
|
||||
* `runFollowupTick`. Investiguei o consumidor de `event_log` REALMENTE em
|
||||
* produção neste repo — `lib/event-log/dispatcher.ts` + `drain.ts` +
|
||||
* `app/api/v1/cron/event-log-drain/route.ts` (roda a cada minuto, tanto no
|
||||
* Vercel quanto no cron do kit self-host — ver README.md) — e ele já resolve
|
||||
* `app/api/v1/cron/event-log-drain/route.ts` (roda a cada minuto pelo serviço
|
||||
* `scheduler` do kit self-host — ver README.md) — e ele já resolve
|
||||
* exatamente este problema: múltiplos consumidores por `event_type`,
|
||||
* idempotência via `consumed_by[]` (sem duplo efeito em re-drain), retry com
|
||||
* backoff, dead-letter. `watchdog_cursors` tem ZERO consumidores TS neste
|
||||
|
||||
@@ -74,8 +74,8 @@ async function aplicarRespostasQueChegaram(admin: SupabaseClient, deps: TickDeps
|
||||
}
|
||||
|
||||
/**
|
||||
* Roda as tarefas de minuto neste processo — sem depender do crontab da VPS
|
||||
* nem do cron pago da Vercel.
|
||||
* Roda as tarefas de minuto neste processo — sem depender do contêiner
|
||||
* `scheduler` do compose nem de um cron da hospedagem.
|
||||
*/
|
||||
export async function executarTickDoRelogio(): Promise<{
|
||||
tarefas: ResultadoDeTarefa[];
|
||||
@@ -112,9 +112,9 @@ export async function executarTickDoRelogio(): Promise<{
|
||||
const acordados = await aplicarRespostasQueChegaram(admin, deps);
|
||||
if (acordados > 0) {
|
||||
mexeu = true;
|
||||
// Sem esta linha o SIM que a ingestão do canal gravou e o Hobby não
|
||||
// processou some
|
||||
// do radar — o sintoma é "Aguardando resposta" com mensagem na inbox.
|
||||
// Sem esta linha o SIM que a ingestão do canal gravou e nenhum tick
|
||||
// processou some do radar — o sintoma é "Aguardando resposta" com
|
||||
// mensagem na inbox.
|
||||
logger.info("[relogio] follow-up avancou por resposta inbound", { acordados });
|
||||
}
|
||||
const summary = await runFollowupTick(deps);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Trabalhos de MINUTO que o Hobby da Vercel não agenda.
|
||||
* Trabalhos de MINUTO para instalação sem agendador próprio.
|
||||
*
|
||||
* No self-host o contêiner `scheduler` já chama cada rota. Esta lista é o
|
||||
* que o relógio HTTP (GitHub Actions, cron-job.org, botão na tela) precisa
|
||||
|
||||
+10
-6
@@ -8,9 +8,12 @@ import type { NextConfig } from "next";
|
||||
* - Initial bundle /app/inbox < 250KB gzipped
|
||||
*/
|
||||
const nextConfig: NextConfig = {
|
||||
// Self-host: gera .next/standalone pro container Docker (node server.js).
|
||||
// Na Vercel (VERCEL=1) fica desligado — Next 16.3 + adapter + standalone
|
||||
// quebra o onBuildComplete com ENOENT next-server.js.nft.json (#96646).
|
||||
// Self-host: gera .next/standalone pro container Docker (node server.js) — é
|
||||
// o que o estágio `runner` do Dockerfile copia, então é o modo de build deste
|
||||
// repositório. O ramo de `process.env.VERCEL` é resíduo defensivo, não um modo
|
||||
// suportado aqui: onde essa variável existe, o standalone precisa ficar
|
||||
// desligado porque Next 16.3 + adapter + standalone quebra o onBuildComplete
|
||||
// com ENOENT next-server.js.nft.json (#96646).
|
||||
output: process.env.VERCEL ? undefined : "standalone",
|
||||
/**
|
||||
* O `standalone` copia SÓ o que o file tracing detecta — e ele não detecta
|
||||
@@ -124,10 +127,11 @@ export default withSentryConfig(nextConfig, {
|
||||
tunnelRoute: "/monitoring",
|
||||
|
||||
webpack: {
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
// See the following for more information:
|
||||
// Herança do wizard do Sentry (instrumentação automática de cron monitors).
|
||||
// Inerte aqui: o bloco `webpack:` inteiro é ignorado pelo build de produção,
|
||||
// que roda Turbopack (ver Dockerfile). Fica como resíduo defensivo, não como
|
||||
// modo de build suportado por este repositório.
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: true,
|
||||
|
||||
// Tree-shaking options for reducing bundle size
|
||||
|
||||
@@ -22,10 +22,10 @@ export async function proxy(request: NextRequest) {
|
||||
response.headers.set("x-pathname", pathname);
|
||||
request.headers.set("x-pathname", pathname);
|
||||
|
||||
// EPIC-11: in dev we route by path (`/admin/*`); in prod the
|
||||
// `admin.deskcomm.com` sub-domain is mapped via Vercel rewrites to the same
|
||||
// `/admin/*` paths. The host-based branch below stays a NOOP today and only
|
||||
// exists as documentation of the intended deploy topology.
|
||||
// EPIC-11: the admin surface is reached by PATH (`/admin/*`) — the self-host kit
|
||||
// points `NEXT_PUBLIC_ADMIN_URL` at the same host as the app and maps no `admin.`
|
||||
// sub-domain. The host-based branch below stays a NOOP today and only exists as
|
||||
// documentation of the intended deploy topology.
|
||||
const host = request.headers.get("host") ?? "";
|
||||
const isAdminSurface = host.startsWith("admin.") || pathname.startsWith("/admin");
|
||||
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
# Liga o relógio Hobby: imprime os comandos (não grava secrets sozinho).
|
||||
# Uso: .\scripts\ligar-relogio-hobby.ps1 -AppUrl https://crm-gabrielle.vercel.app
|
||||
# Liga o relógio de quem não tem agendador: imprime os comandos
|
||||
# (não grava secrets sozinho).
|
||||
# Uso: .\scripts\ligar-relogio-hobby.ps1 -AppUrl https://SEU-DOMINIO [-Repo owner/repo]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$AppUrl,
|
||||
[string]$Repo = "IanCouto/DeskcommCRM"
|
||||
[string]$Repo = ""
|
||||
)
|
||||
|
||||
# Mesmo fallback do irmão `ligar-relogio-hobby.sh`: sem -Repo, o repositório sai
|
||||
# do `gh`, e o placeholder só entra quando não vier nada. Um literal como default
|
||||
# seria um valor que nunca funciona para quem rodar sem passar o parâmetro.
|
||||
if (-not $Repo -and (Get-Command gh -ErrorAction SilentlyContinue)) {
|
||||
$Repo = gh repo view --json nameWithOwner -q .nameWithOwner 2>$null
|
||||
}
|
||||
if (-not $Repo) { $Repo = "SEU_USER/DeskcommCRM" }
|
||||
|
||||
$AppUrl = $AppUrl.TrimEnd("/")
|
||||
$Tick = "$AppUrl/api/v1/system/relogio/tick"
|
||||
|
||||
Write-Host @"
|
||||
=== Relógio Hobby ===
|
||||
=== Relógio sem agendador ===
|
||||
|
||||
1) GitHub Actions (grátis, a cada ~5 min) — o workflow PRECISA estar na main:
|
||||
|
||||
gh variable set RELOGIO_LIGADO -R $Repo -b 1
|
||||
gh secret set RELOGIO_APP_URL -R $Repo -b "$AppUrl"
|
||||
gh secret set RELOGIO_SECRET -R $Repo
|
||||
# (cole o INTERNAL_SECRET da Vercel quando pedir)
|
||||
# (cole o INTERNAL_SECRET do seu .env quando pedir)
|
||||
|
||||
Depois: Actions → relogio → Run workflow
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Liga o relógio Hobby: imprime os comandos (não grava secrets sozinho).
|
||||
# Uso: ./scripts/ligar-relogio-hobby.sh https://crm-gabrielle.vercel.app
|
||||
# Liga o relógio de quem não tem agendador: imprime os comandos
|
||||
# (não grava secrets sozinho).
|
||||
# Uso: ./scripts/ligar-relogio-hobby.sh https://SEU-DOMINIO
|
||||
set -eu
|
||||
APP_URL="${1:-}"
|
||||
REPO="${2:-$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)}"
|
||||
|
||||
if [ -z "$APP_URL" ]; then
|
||||
echo "Uso: $0 <APP_URL> [owner/repo]"
|
||||
echo "Ex.: $0 https://crm-gabrielle.vercel.app IanCouto/DeskcommCRM"
|
||||
echo "Ex.: $0 https://SEU-DOMINIO SEU_USER/DeskcommCRM"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -15,14 +16,14 @@ APP_URL="${APP_URL%/}"
|
||||
TICK="${APP_URL}/api/v1/system/relogio/tick"
|
||||
|
||||
cat <<EOF
|
||||
=== Relógio Hobby ===
|
||||
=== Relógio sem agendador ===
|
||||
|
||||
1) GitHub Actions (grátis, a cada ~5 min) — o workflow PRECISA estar na main:
|
||||
|
||||
gh variable set RELOGIO_LIGADO -R ${REPO:-SEU_USER/DeskcommCRM} -b 1
|
||||
gh secret set RELOGIO_APP_URL -R ${REPO:-SEU_USER/DeskcommCRM} -b "${APP_URL}"
|
||||
gh secret set RELOGIO_SECRET -R ${REPO:-SEU_USER/DeskcommCRM}
|
||||
# (cole o INTERNAL_SECRET da Vercel quando pedir)
|
||||
# (cole o INTERNAL_SECRET do seu .env quando pedir)
|
||||
|
||||
Depois: Actions → relogio → Run workflow
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Supabase CLI config — DeskcommCRM
|
||||
# Docs: https://supabase.com/docs/guides/cli/config
|
||||
# Este arquivo é versionado. Segredos vivem em .env.local / Vercel.
|
||||
# Este arquivo é versionado. Segredos vivem em .env.local (dev) ou no .env da instalação.
|
||||
|
||||
project_id = "deskcomm-crm"
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
*
|
||||
* ## Por que isto não podia continuar sem prova
|
||||
*
|
||||
* Para quem **não tem** o `scheduler` da VPS — o plano gratuito da Vercel é o
|
||||
* caso comum, e é o cenário inteiro do runbook `vercel-hobby-relogio.md` — o
|
||||
* relógio externo não é conveniência: é o **único** motor do follow-up. E a
|
||||
* falha dele é silenciosa: os follow-ups simplesmente não andam, ninguém recebe
|
||||
* erro, e a instalação parece saudável.
|
||||
* Para quem **não tem** o `scheduler` da VPS — hospedagem sem cron de minuto,
|
||||
* ou instalação em que o serviço não subiu; é o cenário inteiro do runbook
|
||||
* `vercel-hobby-relogio.md` — o relógio externo não é conveniência: é o
|
||||
* **único** motor do follow-up. E a falha dele é silenciosa: os follow-ups
|
||||
* simplesmente não andam, ninguém recebe erro, e a instalação parece saudável.
|
||||
*
|
||||
* O que existia era `tests/unit/relogio-hobby-workflow.test.ts`, e ele mede
|
||||
* TEXTO — que o `.yml` cita o caminho do tick, a variável e o `exit 1`. Isso
|
||||
|
||||
@@ -239,9 +239,17 @@ describe("acolhida: um workflow privilegiado que não toca no código do fork",
|
||||
// O molde é `triagem/references/resposta-ao-contribuidor.md`, seção "Acolhida".
|
||||
// A acolhida é segura de ser automática porque não fala do mérito; se alguém
|
||||
// esvaziar o texto, ela deixa de fazer o trabalho pelo qual existe.
|
||||
expect(textoDosRuns, "o Vercel vermelho não é culpa de quem abriu o PR").toMatch(
|
||||
/Vercel[\s\S]*gate de merge/,
|
||||
);
|
||||
//
|
||||
// A primeira asserção cobra o PONTEIRO, nunca a lista: a mensagem manda olhar os
|
||||
// checks `Required` do próprio PR porque quem mexe na branch protection não passa
|
||||
// por aqui. Uma lista de nomes escrita nesta mensagem envelheceria sozinha, no PR
|
||||
// de um estranho e sem nenhum gate para avisar — foi o que aconteceu com o bullet
|
||||
// que esta asserção substituiu, sobre um check que este repositório deixou de
|
||||
// receber.
|
||||
expect(
|
||||
textoDosRuns,
|
||||
"a mensagem diz onde olhar o CI: os checks `Required` do próprio PR",
|
||||
).toMatch(/trava o merge[\s\S]*?Required/);
|
||||
expect(textoDosRuns, "os workflows podem estar parados esperando liberação").toMatch(
|
||||
/esperando[\s\S]*?libera/,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* O cron Hobby some do radar fácil: arquivo só em develop, schedule na main,
|
||||
* variável desligada. Este teste ancora o contrato mínimo no fonte.
|
||||
* Este relógio some do radar fácil: arquivo só numa branch de trabalho,
|
||||
* schedule que só roda na default, variável desligada. Este teste ancora o
|
||||
* contrato mínimo no fonte.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
+4
-3
@@ -109,8 +109,9 @@ Nesta ordem:
|
||||
**A liberação do CI é o primeiro comando da triagem, antes de ler o diff.** Medido em 2026-09-03: numa fila de 26 PRs, **12 workflows** de cinco contribuidores estavam parados em `action_required`, um deles havia mais de um dia — e três PRs tinham **zero** execuções no `head_sha` (ver modo de falha 17). Cada minuto entre abrir o PR e liberar é latência pura, que é o gargalo que este documento existe para matar. Libere primeiro; avalie depois.
|
||||
|
||||
A acolhida **não contém juízo técnico**. É isso, e só isso, que a torna segura de ser automática:
|
||||
ela não pode estar errada sobre o mérito porque não fala do mérito. Ela diz três coisas — o `Vercel`
|
||||
vermelho é esperado em fork e não é culpa dele, o CI está sendo liberado, e quando vem o veredito.
|
||||
ela não pode estar errada sobre o mérito porque não fala do mérito. Ela diz três coisas — o CI está
|
||||
sendo liberado, onde olhar o que trava o merge, e quando vem o veredito. O texto vive em
|
||||
`references/resposta-ao-contribuidor.md`, espelhado em `.github/workflows/acolhida.yml`.
|
||||
|
||||
Todo comentário desta triagem abre com a âncora invisível `<!-- triagem-de-pr:v1:pass=N -->`. Leia as
|
||||
âncoras existentes antes de escrever: **acolhida nunca é postada duas vezes.**
|
||||
@@ -2469,7 +2470,7 @@ Cada um destes foi cometido de verdade nesta casa, e é por isso que estão escr
|
||||
|
||||
```bash
|
||||
gh pr checks <N> --json name,bucket --jq '
|
||||
[.[]|select(.bucket!="skipping")|select(.name|test("^Vercel")|not)]
|
||||
[.[]|select(.bucket!="skipping")]
|
||||
| if (any(.bucket=="fail")) then "VERMELHO"
|
||||
elif (any(.bucket=="pending")) then "AINDA RODANDO"
|
||||
else "VERDE" end'
|
||||
|
||||
@@ -28,14 +28,16 @@ Três informações, nesta ordem. Nada além.
|
||||
<!-- triagem-de-pr:v1:pass=1 -->
|
||||
Recebido, @<login> — obrigado por isto.
|
||||
|
||||
Duas coisas que vão parecer erro seu e não são:
|
||||
Uma coisa vai parecer erro seu e não é:
|
||||
|
||||
- O check **Vercel** vermelho ("Authorization required to deploy") é esperado em PR de fork. A `main`
|
||||
faz deploy de produção e a Vercel se recusa a construir código de fora, o que está certo. **Ele não
|
||||
entra no gate de merge.**
|
||||
- Os workflows ficam parados esperando liberação no primeiro PR de quem nunca contribuiu — política
|
||||
do GitHub, não sua. **Acabei de liberar**, o CI já está rodando.
|
||||
|
||||
Quando eles terminarem, o que trava o merge são só os checks marcados **Required** no seu PR — é essa
|
||||
lista que vale, não qualquer outro vermelho que apareça. No terminal, `gh pr checks <número> --required`
|
||||
mostra os obrigatórios que **já reportaram**, e só esses: enquanto um deles não rodou, ele não aparece
|
||||
ali. Se um reprovar, a saída dele diz o que falta; se não estiver claro, me diga aqui — não feche o PR.
|
||||
|
||||
Vou revisar de verdade — rodando os gates e reproduzindo o comportamento, não só lendo o diff — e
|
||||
volto com o resultado <prazo>. Se eu achar algo, venho com a medição junto, nunca com um "acho que".
|
||||
```
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
/**
|
||||
* Configuração do projeto na Vercel.
|
||||
* Agendamento para quem hospeda um FORK deste repositório na Vercel.
|
||||
*
|
||||
* O caminho oficial é o self-host: quem chama as rotas de cron é o serviço
|
||||
* `scheduler` (`docker/scheduler/entrypoint.sh`). Os crons abaixo são a MESMA
|
||||
* cadência daquele crontab, e `tests/unit/cron-routes-scheduled.test.ts` reprova
|
||||
* quando as duas listas de rotas divergem (a cadência não está sob gate) — rota de
|
||||
* cron nova entra nos dois arquivos.
|
||||
*
|
||||
* Os crons abaixo são a MESMA cadência de `docker/scheduler/entrypoint.sh`.
|
||||
* Plano Pro: minuto a minuto vale. Hobby: expressões mais frequentes que
|
||||
* 1×/dia derrubam o deploy — nesse caso deixe só o `lgpd-sla-watcher` e use
|
||||
* o relógio HTTP (`docs/runbooks/vercel-hobby-relogio.md`).
|
||||
* o relógio HTTP (`app/api/v1/system/relogio/tick/route.ts`; passo a passo em
|
||||
* `docs/runbooks/vercel-hobby-relogio.md`).
|
||||
*
|
||||
* Auth: Vercel Cron manda Bearer CRON_SECRET; em produção `lib/env.ts` copia
|
||||
* isso para INTERNAL_CRON_SECRET. INTERNAL_SECRET continua valendo nas rotas.
|
||||
* Auth: o Vercel Cron manda Bearer CRON_SECRET; em produção `lib/env.ts` copia esse
|
||||
* valor para INTERNAL_CRON_SECRET. A regra canônica de quem é aceito está em
|
||||
* `lib/auth/cron-auth.ts`, que confere o Bearer contra os DOIS segredos — então o
|
||||
* INTERNAL_SECRET segue servindo. A exceção é
|
||||
* `app/api/v1/cron/sync-model-catalog/route.ts`, que espera só o primeiro dos dois
|
||||
* que estiver definido: com CRON_SECRET presente, o INTERNAL_SECRET não passa ali.
|
||||
*/
|
||||
|
||||
import type { VercelConfig } from "@vercel/config/v1";
|
||||
|
||||
Reference in New Issue
Block a user