From 29d78708112469a71d754ba0a6d867b3921fdf31 Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Fri, 10 Jul 2026 19:53:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(ops):=20sync=20de=20config/contas=20dev?= =?UTF-8?q?=E2=86=92prod=20por=20upsert=20seguro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dev (scoreodonto_dev, pgdev/VPS4) e prod (scoreodonto, VPS3) são bancos SEPARADOS — config feita em dev não reflete em prod. Era por isso que o modal de Comunicação Interna ("Preencha para liberar o sistema") seguia pedindo número em produção: pessoa_comunicacao e clinicas_horarios estavam vazias lá. Script de sincronização das 6 tabelas de config/conta (usuarios, clinicas, vinculos, pessoa_comunicacao, clinicas_horarios, feriados) por upsert: carrega dev numa staging em prod e faz INSERT ... ON CONFLICT DO UPDATE, na ordem de FK, dentro de uma transação. INSERE o que falta, ATUALIZA o que mudou, e NUNCA apaga registros que só existam em prod (não há nenhum — dev é superset de prod). NÃO toca pacientes/agenda/financeiro; NÃO migra sessões de WhatsApp. Sem argumento = dry-run (ROLLBACK); --apply faz backup do prod antes e comita. Primeira aplicação (10/jul): +4 pessoa_comunicacao, +13 clinicas_horarios (resolve o modal), +3 usuarios, +4 clinicas, +4 vinculos; e corrigiu 2 divergências reais que dev já tinha certas — nome de um usuário e o timezone da clínica principal (America/Sao_Paulo → America/Campo_Grande). Pacientes intocados (1750). Co-Authored-By: Claude Opus 4.8 --- sync-config-dev-para-prod.sh | 86 ++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100755 sync-config-dev-para-prod.sh diff --git a/sync-config-dev-para-prod.sh b/sync-config-dev-para-prod.sh new file mode 100755 index 0000000..9034e36 --- /dev/null +++ b/sync-config-dev-para-prod.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# ============================================================================= +# Sincroniza CONFIGURAÇÃO + CONTAS do scoreodonto de DEV → PROD (upsert seguro). +# +# DEV : scoreodonto_dev (pgdev, VPS4) — fonte da verdade da config +# PROD: scoreodonto (10.99.0.3, VPS3) — 1.750 pacientes reais; NÃO destruir +# +# Método: para cada tabela do escopo, carrega os dados de dev numa tabela staging +# em prod e faz INSERT ... ON CONFLICT (pk) DO UPDATE. Isso INSERE o que falta e +# ATUALIZA o que mudou, mas NUNCA apaga registros que só existam em prod. +# +# NÃO toca em: pacientes, agendamentos, financeiro, tratamentos, orçamentos, etc. +# NÃO migra sessões de WhatsApp (fora do escopo por decisão do dono). +# +# ./sync-config-dev-para-prod.sh → DRY-RUN (mostra o que mudaria, ROLLBACK) +# ./sync-config-dev-para-prod.sh --apply → aplica de verdade (faz backup antes) +# +# Requisitos: rodar na VPS4 (acesso ao pgdev local e ao Postgres da VPS3 pela VPN). +# A senha do prod vem de scoreodonto.com/.env na VPS1 (SCOREO_DB_PASS) — passe via +# env SCOREO_DB_PASS=... ou o script tenta buscá-la por SSH. +# ============================================================================= +set -euo pipefail + +APPLY="${1:-}" +DEV_CT="pgdev" +DEV_DB="scoreodonto_dev" +DEV_USER="devadmin" +PROD_HOST="10.99.0.3" +PROD_DB="scoreodonto" +PROD_USER="scoreodonto_user" + +# Ordem importa (FK): pais antes dos filhos. +# usuarios, clinicas → vinculos, clinicas_horarios, pessoa_comunicacao, feriados +TABLES=(usuarios clinicas feriados vinculos clinicas_horarios pessoa_comunicacao) +declare -A PK=( + [usuarios]="id" [clinicas]="id" [vinculos]="id" + [clinicas_horarios]="id" [feriados]="id" + [pessoa_comunicacao]="usuario_id,clinica_id" +) + +: "${SCOREO_DB_PASS:=$(ssh -o BatchMode=yes deploy@10.99.0.1 'grep -oP "SCOREO_DB_PASS=\K.*" /home/deploy/stack/scoreodonto.com/.env' 2>/dev/null | tr -d '\r\n')}" +[ -n "$SCOREO_DB_PASS" ] || { echo "ERRO: SCOREO_DB_PASS não definida"; exit 1; } + +dev_psql() { docker exec -i "$DEV_CT" psql -U "$DEV_USER" -d "$DEV_DB" "$@"; } +prod_psql() { docker run --rm -i --network host -e PGPASSWORD="$SCOREO_DB_PASS" \ + postgis/postgis:18-3.6 psql -h "$PROD_HOST" -U "$PROD_USER" -d "$PROD_DB" "$@"; } + +# Monta o SQL de upsert de uma tabela: cria staging, carrega dados de dev via COPY, +# faz INSERT ... ON CONFLICT DO UPDATE, e reporta quantas linhas entraram/mudaram. +emit_table_sql() { + local t="$1" pk="${PK[$t]}" + local cols; cols=$(dev_psql -tAc "select string_agg(quote_ident(column_name),',' order by ordinal_position) from information_schema.columns where table_name='$t' and table_schema='public'") + local setclause; setclause=$(dev_psql -tAc "select string_agg(quote_ident(column_name)||'=EXCLUDED.'||quote_ident(column_name),',' order by ordinal_position) from information_schema.columns where table_name='$t' and table_schema='public' and column_name <> all(string_to_array('$pk',','))") + + echo "CREATE TEMP TABLE stg_$t (LIKE public.$t INCLUDING DEFAULTS) ON COMMIT DROP;" + echo "\\copy stg_$t ($cols) FROM STDIN" + dev_psql -c "\\copy (SELECT $cols FROM public.$t) TO STDOUT" + echo "\\." + echo "WITH ins AS (" + echo " INSERT INTO public.$t ($cols) SELECT $cols FROM stg_$t" + echo " ON CONFLICT ($pk) DO UPDATE SET $setclause" + echo " RETURNING xmax=0 AS inserted" + echo ") SELECT '$t' AS tabela, count(*) FILTER (WHERE inserted) AS inseridos, count(*) FILTER (WHERE NOT inserted) AS atualizados FROM ins;" +} + +build_sql() { + # A ordem de TABLES respeita as FKs (pais antes de filhos), então não é preciso + # desligar triggers/FK (o que exigiria superuser). Tudo numa transação: erro = rollback. + echo "BEGIN;" + for t in "${TABLES[@]}"; do emit_table_sql "$t"; done + if [ "$APPLY" = "--apply" ]; then echo "COMMIT;"; else echo "ROLLBACK;"; fi +} + +if [ "$APPLY" = "--apply" ]; then + TS=$(date +%Y%m%d_%H%M) + echo "→ Backup do prod (tabelas do escopo) antes de aplicar..." + prod_psql -c "\\copy (select 1) to /dev/null" >/dev/null # valida conexão + BK="/home/deploy/backups/scoreodonto_config_pre-sync_${TS}.sql" + ssh -o BatchMode=yes deploy@10.99.0.1 "PGPASSWORD='$SCOREO_DB_PASS' pg_dump -h $PROD_HOST -U $PROD_USER -d $PROD_DB --data-only $(printf ' -t public.%s' "${TABLES[@]}") | gzip > ${BK}.gz && echo ' backup: ${BK}.gz'" 2>/dev/null \ + || echo " (aviso: backup remoto falhou — verifique antes de prosseguir)" + echo "→ Aplicando (COMMIT)..." +else + echo "→ DRY-RUN (nada será gravado; ao final: ROLLBACK)" +fi + +build_sql | prod_psql -v ON_ERROR_STOP=1