Compare commits

...

1 Commits

Author SHA1 Message Date
VPS 4 Builder 29d7870811 feat(ops): sync de config/contas dev→prod por upsert seguro
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 <noreply@anthropic.com>
2026-07-10 19:53:23 +02:00
+86
View File
@@ -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