Runbook único de homologação, checklist de evidências e ADR do ambiente
ci / lint, typecheck, testes e build (push) Has been cancelled
ci / Postgres e Redis reais (push) Has been cancelled
ci / imagem Docker Linux (push) Has been cancelled

Havia dois runbooks sobrepostos, escritos para o piloto, e nenhum documento do
ambiente de homologação.

- `20-homologacao-runbook.md` consolida os dois e cobre DNS, TLS, Nginx,
  Supabase de homologação, banco com a decisão do shim, bucket privado,
  containers, worker, observabilidade, flag, rollback, diagnóstico e smoke.
  Os dois runbooks antigos ficam no repositório marcados como histórico,
  preservando o registro das decisões.
- Rollback de imagem com comandos: identificar o que está no ar, listar tags,
  desligar a flag antes de trocar, subir só web e worker com `--no-deps`,
  conferir saúde, religar a flag. As migrations de segurança — RLS e auditoria
  append-only — estão declaradas sem caminho de volta: desfazê-las afrouxa o
  isolamento entre workspaces, então um rollback que precise disso está errado
  por definição.
- `21-checklist-validacao.md` separa o que foi executado do que depende de
  infraestrutura, com a evidência de cada item e a tabela dos quatro defeitos
  encontrados. Nada está marcado PASS por inspeção de código.
- ADR 0007 registra o ambiente lógico e a autenticação de homologação, com as
  alternativas descartadas e o motivo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
camilafagundesandClaude Opus 5 committed 2026-10-15 20:40:08 -03:00
1 parent a1ceef6c5f
commit 5b11496cd9
6 files changed
+644 -7

No files matched your search

+69 -7
View File
@@ -1,17 +1,26 @@
# Adaptador de Planilhas # Adaptador de Planilhas
Módulo do Elo Pro para validar planilhas XLSX/CSV, mapear colunas para um modelo e gerar um arquivo novo. Não há login, senha ou tabela de usuários própria. Módulo do Elo Pro para validar planilhas XLSX/CSV, mapear colunas para um modelo e gerar um arquivo novo. Não há login, senha ou tabela de usuários própria: a identidade é a do ecossistema.
| Ambiente | Host | `ADAPTER_ENV` | Autenticação |
| --- | --- | --- | --- |
| Desenvolvimento | `localhost:3000` | `development` | sessão local assinada, sem senha |
| Homologação | `hml-ada.elopro.app` | `staging` | Supabase Auth do projeto de homologação |
| Produção | `ada.elopro.app` | `production` | SSO do Elo Pro |
`ADAPTER_ENV` é independente de `NODE_ENV`: homologação roda com `NODE_ENV=production` (build otimizado) e política própria. Ver [ADR 0007](docs/decisions/0007-ambiente-logico-e-auth-de-homologacao.md).
## Desenvolvimento local ## Desenvolvimento local
```bash ```bash
npm install npm install
copy .env.example .env.local cp .env.example .env.local
``` ```
No `.env.local`, para o piloto sem o host do Elo Pro: No `.env.local`:
``` ```
ADAPTER_ENV=development
ADAPTER_DEV_AUTH=1 ADAPTER_DEV_AUTH=1
ADAPTER_DEV_AUTH_SECRET=um-segredo-local ADAPTER_DEV_AUTH_SECRET=um-segredo-local
ADAPTER_JOB_MODE=inline ADAPTER_JOB_MODE=inline
@@ -21,15 +30,58 @@ ADAPTER_JOB_MODE=inline
npm run dev npm run dev
``` ```
Abra `/login` e escolha uma identidade de desenvolvimento. Isso não pede senha: apenas assume um vínculo que o seed cria em `profiles` e `workspace_members`. Abra `/login` e escolha uma identidade de desenvolvimento. Não há senha: apenas assume um vínculo que o seed cria em `profiles` e `workspace_members`.
### Com Docker, igual ao artefato de homologação
Mesma imagem, dois processos, Postgres e Redis de verdade:
```bash
docker compose up --build
```
```bash
docker compose exec web node node_modules/tsx/dist/cli.mjs scripts/migrate.ts
```
Portas só em `127.0.0.1`: web `3000`, Postgres `55432`, Redis `56379`.
## Homologação
O procedimento completo — DNS, Nginx, Supabase, banco, bucket, containers, worker, observabilidade e rollback — está em **[docs/spreadsheet-adapter/20-homologacao-runbook.md](docs/spreadsheet-adapter/20-homologacao-runbook.md)**. O estado de validação está em [21-checklist-validacao.md](docs/spreadsheet-adapter/21-checklist-validacao.md).
Resumo:
```bash
cp .env.staging.example .env.staging # preencher; não versionar
npm run verify:config # recusa configuração incompleta
npm run check:bucket # o bucket precisa existir e ser privado
npm run migrate:check # mostra o que vai rodar
npm run migrate # aplica (passo com aprovação manual)
npm run seed:staging # liga identidades do Supabase a workspaces
docker compose -f docker-compose.staging.yml --env-file .env.staging up -d --no-deps web worker
ADAPTER_SMOKE_URL=https://hml-ada.elopro.app npm run smoke:staging
npm run flag -- <workspace_id> on # último passo, depois do smoke
```
`ADAPTER_APPLY_IDENTITY_SHIM` é obrigatória fora de development: `1` cria o shim local de identidade, `0` usa o schema existente do Elo Pro. Aplicar o shim sobre um schema real é destrutivo em potencial, por isso a escolha nunca é inferida.
## Produção ## Produção
Obrigatórios: `DATABASE_URL`, `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `SUPABASE_STORAGE_BUCKET`, `REDIS_URL`, `SPREADSHEET_TEMP_TTL_SECONDS`, `ELO_PRO_LOGIN_URL`. Além das variáveis de homologação, produção exige `SENTRY_DSN` e `ELO_PRO_LOGIN_URL`, e recusa `ADAPTER_STAGING_AUTH=1`. A feature flag `spreadsheet_adapter` nasce desligada e é ligada por workspace com `npm run flag`, usando a conexão de serviço, nunca pelo browser.
A feature flag `spreadsheet_adapter` nasce desligada. Habilite por workspace com `npm run flag -- <workspace_id> on` usando a conexão de serviço, nunca pelo browser. O processamento usa `ADAPTER_JOB_MODE=worker` e o processo `npm run worker`. `inline` é recusado fora de development.
O processamento de produção deve usar `ADAPTER_JOB_MODE=worker` e o processo `npm run worker`. O modo `inline` existe para desenvolvimento. Produção não é liberada sem smoke real e ensaio de rollback registrados no checklist.
## Saúde
| Rota | O que responde |
| --- | --- |
| `GET /api/health/live` | processo vivo, sem tocar dependência |
| `GET /api/health/ready` | banco, migrations pendentes, Redis (PING), bucket, configuração e modo de job |
Configuração inválida encerra o processo com código diferente de zero, em vez de deixar o servidor no ar respondendo erro.
## Verificação ## Verificação
@@ -39,3 +91,13 @@ npm run typecheck
npm test npm test
npm run build npm run build
``` ```
Suíte que exige Postgres e Redis reais (roda no CI, e localmente com os dois serviços em pé):
```bash
ADAPTER_TEST_DATABASE_URL=postgres://... ADAPTER_TEST_REDIS_URL=redis://... npm run test:services
```
Sem essas variáveis os casos são **ignorados**, nunca aprovados.
O CI tem três jobs: `verify` (lint, typecheck, testes, build), `services` (Postgres e Redis reais: as quatro migrations, shim explícito, `FORCE RLS`, isolamento) e `image` (build da imagem Linux, conteúdo, fail-fast dos dois processos, compose completo, jornada assíncrona entre processos e desligamento gracioso do worker).
@@ -0,0 +1,32 @@
# 0007 — Ambiente lógico e autenticação de homologação
## Contexto
`next start` força `NODE_ENV=production`. Até aqui o módulo só conhecia dois estados: `NODE_ENV=production` ou não. Consequência prática: em homologação a autenticação local sem senha ficava desligada (correto) e o módulo exigia o login central do Elo Pro (que ainda não existe), sem terceira opção. Homologação ficava sem caminho de entrada.
Além disso, nada no repositório gravava o cookie `elo_pro_workspace_id`, lido como origem do workspace ativo. Mesmo com sessão válida, a resposta seria sempre "sem workspace".
## Decisão
1. **`ADAPTER_ENV` com três valores**: `development`, `staging`, `production`. É independente de `NODE_ENV`, que continua `production` em homologação para manter o build otimizado. Toda política passa a derivar de `ADAPTER_ENV`.
2. **Homologação autentica com Supabase Auth real**, em um projeto Supabase exclusivo de homologação, via `signInWithPassword` em `/api/auth/staging/session`. Há senha, emitida e validada pelo Supabase. O Adaptador continua sem tabela de usuário, senha, cadastro ou sessão própria.
3. **`ADAPTER_DEV_AUTH` só existe em `development`.** Não é configuração: em `staging` e `production` o provedor de desenvolvimento não entra na lista de provedores, e `ADAPTER_DEV_AUTH=1` faz o start ser recusado.
4. **Contrato de identidade explícito** em `src/server/auth/identity-provider.ts`. Trocar o Supabase de homologação pelo SSO do Elo Pro é adicionar uma implementação; nenhum outro arquivo conhece o mecanismo.
5. **Workspace é resolvido no servidor, por associação real.** O cookie é preferência, não prova: `resolveWorkspace` só o aceita quando corresponde a uma linha de `workspace_members` lida sob RLS. Sem associação, não há workspace. Com várias, a pessoa escolhe em `/selecionar-workspace`, e `POST /api/auth/workspace` grava o cookie apenas depois de confirmar a associação.
## Alternativas descartadas
- **Habilitar o dev auth em homologação.** Rejeitada: homologação é pública, e autenticação sem senha em host público é uma porta aberta.
- **Aceitar o cookie de workspace como autorização.** Rejeitada: transformaria um valor editável pelo cliente em credencial. O custo de reler a associação por requisição é uma consulta indexada.
- **Usar o projeto Supabase de produção com usuários de teste.** Rejeitada: compartilharia a service role entre ambientes.
- **Inferir o ambiente de `SENTRY_ENVIRONMENT`.** Rejeitada: observabilidade e política de segurança são decisões diferentes; a mesma variável governando as duas foi o que fez o namespace do Redis colidir entre ambientes.
## Consequências
- `ADAPTER_ENV` passa a ser obrigatória nos ambientes implantados, e `ADAPTER_APPLY_IDENTITY_SHIM` passa a ser uma decisão explícita.
- Homologação tem seu próprio projeto Supabase, bucket, banco, Redis e ambiente Sentry.
- O `0002-sso-topology.md` continua valendo para produção. Esta decisão acrescenta o degrau de homologação, sem substituí-lo.
@@ -1,5 +1,11 @@
# Deploy e operação # Deploy e operação
> **Histórico.** Este runbook foi escrito para o piloto, antes do ambiente de
> homologação existir. Não siga este arquivo. O procedimento vigente está em
> [`20-homologacao-runbook.md`](20-homologacao-runbook.md), que consolida este e
> o `16-pilot-deployment-runbook.md`. Mantido para registro das decisões.
## Ordem ## Ordem
1. Conferir envs sem colar segredo no repositório. 1. Conferir envs sem colar segredo no repositório.
@@ -1,5 +1,11 @@
# Runbook de implantação do piloto # Runbook de implantação do piloto
> **Histórico.** Este runbook foi escrito para o piloto, antes do ambiente de
> homologação existir. Não siga este arquivo. O procedimento vigente está em
> [`20-homologacao-runbook.md`](20-homologacao-runbook.md), que consolida este e
> o `16-pilot-deployment-runbook.md`. Mantido para registro das decisões.
Execute nesta ordem. Não apague auditoria, banco ou arquivos como parte do rollback. Execute nesta ordem. Não apague auditoria, banco ou arquivos como parte do rollback.
1. Preparar o projeto Supabase da Elo Pro e anotar URL, anon key e service role. 1. Preparar o projeto Supabase da Elo Pro e anotar URL, anon key e service role.
@@ -0,0 +1,368 @@
# Runbook de homologação — hml-ada.elopro.app
Runbook único do Adaptador de Planilhas em homologação. Substitui `07-deployment-runbook.md` e `16-pilot-deployment-runbook.md`, que ficam no repositório como histórico e não devem ser seguidos.
| | |
| --- | --- |
| Homologação | `hml-ada.elopro.app` |
| Produção futura | `ada.elopro.app` |
| Servidor | compartilhado com os outros aplicativos do ecossistema |
| Projeto do compose | `adaptador-planilhas-hml` |
| Rede Docker | `adaptador-planilhas-hml` |
| Volume | `adaptador-planilhas-hml-redis` |
| Portas publicadas | só em `127.0.0.1`: web `3110`, Redis `56380` |
| Namespace de chaves | `adaptador-planilhas:staging` |
## Princípios que o procedimento respeita
1. Nenhum serviço de outro aplicativo é criado, alterado ou reiniciado. O compose tem nome de projeto, rede e volume próprios, e o deploy usa `up -d --no-deps web worker`.
2. DNS, serviços e banco de produção não são tocados. Homologação tem subdomínio, banco, bucket, Redis, projeto Supabase e projeto/ambiente Sentry próprios.
3. Não há autenticação de desenvolvimento em homologação. `ADAPTER_DEV_AUTH=1` faz o processo recusar o start fora de `development`.
4. Nenhuma credencial administrativa é compartilhada entre aplicações ou entre ambientes.
5. RLS e as validações multi-tenant não são afrouxadas em nenhum passo.
6. Migrations destrutivas não rodam automaticamente. `npm run migrate` é um job separado, com aprovação manual, precedido de `npm run migrate:check`.
7. Produção não é liberada sem smoke real e ensaio de rollback registrados em `21-checklist-validacao.md`.
---
## 1. DNS
Registro novo, sem mexer em nada existente:
```
hml-ada.elopro.app. A <IP do servidor compartilhado>
```
Se o ecossistema usa CNAME para um host único, siga o mesmo padrão dos outros aplicativos. Não altere o registro de `ada.elopro.app` nem o apex `elopro.app`.
Certificado TLS: inclua apenas `hml-ada.elopro.app` na emissão. Com Certbot e Nginx:
```bash
sudo certbot --nginx -d hml-ada.elopro.app
```
Isso cria um bloco novo. Não use `--expand` em um certificado que atende aplicativos existentes: uma renovação com falha derrubaria o TLS deles.
## 2. Nginx
Arquivo novo em `/etc/nginx/sites-available/hml-ada.elopro.app`, sem editar nenhum `server` existente:
```nginx
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name hml-ada.elopro.app;
ssl_certificate /etc/letsencrypt/live/hml-ada.elopro.app/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hml-ada.elopro.app/privkey.pem;
# Upload de modelo e de fonte: o limite do produto é 10 MB por arquivo.
# A margem cobre o envelope multipart.
client_max_body_size 12m;
# Processamento assíncrono responde rápido, mas a geração de PDF e XLSX
# do download é síncrona. O teto do job é SPREADSHEET_JOB_TIMEOUT_MS.
proxy_read_timeout 180s;
proxy_send_timeout 180s;
location / {
proxy_pass http://127.0.0.1:3110;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# Sondas internas: o monitoramento usa estas duas rotas.
location = /api/health/live { proxy_pass http://127.0.0.1:3110; access_log off; }
location = /api/health/ready { proxy_pass http://127.0.0.1:3110; }
}
server {
listen 80;
listen [::]:80;
server_name hml-ada.elopro.app;
return 301 https://$host$request_uri;
}
```
`X-Forwarded-For` é obrigatório: o rate limiting da tela de login identifica o chamador por ele.
Os cabeçalhos de segurança (CSP, HSTS, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`) vêm da aplicação, em `next.config.ts`. Não duplique no Nginx: `add_header` no servidor substituiria o conjunto da aplicação.
```bash
sudo ln -s /etc/nginx/sites-available/hml-ada.elopro.app /etc/nginx/sites-enabled/
sudo nginx -t # precisa passar antes de recarregar
sudo systemctl reload nginx # reload, nunca restart: não derruba os outros sites
```
## 3. Identidade (Supabase Auth de homologação)
O Adaptador não tem tabela de usuário, senha, cadastro ou sessão própria. Em homologação a identidade é um **projeto Supabase separado**, só de homologação.
1. Crie o projeto Supabase de homologação. Anote URL, anon key e service role.
2. A service role de homologação não é a de produção e não é compartilhada com nenhum outro aplicativo.
3. Crie as contas de teste no painel do Supabase (Authentication → Users). Nenhuma senha passa pelo repositório, pelo seed ou por este runbook.
4. Anote o UUID de cada conta criada: é o que liga a identidade ao workspace.
5. Em `.env.staging`, deixe `ADAPTER_STAGING_AUTH=1`. Isso habilita `/api/auth/staging/session`, que faz `signInWithPassword` contra esse projeto. A sessão é emitida e validada pelo Supabase.
6. Quando o SSO do Elo Pro existir, troque para `ADAPTER_STAGING_AUTH=0` e `ELO_PRO_LOGIN_URL=<login central>`. Nada mais muda: o contrato está em `src/server/auth/identity-provider.ts`.
### Workspace ativo
O workspace **não** vem de cookie como prova. O cookie `elo_pro_workspace_id` é preferência; toda requisição relê as associações reais em `workspace_members` sob RLS (`src/server/auth/workspace.ts`):
- nenhuma associação → a pessoa vê "nenhum workspace";
- uma associação → escolha automática;
- várias → tela `/selecionar-workspace`, e `POST /api/auth/workspace` só grava o cookie depois de confirmar a associação.
### Seed de homologação
```bash
cp config/staging-seed.example.json config/staging-seed.json
# preencha users[].id com os UUID do Supabase Auth e escolha os UUID de workspace
npm run seed:staging
```
Idempotente: rodar de novo produz o mesmo estado. Não cria conta nem senha, só liga identidades a workspaces e papéis. A feature flag nasce desligada mesmo aqui.
## 4. Banco de homologação
```bash
# 1. Veja o que vai rodar, antes de rodar.
ADAPTER_ENV=staging DATABASE_URL=... ADAPTER_APPLY_IDENTITY_SHIM=? npm run migrate:check
```
A decisão do shim é **explícita e obrigatória**: sem `ADAPTER_APPLY_IDENTITY_SHIM` o start é recusado.
| Situação do banco | Valor | Efeito |
| --- | --- | --- |
| Banco novo, sem `profiles`/`workspaces`/`workspace_members` | `1` | `0001_platform_shim.sql` cria o shim e o papel `app_user` |
| Banco já com o schema do Elo Pro | `0` | `0001` é pulado; só `0002`, `0003` e `0004` rodam |
Com `0`, antes de migrar: confirme que existem `public.profiles(id, display_name, is_superadmin)`, `public.workspaces(id, name)` e `public.workspace_members(workspace_id, user_id, role)`, e que o papel `app_user` existe sem `BYPASSRLS`. Se os nomes ou as colunas diferirem, adapte `src/server/auth/identity-port.ts` **antes** — é o único arquivo que precisa mudar.
```bash
# 2. Aplicar (job com aprovação manual no workflow).
npm run migrate
```
Nenhum processo aplica migration sozinho: o worker recusa iniciar com migration pendente e a web não migra em ambiente implantado.
## 5. Bucket privado
```bash
# Crie o bucket no painel do Supabase de homologação, com visibilidade privada.
# Nome sugerido: adaptador-planilhas-hml
npm run check:bucket
```
`check:bucket` falha se o bucket não existir ou for público. A aplicação também recusa o valor literal `public` em `SUPABASE_STORAGE_BUCKET`. Nenhuma URL pública é gerada: o download passa por `/api/spreadsheet-adapter/jobs/[id]/download`, autenticado e sob RLS.
## 6. Containers
A imagem tem um entrypoint com subcomandos. Use sempre o subcomando, nunca `node` direto: ele garante `node --import tsx`, que é o que faz o SIGTERM do `docker stop` chegar ao processo do worker. O CLI do `tsx` cria um processo filho e o sinal pararia nele.
```bash
cd <HML_DEPLOY_PATH>
cp .env.staging.example .env.staging
# preencha todos os campos e confira
export C="docker compose -f docker-compose.staging.yml --env-file .env.staging"
$C run --rm web verify-config
$C up -d redis
$C up -d --no-deps web worker
$C ps
```
| Subcomando | O que faz |
| --- | --- |
| `web` | servidor Next (saída `standalone`) |
| `worker` | processo de fila |
| `migrate [--dry-run]` | migrations |
| `seed-staging` | associa identidades do Supabase a workspaces |
| `flag <workspace_id> on off` | feature flag do módulo |
| `cleanup` | expira temporários fora do ciclo do worker |
| `verify-config` | confere a configuração sem tocar em recurso externo |
| `check-bucket` | confere que o bucket existe e é privado |
| `smoke` | smoke do ambiente |
| `worker-health` | confere o heartbeat do worker |
`--no-deps` e o `--project-name` embutido (`adaptador-planilhas-hml`) garantem que nenhum stack de outro aplicativo é tocado. Nunca rode `docker compose down` sem `-f docker-compose.staging.yml`, e nunca `docker system prune` neste servidor.
Web e worker saem da **mesma imagem**: mesmo código, mesmas dependências. A diferença é só o subcomando. O `node_modules` de produção e o traçado pelo `standalone` vêm do mesmo lockfile e não divergem em nenhuma versão; a imagem sobrepõe as duas árvores nessa ordem, com a traçada por último, que é a que o `server.js` espera.
O Redis é um contêiner exclusivo deste aplicativo, com senha, volume e porta próprios. Mesmo assim as chaves são prefixadas com `adaptador-planilhas:staging`, então um Redis compartilhado por engano não misturaria homologação com produção.
## 7. Worker
```bash
$C logs -f worker
$C exec worker /app/docker/entrypoint.sh worker-health
```
O start imprime `[worker] pronto. intervalo=…ms lote=… carencia=…ms alvo=postgres heartbeat=…`.
- `ADAPTER_JOB_MODE=worker` é obrigatório. `inline` faz o start ser recusado.
- Migration pendente faz o worker sair com código 1, com a lista no log.
- `SIGTERM` espera o tick corrente (até `ADAPTER_WORKER_GRACE_MS`, padrão 20 s, dentro do `stop_grace_period` de 30 s) para o `finally` liberar o lock. Sem isso o job ficaria em `processing` até o lock vencer por TTL. O log de uma parada limpa traz `SIGTERM recebido` e `[worker] encerrado`; se faltarem, o contêiner levou SIGKILL e o lock vai esperar o TTL de 120 s.
- Um job que estava apenas na fila, e não começou, permanece `queued`: nada pela metade. O worker seguinte o conclui.
- O worker grava um heartbeat ao fim de cada tick (`ADAPTER_WORKER_HEARTBEAT`, padrão `/tmp/adaptador-worker-heartbeat`). O healthcheck do contêiner usa esse arquivo, porque um processo vivo mas travado passa em qualquer verificação de PID.
- Cada tick também expira temporários (`cleanupExpired`) e recupera jobs órfãos (`recoverStaleJobs`). Não há cron separado.
- O log traz nome e código do erro. Nunca conteúdo de célula.
## 8. Observabilidade
| Item | Onde |
| --- | --- |
| Ambiente no Sentry | `SENTRY_ENVIRONMENT=staging`; sem a variável, cai em `ADAPTER_ENV` |
| Release | `SENTRY_RELEASE` = SHA do commit, injetado como build-arg da imagem |
| Sourcemap | `withSentryConfig` só atua com `SENTRY_ORG`, `SENTRY_PROJECT` e `SENTRY_AUTH_TOKEN` no build |
| Erros de rota e render | `onRequestError` em `src/instrumentation.ts` |
| Sanitização | `scrubSentryEvent` descarta texto livre, cookie, header de autorização e qualquer campo de célula |
| Liveness | `GET /api/health/live` — não toca dependência |
| Readiness | `GET /api/health/ready` — banco, migrations pendentes, Redis (PING), bucket (listagem), configuração e modo de job |
| Auditoria | `public.audit_log`, append-only para `app_user`, sem conteúdo de célula |
Teste a sanitização provocando um erro controlado e confirmando no Sentry que o evento não traz célula, cookie nem nome de pessoa.
## 9. Liberação da feature flag
Último passo, depois do smoke:
```bash
npm run flag -- <workspace_id> on
```
O script recusa um workspace que não existe e registra o evento em `audit_log`. A flag nasce desligada, inclusive em homologação.
## 10. Rollback
### Aplicação: rollback de imagem (reversível, é o caminho normal)
O artefato é imutável e marcado com o SHA do commit: `ghcr.io/<org>/<repo>:staging-<sha>`. Voltar é apontar o compose para a tag anterior. Nada de banco, bucket ou auditoria é tocado.
```bash
export C="docker compose -f docker-compose.staging.yml --env-file .env.staging"
# 1. Qual imagem está no ar agora. Anote: é o ponto de retorno se o rollback falhar.
$C ps --format '{{.Service}} {{.Image}}'
docker inspect "$($C ps -q web)" --format '{{.Config.Image}}'
# 2. Quais tags existem para voltar (mais recentes primeiro).
docker image ls 'ghcr.io/<org>/<repo>' --format '{{.Tag}} {{.CreatedAt}}' | sort -k2 -r | head
# 3. Desligar o módulo para os usuários. Efeito imediato, sem deploy.
# Faça isto ANTES de trocar a imagem: encerra o incidente de produto em segundos.
$C run --rm web flag <workspace_id> off
# 4. Trocar para a imagem anterior. Só web e worker; o Redis fica de pé.
export ADAPTER_IMAGE=ghcr.io/<org>/<repo>:staging-<sha-anterior>
docker pull "$ADAPTER_IMAGE"
$C up -d --no-deps web worker
# 5. Conferir que voltou e está saudável.
docker inspect "$($C ps -q web)" --format '{{.Config.Image}}'
curl -fsS https://hml-ada.elopro.app/api/health/live
curl -fsS https://hml-ada.elopro.app/api/health/ready
$C exec worker /app/docker/entrypoint.sh worker-health
# 6. Religar a flag só depois de confirmar o ambiente.
$C run --rm web flag <workspace_id> on
```
Para parar apenas o processamento, sem derrubar a interface:
```bash
$C stop -t 30 worker # respeita a carência: o lock é liberado
```
Jobs `queued` param de avançar e são retomados quando o worker volta. Jobs `processing` sem lock viram `INTERRUPTED` na próxima varredura e podem ser repetidos pelo usuário. Nenhuma saída já gerada é apagada: ela vence pelo TTL.
Fixe a tag no `.env.staging` (`ADAPTER_IMAGE=...:staging-<sha>`) para que um `up -d` acidental não traga de volta a imagem com defeito.
**Migrations não voltam no rollback de imagem.** As migrations do Adaptador são aditivas: a imagem anterior convive com o schema novo. Se a imagem anterior não conviver, o problema é de compatibilidade de schema e vira operação planejada, não rollback às pressas.
### Schema (não automatizado, de propósito)
Não existe rollback automático de schema e não deve existir.
| Migration | Caminho de volta |
| --- | --- |
| `0002_spreadsheet_adapter.sql` | `migrations/down/0002_spreadsheet_adapter_down.sql`, aplicado à mão, com backup antes |
| `0003_rls.sql` | sem down. Remover policy é afrouxar isolamento: não se desfaz, corrige-se para frente |
| `0004_audit_append_only.sql` | sem down. A trava de auditoria não se remove em rollback |
| `0001_platform_shim.sql` | sem down. O shim só existe em banco que não tem o schema do Elo Pro |
Regra: desligar a flag resolve o incidente de produto. Reverter imagem resolve o incidente de código. Mexer em schema é operação planejada com backup, nunca parte de um rollback às pressas.
**Migrations de segurança são preservadas em qualquer rollback.** `0003_rls.sql` (RLS e `FORCE RLS`) e `0004_audit_append_only.sql` (auditoria append-only) não têm e não terão caminho de volta: desfazê-las afrouxa isolamento entre workspaces e abre a auditoria para alteração pelo papel da aplicação. Um rollback que precise disso está errado por definição.
**Não apague `audit_log`, modelos, banco ou arquivos em nenhum rollback.**
## 11. Diagnóstico
| Sintoma | Causa provável | Verificação |
| --- | --- | --- |
| Container sai no start com código 1 | configuração recusada | `docker compose logs web` traz a lista de variáveis |
| `/api/health/live` não responde | processo caiu | configuração é a primeira hipótese: veja o log do start |
| `ready` com `config: missing` | variável ausente ou inválida | `configIssues` na resposta nomeia cada uma |
| `ready` com `redis: down` | senha, host ou contêiner | `docker compose ps redis` e o PING |
| `ready` com `storage: down` | bucket, nome ou service role | `npm run check:bucket` |
| `ready` com `migrations: missing` | migration pendente | `npm run migrate:check` |
| `ready` com `job_mode: missing` | `ADAPTER_JOB_MODE=inline` | corrija e suba de novo |
| Login cai em branco | `ADAPTER_STAGING_AUTH` desligado e sem `ELO_PRO_LOGIN_URL` | `/login` explica qual falta |
| Fica pedindo workspace | identidade sem associação | `npm run seed:staging` ou incluir em `workspace_members` |
| Job preso em `processing` | worker parado ou lock vencido | log do worker; a varredura marca `INTERRUPTED` |
| 429 nas chamadas | rate limiting | `Retry-After` na resposta; tetos em `src/server/rate-limit.ts` |
## 12. Ordem completa, do zero
1. DNS de `hml-ada.elopro.app`.
2. Certificado TLS só desse host.
3. `server` novo no Nginx, `nginx -t`, `systemctl reload nginx`.
4. Projeto Supabase de homologação; contas de teste no painel.
5. Bucket privado; `npm run check:bucket`.
6. `.env.staging` preenchido; `npm run verify:config`.
7. `npm run migrate:check` e depois `npm run migrate` (aprovação manual).
8. `config/staging-seed.json` e `npm run seed:staging`.
9. `up -d redis`, depois `up -d --no-deps web worker`.
10. `/api/health/live` responde `live`; `/api/health/ready` responde `ready`.
11. Smoke, nas duas fases (ver seção 13).
12. Roteiro manual de `21-checklist-validacao.md`: login real, isolamento, processamento assíncrono, download XLSX/CSV/PDF, erro no Sentry.
13. `npm run flag -- <workspace_id> on`.
14. Ensaio de rollback, com evidência.
15. Só então discutir produção.
## 13. Smoke de homologação
Fase 1 roda sem nenhuma credencial: saúde, recusa sem sessão, cabeçalhos de segurança e ausência de dev auth.
```bash
ADAPTER_SMOKE_URL=https://hml-ada.elopro.app npm run smoke:staging
```
Fase 2 só roda quando a operação fornece uma conta de teste **criada no painel do Supabase de homologação**. Nenhuma credencial fica no repositório, e o script não inventa conta: sem estas variáveis a fase 2 é reportada como PENDENTE, nunca como aprovada.
| Variável | Obrigatória | O que é |
| --- | --- | --- |
| `ADAPTER_SMOKE_URL` | sim | `https://hml-ada.elopro.app` |
| `ADAPTER_SMOKE_EMAIL` | fase 2 | e-mail de uma conta de teste do Supabase de homologação |
| `ADAPTER_SMOKE_PASSWORD` | fase 2 | senha dessa conta |
| `ADAPTER_SMOKE_WORKSPACE` | opcional | UUID do workspace que deve aparecer associado |
```bash
ADAPTER_SMOKE_URL=https://hml-ada.elopro.app ADAPTER_SMOKE_EMAIL=... ADAPTER_SMOKE_PASSWORD=... ADAPTER_SMOKE_WORKSPACE=... npm run smoke:staging
```
A fase 2 confere: login real emitindo sessão, associações de workspace resolvidas no servidor, seleção de workspace aceita e leitura autenticada do módulo. Com a feature flag ainda desligada, `FEATURE_DISABLED` é resposta correta e passa.
Continua fora do smoke, no roteiro manual, porque exige arquivo e flag ligada:
- envio de modelo e de fonte pela interface;
- processamento assíncrono com o worker e download XLSX, CSV e PDF;
- troca de workspace e sessão expirada no navegador;
- erro controlado no Sentry, confirmado sem conteúdo de célula;
- ensaio de rollback de imagem.
@@ -0,0 +1,163 @@
# Checklist de validação e evidências — homologação
Estado em 15 de outubro de 2026, nesta máquina de desenvolvimento, sem acesso ao servidor do ecossistema, ao Supabase de homologação nem ao banco do Elo Pro.
**Sem runtime de contêiner nesta máquina.** O Docker Desktop está instalado e foi iniciado, mas o WSL2 não tem distribuição instalada (`wsl --list` reporta nenhuma), então o engine Linux não sobe. Não há Postgres, Redis, Podman nem nerdctl locais. O que exige contêiner ou serviço real foi validado por **simulação fiel do layout de runtime da imagem** (apenas `node_modules` de produção + `src` + `scripts` + `migrations` + `tsconfig.json`, executado com `node --import tsx`) e, onde nem isso alcança, está PENDENTE para o CI.
Regra de preenchimento: **PASS** só com evidência executada. Sem infraestrutura ou credencial, o item é **PENDENTE** — nunca aprovado por inspeção de código.
## A. Verificação local do repositório
| # | Item | Resultado | Evidência |
| --- | --- | --- | --- |
| A1 | `npm run lint` | PASS | sem apontamentos |
| A2 | `npm run typecheck` | PASS | `tsc --noEmit` sem erro |
| A3 | `npm test` | PASS | 17 arquivos, **102 testes**, 1 skip proposital (smoke real) |
| A4 | `npm run build` | PASS | 25 rotas + Proxy, zero avisos, saída `standalone` com 70 MB contendo só `.next`, `node_modules`, `migrations`, `src`, `package.json` e `server.js` |
| A5 | Testes HTTP das rotas (novos) | PASS | `tests/http/routes.test.ts`, 12 casos |
| A6 | Testes do proxy (novos) | PASS | `tests/http/proxy.test.ts`, 7 casos |
| A7 | Política de ambiente e workspace (novos) | PASS | `tests/unit/staging-policy.test.ts`, 22 casos |
| A8 | Suíte com Postgres e Redis reais | PENDENTE | `npm run test:services` reporta 15 ignorados sem `ADAPTER_TEST_DATABASE_URL`/`ADAPTER_TEST_REDIS_URL`. O CI executa no job `services` |
| A9 | Sintaxe do compose local | PASS | `docker compose -f docker-compose.yml config` sem erro |
| A10 | Sintaxe do compose de homologação | PASS | `docker compose -f docker-compose.staging.yml config` resolve todas as variáveis |
| A11 | Build da imagem Docker | PENDENTE | Docker Desktop está instalado nesta máquina, foi iniciado durante o trabalho e o engine Linux não chegou a responder (`500` no pipe `dockerDesktopLinuxEngine`). O CI executa no job `image` |
| A12 | `docker compose up` local | PENDENTE | mesmo motivo de A11 |
| A13 | Artefato `standalone` roda de verdade | PASS | `node .next/standalone/server.js` sobe, responde e atende as rotas (ver B6, C14, D3) |
| A14 | Deps de produção cobrem a árvore traçada | PASS | `npm ci --omit=dev` em diretório limpo: 318 pacotes contra 153 traçados, **zero divergência de versão** entre os compartilhados; 3 pacotes existem só na traçada (`@emnapi/runtime`, `isarray`, `supports-color`) e o `COPY` do Docker mescla sem removê-los |
| A15 | Worker e scripts rodam sem devDependencies | PASS | os 13 pacotes externos usados por `src/` e `scripts/` existem no install de produção; `tsx` 4.23.15 e `esbuild` 0.28.2 presentes |
| A16 | Scripts operacionais executam no layout de runtime | PASS | no layout simulado: `verify-config`, `migrate`, `migrate --dry-run`, `seed-dev`, `cleanup-temp`, `set-flag` (recusa workspace inexistente), `seed-staging` (recusa em development) — todos com o código de saída correto |
## B. Fail-fast de configuração
| # | Item | Resultado | Evidência |
| --- | --- | --- | --- |
| B1 | Configuração incompleta é recusada | PASS | `tests/unit/staging-policy.test.ts`: `assertDeployedEnv` lista `DATABASE_URL` e `REDIS_URL` ausentes |
| B2 | `ADAPTER_DEV_AUTH=1` recusado fora de development | PASS | teste de política e `devAuthEnabled()` |
| B3 | `ADAPTER_JOB_MODE=inline` recusado em staging e produção | PASS | teste de política; `jobRunsInline` retorna `false` |
| B4 | Shim de identidade exige decisão explícita | PASS | ausência de `ADAPTER_APPLY_IDENTITY_SHIM` entra em `configIssues` |
| B5 | Sem `DATABASE_URL` nunca cai em PGlite | PASS | `requireDatabaseUrl` lança em staging e produção |
| B6 | O processo **sai** com código 1, em vez de responder 500 | PASS | executado no artefato construído: `ADAPTER_ENV=staging node .next/standalone/server.js` imprimiu a lista de variáveis, saiu com **código 1** e a porta ficou sem nada ouvindo. Antes desta entrega o mesmo comando deixava o servidor no ar respondendo 500 em todas as rotas, inclusive `/api/health/live` |
| B7 | `npm run verify:config` recusa e explica | PASS | em `ADAPTER_ENV=staging` sem variáveis: 9 itens nomeados, saída com código 1 |
| B8 | O worker também recusa configuração ausente | PASS | no layout de runtime, `ADAPTER_ENV=staging` sem variáveis: saída com código 1 antes de tocar banco ou Redis |
| B9 | Contêiner da web e do worker saem com código ≠ 0 | PENDENTE | passo `Configuração ausente encerra com código diferente de zero` do job `image`, que inspeciona `.State.ExitCode` dos dois |
## C. Autenticação e multi-tenant
| # | Item | Resultado | Evidência |
| --- | --- | --- | --- |
| C1 | API do módulo sem sessão responde 401 | PASS | `tests/http/routes.test.ts` |
| C2 | Página protegida sem sessão vai ao login | PASS | `tests/http/proxy.test.ts` |
| C3 | Cookie de workspace não é prova de autorização | PASS | `resolveWorkspace` ignora dica sem associação real; teste com dica forjada e com injeção |
| C4 | Gravar workspace de terceiro é recusado | PASS | `POST /api/auth/workspace` → 403 e cookie não gravado |
| C5 | Associações listadas são só as do usuário | PASS | teste com Eva, que só vê Beta |
| C6 | Módulo desligado bloqueia o workspace | PASS | teste com Diego em Beta |
| C7 | Papel sem permissão não altera configuração | PASS | teste com Bruno (agent) |
| C8 | Login de homologação indisponível fora de staging | PASS | `/api/auth/staging/session` → 404 |
| C16 | Dev auth não liga em homologação, em nenhuma combinação | PASS | `tests/unit/dev-auth-guard.test.ts`, 18 casos: matriz de ambientes na função, recusa na validação de configuração, e o provedor local fora da lista de provedores em staging e produção. Um token de dev válido é recusado quando o ambiente é staging |
| C9 | Isolamento entre workspaces na suíte de aplicação | PASS | `tests/security/isolation.test.ts`, 6 casos |
| C10 | RLS em PostgreSQL real, papel `app_user` sem BYPASSRLS | PENDENTE | `tests/services/postgres.test.ts` escrito; roda no CI e no banco de homologação |
| C11 | Login real no navegador, com conta do Supabase de homologação | PENDENTE | exige o projeto Supabase de homologação. O caminho equivalente foi executado no nível de development (C14) |
| C14 | Login real e isolamento no artefato em execução | PASS | contra `node server.js`: sessão emitida, `GET /api/auth/workspace` devolveu só o workspace da identidade, `POST` com workspace de terceiro → **403** sem gravar cookie, `POST` com o próprio → 200, `/adaptador` → 200 |
| C15 | Cookie de workspace forjado é ignorado em execução | PASS | três tentativas com a sessão de Ana (Alpha): cookie apontando Beta, cookie com `' or 1=1 --` e cookie com UUID inexistente. Em todas a resolução permaneceu em Alpha; Beta nunca foi alcançado |
| C12 | Troca de workspace na interface | PENDENTE | exige duas associações reais em homologação |
| C13 | Sessão expirada volta ao login | PENDENTE | exige sessão real |
## D. Infraestrutura e processamento
| # | Item | Resultado | Evidência |
| --- | --- | --- | --- |
| D1 | Rate limiting devolve 429 com `Retry-After` | PASS | `tests/http/routes.test.ts` |
| D2 | Cabeçalhos de segurança configurados | PASS | `next.config.ts`: CSP, HSTS, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`, COOP |
| D18 | Smoke prepara a fase autenticada do Supabase | PASS no script / PENDENTE na execução | `scripts/smoke-staging.ts` tem as duas fases; sem `ADAPTER_SMOKE_EMAIL`/`PASSWORD` a fase 2 imprime PENDENTE. Credenciais listadas na seção 13 do runbook |
| D3 | Cabeçalhos presentes na resposta HTTP real | PASS no artefato / PENDENTE atrás do Nginx | resposta de `/api/health/live` trouxe CSP, HSTS, `X-Frame-Options: DENY`, `Referrer-Policy`, `Permissions-Policy`, COOP, `X-Content-Type-Options` e `Cache-Control: no-store`. Falta confirmar que o Nginx não sobrescreve |
| D4 | Redis e Supabase reutilizados por processo | PASS | singleton em `src/infra/redis/index.ts` e `src/infra/storage/index.ts` |
| D5 | Namespace separado por aplicativo e ambiente | PASS | `resourceNamespace`; teste confirma que staging ≠ production |
| D6 | Listagem de bucket paginada | PASS | `src/infra/storage/supabase.ts` pagina com `offset` e desce em subpastas |
| D7 | Bucket privado conferido | PENDENTE | `npm run check:bucket` exige o bucket real |
| D8 | Lock entre processos com Redis real | PENDENTE | `tests/services/redis.test.ts`; roda no CI |
| D9 | Processamento assíncrono entre processos | PENDENTE | `.github/scripts/async-journey.sh` percorre web → Redis → worker → download no compose; roda no job `image` |
| D9b | Jornada assíncrona pelo loop real do worker | PASS | `tests/integration/worker.test.ts`: a web enfileira e devolve `queued`, o download falha antes do tick, o tick conclui e os três formatos saem; o modelo original não muda |
| D9c | Job sobrevive à perda da fila do Redis | PASS | fila drenada à mão: a varredura do Postgres acha o job `queued` e conclui |
| D9d | Worker relê o contexto do dono | PASS | flag do módulo desligada entre o enfileiramento e o tick: o job não é processado |
| D10 | Worker recusa iniciar com migration pendente | PASS por construção | `pendingMigrations` no start; a execução real é PENDENTE |
| D11 | Desligamento gracioso do worker | PASS no loop / PENDENTE no sinal | `shutdown()` para de aceitar trabalho, espera o tick corrente sair, e o job que não começou fica `queued` com o lock reservado para ele; concluído, o lock é liberado. A **entrega do SIGTERM** ao processo é PENDENTE: no Windows não há sinal real, e o job `image` do CI confere `SIGTERM recebido`, `[worker] encerrado` e exit 0 em menos de 25 s |
| D17 | Heartbeat do worker e healthcheck | PASS | worker no layout de runtime gravou o heartbeat; `docker/worker-health.mjs` aprovou com 1 s e reprova quando o arquivo falta |
| D12 | Download XLSX, CSV e PDF ponta a ponta | PASS no local / PENDENTE no ambiente | `tests/integration/journey.test.ts` cobre o fluxo; falta a jornada em homologação |
| D13 | Modelo original não é alterado | PASS | `tests/unit/xlsx-fixtures.test.ts` compara bytes |
| D14 | TTL de temporários aplicado | PASS no local / PENDENTE no ambiente | `tests/integration/journey.test.ts`; falta observar o vencimento real |
| D15 | Health check responde `ready` com dependências satisfeitas | PASS | primeira chamada com banco frio: `{"status":"ready"}` com `config`, `database`, `migrations`, `redis`, `storage` e `job_mode` em `ok`, sem connection string na resposta |
| D16 | Health check aponta a dependência que falhou | PASS | campo `reasons` traz só o nome do erro e os ids de migration pendente |
## E. Observabilidade
| # | Item | Resultado | Evidência |
| --- | --- | --- | --- |
| E1 | Payload do Sentry sem conteúdo de célula | PASS | `tests/unit/pilot-policy.test.ts` com sentinela |
| E2 | Ambiente e release por ambiente | PASS | `sentryInitOptions` usa `ADAPTER_ENV` e `SENTRY_RELEASE` |
| E3 | `onRequestError` registrado | PASS | export em `src/instrumentation.ts` |
| E4 | Evento real chega ao projeto Sentry de homologação | PENDENTE | exige DSN real |
| E5 | Sourcemap com stack legível | PENDENTE | exige `SENTRY_ORG`, `SENTRY_PROJECT` e `SENTRY_AUTH_TOKEN` no build |
| E6 | Auditoria sem conteúdo de célula | PASS | `tests/integration/rls.test.ts` e `sanitizeAuditMetadata` |
## F. Ambiente e deploy
| # | Item | Resultado | Evidência |
| --- | --- | --- | --- |
| F1 | Dockerfile multistage com `standalone` | PASS | `Dockerfile` + `output: "standalone"` |
| F2 | Scripts operacionais funcionam sem devDependencies | PASS | executados no layout de runtime simulado (ver A15 e A16), não apenas por construção |
| F12 | Job de build Docker Linux no CI com validação da imagem | PASS no workflow / PENDENTE na execução | job `image`: build, conteúdo, usuário não-root, ausência de segredo/docs/testes, carga dos scripts, fail-fast dos dois processos, liveness com cabeçalhos, compose completo, jornada assíncrona entre processos e desligamento gracioso |
| F13 | Rollback de imagem documentado | PASS | runbook seção 10: identificar a imagem no ar, listar tags, desligar a flag antes, trocar só web e worker com `--no-deps`, conferir saúde, religar a flag. `0003_rls.sql` e `0004_audit_append_only.sql` declaradas sem caminho de volta |
| F3 | Node e gerenciador fixados | PASS | `.nvmrc` 24.11.1, `engines.node >=22.12 <25`, `packageManager npm@11.6.2` |
| F4 | Compose isolado dos outros aplicativos | PASS | nome de projeto, rede e volume próprios; portas só em `127.0.0.1`; deploy com `--no-deps` |
| F5 | `.env.staging.example` sem segredo | PASS | todos os campos sensíveis vazios |
| F6 | Migration, deploy e flag em jobs separados com aprovação | PASS | `.github/workflows/deploy-staging.yml`, ambientes `hml-migracao`, `hml-deploy`, `hml-flag` |
| F7 | Deploy real em `hml-ada.elopro.app` | PENDENTE | exige servidor, DNS e segredos |
| F8 | DNS e TLS do subdomínio | PENDENTE | exige acesso ao DNS |
| F9 | Nginx do novo `server` | PENDENTE | exige acesso ao servidor |
| F10 | Smoke de homologação | PENDENTE | `npm run smoke:staging` exige o ambiente |
| F11 | Ensaio de rollback | PENDENTE | exige duas imagens publicadas e o ambiente |
## Resumo
| | Quantidade |
| --- | --- |
| PASS com evidência executada | 54 |
| PENDENTE por falta de infraestrutura ou credencial | 20 |
| FAIL | 0 |
Nada foi marcado PASS por inspeção de código. Os PENDENTE que dependem só de Docker (A11, A12, A8, B9, C10, D8, D9, F12) executam no CI no primeiro push; os demais dependem de infraestrutura do ecossistema.
## Defeitos encontrados nesta rodada de validação
Os quatro apareceram justamente ao exercitar o layout de runtime da imagem, e nenhum aparecia na suíte de testes ou no build.
| # | Defeito | Efeito em homologação | Correção |
| --- | --- | --- | --- |
| 1 | O CLI do `tsx` cria processo filho | SIGTERM do `docker stop` pararia no pai; o worker nunca rodaria o handler, o lock esperaria o TTL de 120 s e o `stop_grace_period` só atrasaria um SIGKILL | entrypoint usa `node --import tsx`; PID conferido igual |
| 2 | Top-level await nos scripts com `--import tsx` | `migrate`, `worker`, `seed-staging`, `set-flag`, `cleanup` e `check-bucket` falhariam com `Transform failed`. O worker não subiria e a migração não rodaria | `runScript`/`runWithDb` em `scripts/_bootstrap.ts`; nenhum script usa top-level await |
| 3 | `import "server-only"` em `worker-loop.ts` | o pacote lança fora do bundler do Next: o worker morreria no start | removido do módulo compartilhado, com o motivo registrado no arquivo |
| 4 | `@react-pdf/renderer` carregado no import de `process-job.ts` | `ERR_PACKAGE_PATH_NOT_EXPORTED` ao resolver a hifenização fora do bundler: derrubava o worker e o `cleanup` | import tardio dentro do ramo de PDF, que só o download usa |
## Bloqueios para liberar homologação
Todos externos ao repositório. Nenhum depende de código.
1. DNS de `hml-ada.elopro.app` e certificado TLS.
2. Projeto Supabase de homologação, com contas de teste e bucket privado.
3. Banco de homologação com `DATABASE_URL` e a decisão do shim conferida.
4. Servidor com Docker, `HML_DEPLOY_PATH` e os segredos dos ambientes do GitHub.
5. DSN do Sentry de homologação.
## Bloqueios para discutir produção
Além de tudo acima, concluído:
1. Smoke real completo, com evidência, em homologação.
2. Ensaio de rollback executado, com evidência.
3. SSO do Elo Pro integrado e validado no navegador.
4. Schema real do Elo Pro confirmado e `identity-port.ts` apontando para ele.
5. Tokens oficiais do Design System. O azul `#0B3D91` continua provisório.
## Evidências a anexar
Para cada item PENDENTE que for executado, registre aqui: data, quem executou, comando ou tela, e saída obtida. Resposta de `GET /api/health/ready` (sem segredo), log do worker concluindo um job, print do download dos três formatos, print do evento sanitizado no Sentry e log do ensaio de rollback.