Appearance
Runbook
Todos los comandos se ejecutan desde
migrations/scripts/con el venv activado:bashcd migrations/scripts source .venv/bin/activate
Modos del servicio
text
--mode full Pasada completa (sin filtro de fecha). Para carga inicial.
--mode incremental Una sola pasada que solo trae cambios desde last_sync_at.
--mode loop Pasada incremental cada --interval segundos, en bucle.Default: incremental.
Carga inicial
bash
# Fase 0 — Bootstrap del tenant (companies + users)
python3 sync_service.py --resource bootstrap
# Carga inicial completa (todo full)
python3 sync_service.py --resource all --mode full
# Alternativa: por fases (mismo efecto, pero permite verificar entre pasos)
python3 sync_service.py --resource custom_values --mode full
python3 sync_service.py --resource tags --mode full
python3 sync_service.py --resource pipelines --mode full
python3 sync_service.py --resource forms --mode full
python3 sync_service.py --resource businesses --mode full # ← B2B → contacts corporate
python3 sync_service.py --resource contacts --mode full
python3 sync_service.py --resource opportunities --mode full
python3 sync_service.py --resource conversations --mode full
python3 sync_service.py --resource messages --mode full
python3 sync_service.py --resource notes --mode full
python3 sync_service.py --resource tasks --mode fullPendiente de aprobación (ver TODO):
calendars/appointments/ persistencia del catálogocustom_field_definitions.
Sync incremental
Una pasada (cron-style)
bash
# Pasada incremental de todo
python3 sync_service.py --resource all --mode incremental
# Solo un recurso
python3 sync_service.py --resource contacts --mode incrementalLoop continuo (servicio)
bash
# Cada 10 minutos (default)
python3 sync_service.py --resource all --mode loop
# Cada 5 minutos
python3 sync_service.py --resource all --mode loop --interval 300
# En background con logs a archivo
nohup python3 sync_service.py --resource all --mode loop \
> ../logs/sync-loop.log 2>&1 &Como cron job
cron
# /etc/crontab — cada 10 min
*/10 * * * * marcosismaelrodriguez949 cd /home/.../migrations/scripts && \
./.venv/bin/python3 sync_service.py --resource all --mode incremental \
>> ../logs/sync-cron.log 2>&1Como systemd service (loop)
/etc/systemd/system/crm-sync.service:
ini
[Unit]
Description=CRM GHL→imcrmdev sync service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/home/marcosismaelrodriguez949/crm/migrations/scripts
ExecStart=/home/marcosismaelrodriguez949/crm/migrations/scripts/.venv/bin/python3 \
sync_service.py --resource all --mode loop --interval 600
Restart=on-failure
RestartSec=30
StandardOutput=append:/home/marcosismaelrodriguez949/crm/migrations/logs/sync.log
StandardError=append:/home/marcosismaelrodriguez949/crm/migrations/logs/sync.log
[Install]
WantedBy=multi-user.targetbash
sudo systemctl daemon-reload
sudo systemctl enable --now crm-sync.service
sudo journalctl -u crm-sync.service -fResumir de un crash en full
Dentro de una pasada full, el script persiste el cursor de paginación en migrations/state/<resource>.json. Si muere por cualquier razón:
bash
# Solo vuelve a correr el mismo comando
python3 sync_service.py --resource contacts --mode fullLee el state, salta lo ya procesado, y continúa. No se duplican documentos porque el upsert es por (company_id, external_id).
Para forzar un re-inicio limpio (peligroso, borra el state):
bash
python3 sync_service.py --resource contacts --mode full --reset-stateModo dry-run
bash
python3 sync_service.py --resource contacts --mode full --dry-run --limit 5Lee 5 contactos de GHL, muestra el doc transformado pero no escribe nada.
Logging
Los logs se escriben a migrations/logs/:
migrate-YYYY-MM-DD.log— log general (INFO+) del procesodlq-<resource>.jsonl— Dead Letter Queue: docs que fallaron validación o E11000sync-loop.log/sync-cron.log— outputs de los modos en background
Monitoreo en vivo
bash
# Conteo en tiempo real durante la corrida
watch -n 5 'mongosh "$TARGET_MONGO_URI" --quiet --eval "
print(\"contacts_new: \" + db.contacts_new.countDocuments({}));
print(\"opportunities: \" + db.opportunities.countDocuments({}));
print(\"conversations: \" + db.conversations.countDocuments({}));
print(\"messages: \" + db.messages.countDocuments({}));
print(\"tasks: \" + db.tasks.countDocuments({}));
"'Verificar last_sync_at por recurso
bash
for f in ../state/*.json; do
printf "%-20s " "$(basename $f .json):"
python3 -c "import json; d=json.load(open('$f')); print(d.get('last_sync_at', '(nunca)'))"
doneCasos comunes
"Token does not have access to this location"
El PIT que estás usando no es el de Trebol o le falta scope. Revisar scripts/.env.
Errores 429 frecuentes
Bajar RATE_LIMIT_RPS en scripts/.env a 5 o 4 y reiniciar el servicio. El cursor está preservado.
E11000 duplicate key
Visible en contacts_new.email. El script ya maneja esto: el primer contacto gana, los demás van al DLQ con email omitido. Revisar migrations/logs/dlq-contacts-duplicate-email.jsonl.
Una pasada incremental no trae nada y debería
Posibles causas:
last_sync_atestá más adelantado que el cambio real (puede pasar si el reloj difiere).- El campo
dateUpdateden GHL no se actualizó (raro pero pasa con cambios de child entities como notes). - Solución de fuerza bruta: forzar una pasada
fulldel recurso afectado.
Generar reporte de conteos
bash
python3 sync_service.py --resource reportCutover a producción
Una vez validada la sincronización en el Mongo de pruebas (puerto 27019):
bash
# Estos comandos van desde la raíz de migrations/
# 1. Dump del Mongo migrado
docker exec crm-migration-mongo-1 mongodump \
--uri="mongodb://admin:migration_pwd_change_me@localhost:27017/imcrmdev?authSource=admin" \
--out=/tmp/dump
# 2. Copiar dump al host
docker cp crm-migration-mongo-1:/tmp/dump ./dump-trebol
# 3. Restore en Mongo de producción de Trebol
mongorestore --uri="mongodb://admin:PROD_PWD@trebol-mongo:27017/imcrmdev?authSource=admin" \
--drop ./dump-trebol/imcrmdev
# 4. Apuntar el sync service al Mongo de producción
# (cambiar TARGET_MONGO_URI en .env)
# y arrancar el servicio en modo loop alláEl
--dropborra cada colección antes de restaurar. Usar solo en una BD que aún no esté en uso por usuarios reales.