#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PROD_DIR="${ROOT_DIR}" DEV_DIR="${DEV_DIR:-/tmp/mh-dashboard-organization-dev}" SCOPE="${1:-minimal}" if [[ ! -f "${PROD_DIR}/docker-compose.yml" ]]; then echo "Production workspace not found: ${PROD_DIR}" >&2 exit 1 fi if [[ ! -f "${DEV_DIR}/docker-compose.yml" ]]; then echo "Development workspace not found: ${DEV_DIR}" >&2 echo "Set DEV_DIR=/path/to/dev-copy if the dev workspace moved." >&2 exit 1 fi case "${SCOPE}" in minimal) TABLES=( member_aliases member_overrides member_retirements members seat_maps seat_slots ) ;; full) TABLES=( integration_import_batches integration_raw_organization_rows integration_raw_mh_rows integration_raw_mh_pm_rows integration_raw_payment_rows integration_project_aliases integration_project_category_mappings integration_project_pm_assignments integration_projects integration_work_logs integration_work_log_segments integration_vouchers member_aliases member_overrides member_retirements members seat_maps seat_slots ) ;; *) echo "Usage: $0 [minimal|full]" >&2 exit 1 ;; esac PROD_COMPOSE=(docker compose --project-directory "${PROD_DIR}") DEV_COMPOSE=(docker compose --project-directory "${DEV_DIR}") require_service() { local dir="$1" shift (cd "${dir}" && "$@") >/dev/null } echo "[1/6] Checking source and target stacks" require_service "${PROD_DIR}" "${PROD_COMPOSE[@]}" ps require_service "${DEV_DIR}" "${DEV_COMPOSE[@]}" ps echo "[2/6] Ensuring db containers are reachable" (cd "${PROD_DIR}" && "${PROD_COMPOSE[@]}" exec -T db pg_isready -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") >/dev/null (cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db pg_isready -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") >/dev/null WORK_DIR="$(mktemp -d)" cleanup() { rm -rf "${WORK_DIR}" } trap cleanup EXIT DUMP_FILE="${WORK_DIR}/prod_to_dev_${SCOPE}.sql" TRUNCATE_FILE="${WORK_DIR}/truncate_${SCOPE}.sql" SEAT_POSITIONS_FILE="${WORK_DIR}/seat_positions.csv" echo "[3/6] Building truncate script for ${SCOPE} scope" { echo "BEGIN;" echo "SET session_replication_role = replica;" printf 'TRUNCATE TABLE %s RESTART IDENTITY CASCADE;\n' "$(IFS=,; echo "${TABLES[*]}")" echo "SET session_replication_role = DEFAULT;" echo "COMMIT;" } > "${TRUNCATE_FILE}" echo "[4/6] Dumping ${SCOPE} data from 8080 source DB" TABLE_ARGS=() for table in "${TABLES[@]}"; do TABLE_ARGS+=(-t "public.${table}") done (cd "${PROD_DIR}" && "${PROD_COMPOSE[@]}" exec -T db \ pg_dump -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \ --data-only --column-inserts --disable-triggers --no-owner --no-privileges \ "${TABLE_ARGS[@]}") > "${DUMP_FILE}" echo "[4.5/6] Exporting seat_positions in portable format" (cd "${PROD_DIR}" && "${PROD_COMPOSE[@]}" exec -T db \ psql -At -F ',' -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \ -c "COPY ( SELECT member_id, seat_map_id, seat_slot_id, row_index, col_index, seat_label, updated_at FROM public.seat_positions ORDER BY member_id ) TO STDOUT WITH CSV") > "${SEAT_POSITIONS_FILE}" echo "[5/6] Truncating target tables in 8081 dev DB" (cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \ psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") < "${TRUNCATE_FILE}" echo "[6/6] Restoring dumped data into 8081 dev DB" (cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \ psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}") < "${DUMP_FILE}" echo "[6.5/6] Restoring portable seat_positions and rebuilding auth users" (cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \ psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \ -c "DELETE FROM public.seat_positions") (cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T db \ psql -q -v ON_ERROR_STOP=1 -U "${POSTGRES_USER:-orgapp}" -d "${POSTGRES_DB:-orgdb}" \ -c "COPY public.seat_positions (member_id, seat_map_id, seat_slot_id, row_index, col_index, seat_label, updated_at) FROM STDIN WITH CSV") < "${SEAT_POSITIONS_FILE}" (cd "${DEV_DIR}" && "${DEV_COMPOSE[@]}" exec -T backend python - <<'PY' from backend.app.main import get_conn, sync_auth_users_from_members with get_conn() as conn: with conn.cursor() as cur: cur.execute("UPDATE members SET seat_label = ''") cur.execute( """ UPDATE members AS m SET seat_label = sp.seat_label FROM seat_positions AS sp WHERE sp.member_id = m.id """ ) sync_auth_users_from_members(cur) conn.commit() print("members, seat labels, and auth users synced") PY ) echo echo "Sync complete." echo "Source: ${PROD_DIR} (8080)" echo "Target: ${DEV_DIR} (8081)" echo "Scope : ${SCOPE}"