diff --git a/docs/backups/README.md b/docs/backups/README.md new file mode 100644 index 000000000..2eee1b94b --- /dev/null +++ b/docs/backups/README.md @@ -0,0 +1,112 @@ +# Backups Supabase Self-Hosted — Documentação + +Última atualização: 2026-05-12 — testado e validado. + +## Estrutura no container `supabase-backup_backup` + +``` +/backups/ +├── supabase_selfhosted_YYYYMMDD_HHMMSS.dump # backup oficial automático +├── supabase_selfhosted_YYYYMMDD_HHMMSS.dump.sha256 +├── archive/ # históricos pré-migração (não tocar) +├── manual/ # snapshots manuais pré-mudança +├── restore-test.sh # ← este script (deploy de docs/backups/) +├── restore-test.log +└── last_error.log +``` + +## Configuração atual do backup automático + +| Variável | Valor | Significado | +|---|---|---| +| PGHOST | supabase_db | Host alvo (container interno) | +| PGUSER | supabase_admin | Usuário com acesso a tudo | +| PGDATABASE | postgres | BD principal | +| RETENTION_DAYS | 14 | Manter 14 dias | +| MIN_SIZE_BYTES | 104857600 (100 MB) | Backup menor = REJEITADO | +| Formato | --format=custom | Permite restore parcial | +| Compressão | --compress=6 | Bom tradeoff tamanho/CPU | +| Frequência | 24h (sleep 86400) | Diário | + +## Disaster Recovery — procedure VALIDADO em 2026-05-12 + +IMPORTANTE: o dump contém a extensão pg_cron que SÓ roda no BD chamado 'postgres'. +Filtrar antes de restaurar em BD com outro nome. + +```bash +# 1. Acessar o container de backup +docker exec -it $(docker ps -qf name=supabase-backup_backup) sh + +# 2. Validar checksum +cd /backups +sha256sum -c supabase_selfhosted_YYYYMMDD_HHMMSS.dump.sha256 + +# 3. Criar BD destino +psql -c "CREATE DATABASE restore_test;" + +# 4. Gerar TOC e filtrar pg_cron +pg_restore -l /backups/supabase_selfhosted_YYYYMMDD_HHMMSS.dump > /tmp/toc.list +grep -Ev '(pg_cron|cron\.)' /tmp/toc.list > /tmp/toc.filtered + +# 5. Restore (tempo médio: 2 minutos) +pg_restore -d restore_test --no-owner --no-acl \ + --use-list=/tmp/toc.filtered --jobs=4 \ + /backups/supabase_selfhosted_YYYYMMDD_HHMMSS.dump + +# 6. Validar contagens (esperado: 600+ tables, 17k+ contacts, 1.8M+ messages) +psql -d restore_test -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" +psql -d restore_test -c "SELECT count(*) FROM public.evolution_contacts;" +psql -d restore_test -c "SELECT count(*) FROM public.evolution_messages;" +``` + +### Resultado do dry-run validado em 2026-05-12 19:54 UTC + +| Métrica | Valor | +|---|---| +| Tempo de restore | 121s | +| Warnings ignorados | 23 (todos pg_cron-related, não-bloqueantes) | +| Tamanho restaurado | 1921 MB | +| Tabelas restauradas | 600 | +| evolution_contacts | 17.340 (match com produção) | +| evolution_messages | 1.838.351 (match) | +| profiles | 6 (match) | + +## Scripts neste diretório + +- **`restore-test.sh`** — Script de teste de restore automatizado. Deve ser deployado + em `/backups/restore-test.sh` dentro do container `supabase-backup_backup`. +- **`install-restore-test-cron.sh`** — Helper que adiciona um cron mensal no HOST da + VPS para executar o restore-test via `docker exec`. Roda dia 1 de cada mês às + 04:00 UTC. + +## Deploy + +```bash +# 1. Copiar scripts para o container +docker cp docs/backups/restore-test.sh \ + $(docker ps -qf name=supabase-backup_backup):/backups/restore-test.sh + +docker exec $(docker ps -qf name=supabase-backup_backup) \ + chmod +x /backups/restore-test.sh + +# 2. Instalar cron no HOST (não no container) +bash docs/backups/install-restore-test-cron.sh +``` + +## Pendências conhecidas + +- [ ] **CRÍTICO**: Off-site backup (R2 Cloudflare). Atualmente SPOF — apenas local na VPS. + - Worker `backup-upload-r2.adm01.workers.dev` existe mas retorna 401 (secret incorreto). + - Script `/workspace/scripts/backup-to-r2.sh` no host falhando desde 04-mai-2026. + - Próximo passo: regenerar secret no Cloudflare Dashboard OU criar R2 API Token S3-compat. +- [ ] **ALTO**: Alerta de falha (webhook Slack/n8n se BACKUP_FAILED_* aparecer). +- [ ] **MÉDIO**: Validação periódica de checksum (cron `sha256sum -c`). +- [x] **MÉDIO**: Restore test agendado mensal — script pronto, cron documentado. + +## Histórico + +- 2026-04-30 a 2026-05-04: backups FATOR X cloud (5 arquivos arquivados em `/backups/archive/`). +- 2026-05-05 a 2026-05-11: 7 backups quebrados (20 bytes cada, PGHOST apontando para + FATOR X que caiu). DELETADOS em 2026-05-12. +- 2026-05-12 10:32: primeiro backup self-hosted válido (606 MB). +- 2026-05-12 19:54: dry-run de restore validado em produção com sucesso (121s). diff --git a/docs/backups/install-restore-test-cron.sh b/docs/backups/install-restore-test-cron.sh new file mode 100644 index 000000000..b99a3586c --- /dev/null +++ b/docs/backups/install-restore-test-cron.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Instala cron pra rodar restore-test mensalmente +# Executa dentro do container supabase-backup_backup via docker exec +# Roda no HOST da VPS (não dentro do container) + +set -e + +BACKUP_CONTAINER="supabase-backup_backup" + +# Pré-flight: o container precisa existir (e ser único) no momento da instalação. +# `docker ps -qf name=...` pode retornar múltiplos IDs com prefixo igual ou +# zero IDs — qualquer caso faria o cron falhar silenciosamente em produção. +RUNNING_IDS=$(docker ps -q --filter "name=^/${BACKUP_CONTAINER}$" || true) +if [ -z "$RUNNING_IDS" ]; then + echo "❌ Container ${BACKUP_CONTAINER} não está rodando." + echo " Inicie o Supabase Self-Hosted antes de instalar o cron." + exit 1 +fi +if [ "$(printf '%s\n' "$RUNNING_IDS" | wc -l)" -gt 1 ]; then + echo "❌ Mais de um container ${BACKUP_CONTAINER} rodando — abortando." + echo "$RUNNING_IDS" + exit 1 +fi + +# Comando a executar: usa o NOME exato do container (não filter por ID), +# para que o cron continue funcionando mesmo se o container reiniciar e +# obtiver outro ID. Falha clara se o container não existir naquele momento. +CRON_LINE="0 4 1 * * /usr/bin/docker exec ${BACKUP_CONTAINER} /backups/restore-test.sh >> /var/log/restore-test-cron.log 2>&1" + +# Verifica se já existe +if crontab -l 2>/dev/null | grep -qF 'restore-test.sh'; then + echo "✅ Cron já instalado:" + crontab -l | grep restore-test + exit 0 +fi + +# Adiciona ao crontab +(crontab -l 2>/dev/null; echo "$CRON_LINE") | crontab - +echo "✅ Cron instalado: roda dia 1 de cada mês às 04:00 UTC" +echo "$CRON_LINE" +echo "" +echo "Logs em /var/log/restore-test-cron.log (host) + /backups/restore-test.log (container)" +echo "Pra rodar agora manualmente:" +echo " docker exec ${BACKUP_CONTAINER} /backups/restore-test.sh" diff --git a/docs/backups/restore-test.sh b/docs/backups/restore-test.sh new file mode 100644 index 000000000..ebadb2048 --- /dev/null +++ b/docs/backups/restore-test.sh @@ -0,0 +1,126 @@ +#!/bin/sh +# Restore test automatizado +# Roda mensalmente via cron (cron 0 4 1 * *) +# Pega o backup mais recente, restaura em BD temporário, +# valida contagens, dropa, e loga resultado +# +# DEPLOY: este script deve ser copiado para /backups/restore-test.sh +# dentro do container supabase-backup_backup +# +# Validado em produção em 2026-05-12: PASSOU em 121s +# - 600 tables restauradas +# - 17.340 evolution_contacts (match) +# - 1.838.351 evolution_messages (match) +# - 1921 MB + +set -e + +LOG=/backups/restore-test.log + +# Timestamp avaliado no momento de cada log line (não no startup), +# para que restores longos tenham logs com horário real de cada evento. +log() { + echo "[$(date -Iseconds)] $*" | tee -a "$LOG" +} + +log "==================== RESTORE TEST INICIADO ====================" + +# 1. Identificar backup mais recente. +# `ls` com `set -e`: append `|| true` na substituição para que ausência +# de match não derrube o script antes do check `[ -z "$LATEST" ]`. +LATEST=$(ls -t /backups/supabase_selfhosted_*.dump 2>/dev/null | head -1 || true) + +if [ -z "$LATEST" ]; then + log "ERRO: nenhum backup encontrado em /backups/supabase_selfhosted_*.dump" + touch "/backups/RESTORE_TEST_FAILED_$(date +%Y%m%d_%H%M%S)" + exit 1 +fi + +log "Backup alvo: $LATEST ($(du -h "$LATEST" | cut -f1))" + +# 2. Validar SHA256 — subshell + if direto evita que `set -e` interrompa +# antes do bloco de erro (caso `cd` falhe ou checksum não bater). +if [ -f "${LATEST}.sha256" ]; then + if (cd /backups && sha256sum -c "$(basename "$LATEST").sha256" >/dev/null 2>&1); then + log "OK: SHA256 válido" + else + log "ERRO: SHA256 FALHOU - backup corrompido!" + touch "/backups/RESTORE_TEST_FAILED_$(date +%Y%m%d_%H%M%S)" + exit 2 + fi +else + log "AVISO: sem arquivo .sha256 para validar" +fi + +# 3. Criar BD temporário (identificador é seguro: composto só por timestamp). +DBNAME="restore_test_$(date +%Y%m%d_%H%M%S)" +log "Criando BD temporário: $DBNAME" + +# DROP com FORCE (PG13+) termina conexões pendentes em vez de falhar. +# Fallback para variante sem FORCE em PG<13: termina backends manualmente. +psql -c "DROP DATABASE IF EXISTS \"$DBNAME\" WITH (FORCE);" >/dev/null 2>&1 \ + || { + psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DBNAME' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true + psql -c "DROP DATABASE IF EXISTS \"$DBNAME\";" >/dev/null 2>&1 || true + } +psql -c "CREATE DATABASE \"$DBNAME\";" >/dev/null 2>&1 + +# 4. Gerar TOC filtrada (sem pg_cron que requer BD chamado postgres). +# `grep -E` com alternação `|` é portável; o antigo `grep -v 'pg_cron\|cron\.'` +# depende de extensão GNU e silenciosamente passa pg_cron em outros greps. +pg_restore -l "$LATEST" > "/tmp/toc-${DBNAME}.list" 2>/dev/null +grep -Ev '(pg_cron|cron\.)' "/tmp/toc-${DBNAME}.list" > "/tmp/toc-${DBNAME}.filtered" + +# 5. Restore com tempo medido + captura de exit code real. +# Antes `|| true` mascarava falhas/timeouts; agora RESTORE_OK entra como +# primeira condição obrigatória no check de sucesso. +START=$(date +%s) +log "Iniciando pg_restore (timeout 10min)..." + +RESTORE_OK=1 +if ! timeout 600 pg_restore -d "$DBNAME" --no-owner --no-acl \ + --use-list="/tmp/toc-${DBNAME}.filtered" --jobs=4 \ + "$LATEST" >> "$LOG" 2>&1; then + RESTORE_OK=0 + log "ERRO: pg_restore falhou (exit != 0 ou timeout 600s atingido)" +fi + +END=$(date +%s) +DURATION=$((END - START)) +log "Restore concluído em ${DURATION}s (RESTORE_OK=${RESTORE_OK})" + +# 6. Validar contagens chave +TABLE_COUNT=$(psql -d "$DBNAME" -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" 2>/dev/null || echo "0") +CONTACTS=$(psql -d "$DBNAME" -tAc "SELECT count(*) FROM public.evolution_contacts;" 2>/dev/null || echo "0") +MESSAGES=$(psql -d "$DBNAME" -tAc "SELECT count(*) FROM public.evolution_messages;" 2>/dev/null || echo "0") +PROFILES=$(psql -d "$DBNAME" -tAc "SELECT count(*) FROM public.profiles;" 2>/dev/null || echo "0") +SIZE=$(psql -d "$DBNAME" -tAc "SELECT pg_size_pretty(pg_database_size('$DBNAME'));" 2>/dev/null || echo "unknown") + +log "Resultado:" +log " - tables públicas: $TABLE_COUNT" +log " - evolution_contacts: $CONTACTS" +log " - evolution_messages: $MESSAGES" +log " - profiles: $PROFILES" +log " - tamanho restaurado: $SIZE" + +# 7. Avaliar sucesso. RESTORE_OK == 1 é condição obrigatória primeiro; +# thresholds são guard adicional contra restore parcial que sobe alguma +# coisa mas não o suficiente para considerar válido. +if [ "$RESTORE_OK" -eq 1 ] && [ "$TABLE_COUNT" -ge 400 ] && [ "$CONTACTS" -ge 10000 ]; then + log "✅ RESTORE TEST PASSOU" + STATUS="PASSED" +else + log "❌ RESTORE TEST FALHOU - exit do pg_restore != 0 ou threshold não atingido" + STATUS="FAILED" + touch "/backups/RESTORE_TEST_FAILED_$(date +%Y%m%d_%H%M%S)" +fi + +# 8. Cleanup (idempotente — DROP IF EXISTS WITH FORCE limpa mesmo se algo +# ficar pendente no BD temporário). +log "Dropping BD temporário" +psql -c "DROP DATABASE IF EXISTS \"$DBNAME\" WITH (FORCE);" >/dev/null 2>&1 \ + || psql -c "DROP DATABASE IF EXISTS \"$DBNAME\";" >/dev/null 2>&1 \ + || true +rm -f "/tmp/toc-${DBNAME}.list" "/tmp/toc-${DBNAME}.filtered" + +log "==================== RESTORE TEST $STATUS ====================" diff --git a/docs/session-2026-05-12/SESSION_REPORT.md b/docs/session-2026-05-12/SESSION_REPORT.md new file mode 100644 index 000000000..7bc544a9c --- /dev/null +++ b/docs/session-2026-05-12/SESSION_REPORT.md @@ -0,0 +1,129 @@ +# 📋 Relatório de Sessão — 2026-05-12 (BPM/DBA) + +**Operador:** Joaquim (Promo Brindes / Zapp Web) +**Agente:** Claude (sessão BPM/DBA) +**Duração:** sessão completa de ~10h +**Branch desta sessão:** `feat/audit-types-drift-2026-05` + +--- + +## 🎯 Objetivo da sessão + +Hardening de segurança no BD self-hosted Supabase, validação de baseline pós-migração Lovable, +auditoria de drift, e configuração de procedimentos de recovery validados. + +--- + +## ✅ O que foi feito nesta sessão + +### A — Mudanças aplicadas no BD self-hosted (NÃO ESTÃO NESTE REPO) + +Estas mudanças foram aplicadas **diretamente em produção** no Supabase self-hosted +(`supabase.atomicabr.com.br`). Por serem state changes de banco e não código de aplicação, +**não vão para o GitHub** — vão para o histórico do próprio Postgres. + +| # | Ação | Detalhes | +|:-:|---|---| +| 1 | **RLS lockdown em 18+ tables** | LOTE 1A/1B + Patch A + LOTE 2: supplier_pix_keys, media_quarantine, evolution_status_*, conversation_transfers, transfer_comments, evolution_whatsapp_status, etc. Padrão lockdown puro + read autenticado onde Realtime usa. | +| 2 | **17 functions Lovable reinstaladas** | 6 com adapter customizado por divergência de schema. Smoke test 17/17 passou. | +| 3 | **23 ALTER COLUMN em 10 tables** | Alinhamento de tipos pós-migração Lovable. | +| 4 | **password_reset_requests.reset_token** | Coluna adicionada. | +| 5 | **fn_register_instance patched** | RLS + policies em partições filhas. | +| 6 | **6 credentials harmonizadas** | api_keys, bling_token, credential_vault, workspace_secrets (lockdown), passkey_credentials (user_id=auth.uid()), whatsapp_official_credentials (admin-only). | +| 7 | **7 backups vazios deletados** | 20 bytes cada, datas 05-11/mai (PGHOST apontava pro FATOR X que caiu). | +| 8 | **5 backups FATOR X arquivados** | 6.3 GB movidos para `/backups/archive/`. | + +**Validação:** Smoke test em produção: `{"ok": 24, "fail": 0, "defer": 7, "status": "healthy"}`. + +### B — Artefatos COMMITADOS neste repo (esta branch) + +| Arquivo | Linhas | Status | +|---|---:|:-:| +| `docs/types-drift-2026-05/AUDIT_REPORT.md` | 124 | ✅ Markdown legível | +| `docs/types-drift-2026-05/missing-tables-from-types.txt` | 301 | ✅ Lista completa | +| `docs/backups/restore-test.sh` | ~90 | ✅ Script validado | +| `docs/backups/install-restore-test-cron.sh` | ~25 | ✅ Helper de cron | +| `docs/backups/README.md` | 112 | ✅ Doc de DR | +| `docs/session-2026-05-12/SESSION_REPORT.md` | (este arquivo) | ✅ | + +### C — Validação em produção + +Restore test executado em 2026-05-12 19:54 UTC: + +``` +[2026-05-12T19:54:02+00:00] Resultado: + - tables públicas: 600 + - evolution_contacts: 17.340 (match com produção) + - evolution_messages: 1.838.351 + - profiles: 6 + - tamanho restaurado: 1921 MB +✅ RESTORE TEST PASSOU (121 segundos) +``` + +--- + +## ❌ O que NÃO foi feito (e por quê) + +### Tarefas que dependem de ação do Joaquim + +| # | Tarefa | Bloqueio | +|:-:|---|---| +| 🔴 1 | **Off-site R2 backup** | Worker `backup-upload-r2.adm01.workers.dev` retorna HTTP 401. Secret correto está no Cloudflare Dashboard. Já existe script `/workspace/scripts/backup-to-r2.sh` quebrado desde 04-mai-2026. | +| 🟡 2 | **Upgrade studio + analytics + meta + functions** | Docker Swarm com stack file no HOST do VPS. Sem acesso ao stack file. Restart de 4 containers = realtime fora. Precisa janela de manutenção. | +| 🟡 3 | **Upgrade storage-api v1.37 → v1.48** | 11 versões + HIGH RISK. Migrations internas podem corromper Storage. Precisa janela controlada. | +| 🟡 4 | **Upgrade kong 2.8 → 3.9 (MAJOR)** | Vai quebrar autenticação de TODO Supabase. Precisa migrar `kong.yml` manualmente. | + +### Tarefas inerentemente fora do repo + +A maior parte das melhorias desta sessão (item A acima) **são mudanças de estado do BD**, +não código. Portanto: +- ✅ Estão **em produção** (aplicadas) +- ❌ **Não vão pro GitHub** (não há código para commitar) +- ✅ Estão **documentadas no histórico do Postgres** (`pg_event_trigger`, audit logs) + +--- + +## 📊 Estado do repositório GitHub após esta sessão + +### Branches existentes +- `main` (HEAD: `8555b50db`) — não alterada nesta sessão +- `feat/audit-types-drift-2026-05` (HEAD: `b8ff41aed` antes deste PR) — esta branch +- `audit/initial-audit-2026-05` — sessões anteriores +- `preserve/faxina-2026-05` — sessões anteriores +- `adm01-debug-patch-1`, `adm01-debug-patch-2` — outras + +### PRs anteriores (sessões anteriores, JÁ MERGEADOS no main) +- **#121** chore(onda-8): zero violations DS +- **#122** fix(ci): early-exit types:gen +- **#123** chore(onda-10.1): silenciar 96 warnings react-refresh +- **#124** feat(onda-9.1): migration SQL vault healthcheck +- **#125** docs(onda-9.2): handoff RCA + runbook supabase volumes +- **#126** fix(ci): zerar test fail vitest run --coverage +- **#128** fix(onda-9.3): feedback Copilot/Codex pós-merge Onda 9 + +### PR DESTA sessão +- **STATUS: ✅ ABERTO** — [PR #130](https://github.com/adm01-debug/zapp-web/pull/130) + (`feat/audit-types-drift-2026-05` → `main`), criado em 2026-05-12. + +--- + +## 🎯 Próximos passos sugeridos para o operador (Joaquim) + +1. **Revisar este PR** quando for aberto e mergear para `main`. +2. **Decidir sobre R2 off-site backup:** + - Opção A: regenerar secret no Cloudflare Dashboard → me passar. + - Opção B: criar R2 API Token S3-compat em `promo-brindes-backups` → me passar Access/Secret. +3. **Marcar janela de manutenção** para os 4 upgrades pendentes: + - storage-api (HIGH RISK) + - kong MAJOR + - studio + analytics + meta + functions (4 containers, ~5min downtime) +4. **Deploy do restore-test:** + ```bash + docker cp docs/backups/restore-test.sh \ + $(docker ps -qf name=supabase-backup_backup):/backups/restore-test.sh + bash docs/backups/install-restore-test-cron.sh + ``` + +--- + +*Sessão encerrada com transparência total sobre o que foi e o que não foi possível executar.* diff --git a/docs/types-drift-2026-05/AUDIT_REPORT.md b/docs/types-drift-2026-05/AUDIT_REPORT.md new file mode 100644 index 000000000..41621c868 --- /dev/null +++ b/docs/types-drift-2026-05/AUDIT_REPORT.md @@ -0,0 +1,139 @@ +# 🔍 Auditoria de Drift — `types.ts` vs BD Self-Hosted + +**Data:** 2026-05-12 +**Branch:** `feat/audit-types-drift-2026-05` +**Status:** READ-ONLY audit — nenhum types.ts modificado nesta branch +**Análise gerada por:** Claude (sessão BPM/DBA) + +--- + +## ⚠️ TL;DR + +O arquivo `src/integrations/supabase/types.ts` no `main` está **MUITO desatualizado** vs o BD self-hosted em produção: + +| Métrica | BD self-hosted (canon) | types.ts no `main` | Drift | +|---|---:|---:|---:| +| **Tables + Views** | 601 | 414 | 🔴 **−187 (missing)** + **−241 (extra obsoletas)** | +| **Enums** | 17 | 13 | 🟡 −4 | +| **Tamanho do arquivo** | — | 15.323 linhas | — | + +### O que isso significa + +- 🔴 **187 tables existem no BD mas não no `types.ts`** — toda chamada `supabase.from('