panels-origin/db/RUNBOOK-corte.md
Cursor Agent d36c287a82
db/infra: ETL de datos, backups y limpieza de despliegue (fase 5-7)
Fase 5 (migracion de datos):
- api/scripts/migrate-sqlite-to-postgres.ts: ETL unico SQLite -> Postgres.
  Pre-flight (CURP/RFC duplicados case-insensitive, FKs huerfanas,
  tenant_id invalido, fechas mal formateadas) -> carga por base/esquema
  con credenciales _owner (setval de secuencias, OVERRIDING SYSTEM VALUE,
  ON CONFLICT DO NOTHING idempotente) -> verificacion de conteos y sumas
  de dinero con tolerancia. Probado end-to-end contra un dataset SQLite
  legacy sintetico y un Postgres limpio (solo schema+catalogos, sin
  contexto dev): 100% de las filas migradas, sumas de dinero exactas,
  snapshot de uploaded_by/created_by resuelto correctamente.
- db/RUNBOOK-corte.md: checklist go/no-go para el corte por ambiente.
- Fix de bug real encontrado al probar: sin --context-filter explicito,
  Liquibase corre TAMBIEN los changesets context=dev (comportamiento por
  defecto, no "modo seguro") -- documentado en db/README.md con el
  ejemplo correcto (--context-filter='!dev' para staging/produccion).

Fase 5b: patron de compensacion para createTenant ya resuelto en el
rewrite de saas.ts (fase 3/4).

Fase 6 (backups): db/backups/ con plantillas pgBackRest por base,
politica de retencion, nota de persistencia minima de Redis (RDB, sin
retencion de negocio), checklist de simulacro de restauracion mensual,
y la validacion pendiente de si Coolify permite WAL archiving antes de
comprometerse a PITR real.

Fase 7 (limpieza y CI):
- Dockerfile.api simplificado (sin JRE/Liquibase/FFI). Dockerfile.migrate
  y Dockerfile.provision nuevos, de un solo uso, para el paso explicito
  de deploy (nunca sirven trafico).
- docker-compose.yml: postgres+redis+provision+migrate para dev local
  end-to-end; api ya no arranca hasta que migrate termina bien.
- docs/coolify.md y .env.example actualizados al modelo de 2 bases +
  Redis + Contabo.
- api/schema.sql eliminado (desincronizado, competia con Liquibase como
  fuente de verdad).
- .github/workflows/ci.yml: Postgres+Redis de servicio, aprovisiona
  roles/ACLs, dry-run de Liquibase (updateSQL) antes de aplicar,
  verify-isolation.sh, deno check + deno test, build de los dos frontends.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-02 21:05:07 +00:00

89 lines
3.8 KiB
Markdown

# Runbook de corte SQLite → Postgres (Fase 5)
Checklist operativo para migrar UN ambiente (dev, luego staging, luego
producción -- nunca saltar directo a producción). Usa
[`api/scripts/migrate-sqlite-to-postgres.ts`](../api/scripts/migrate-sqlite-to-postgres.ts).
## 0. Antes de empezar
- [ ] Postgres del ambiente aprovisionado (`db/provision/`) y migrado
**sin** datos de demo: `./db/update.sh all --context-filter='!dev'`.
- [ ] Backup fresco del volumen SQLite actual (`data/app.db`, `data/platform.db`)
guardado aparte, fuera del volumen que se va a apagar.
- [ ] Credenciales `_owner` de los 3 módulos disponibles en el entorno
(`DATABASE_URL_PLATFORM_OWNER`, `DATABASE_URL_IAM_OWNER`, `DATABASE_URL_CORE_OWNER`).
## 1. Pre-flight (sin ventana de mantenimiento, se puede correr en caliente)
```bash
cd api
deno run --allow-ffi --allow-net --allow-read --allow-write --allow-env \
scripts/migrate-sqlite-to-postgres.ts --dry-run \
--app-db=../data/app.db --platform-db=../data/platform.db
```
Si el pre-flight reporta `[FAIL]`, **no continuar** -- corregir los datos
en SQLite (duplicados, huérfanas, tenant_id inválido, fechas mal
formateadas) y repetir hasta que todo salga `[OK]`.
## 2. Ventana de mantenimiento (corte real)
Hoy no existe un "modo mantenimiento" en la app. Opciones, de menor a
mayor invasividad:
- Parar el contenedor/proceso `api` (nadie puede escribir mientras está
abajo -- los fronts mostrarán error de conexión).
- Responder 503 temporal en nginx para `/v1/*` mientras se corre el ETL.
Elegir una, documentar la hora exacta de inicio.
## 3. Migración
```bash
cd api
set -a && source /ruta/al/.env.del.ambiente && set +a
deno run --allow-ffi --allow-net --allow-read --allow-write --allow-env \
scripts/migrate-sqlite-to-postgres.ts \
--app-db=/ruta/a/app.db --platform-db=/ruta/a/platform.db
```
El script hace: pre-flight → carga (una transacción por base/esquema,
todo o nada) → `setval()` de secuencias → verificación de conteos y sumas
de dinero. Si CUALQUIER paso falla, no queda un estado a medias en
Postgres (la transacción de esa base se revierte completa), pero SQLite
sigue siendo la fuente de verdad -- no se ha cortado nada todavía.
## 4. Criterios go/no-go
Antes de apuntar la app a Postgres y apagar SQLite:
- [ ] El script terminó con `Migración completa y verificada.` (exit code 0).
- [ ] Conteos de filas origen=destino en **todas** las tablas listadas
(no solo las 9 de ejemplo del script -- ampliar `verifyCounts()` si
el ambiente tiene datos en tablas no cubiertas ahí).
- [ ] Diferencia de sumas de dinero dentro de la tolerancia ($0.05) -- si
no cuadra, decidir explícitamente si se acepta el redondeo
REAL→NUMERIC o se investiga antes de continuar.
- [ ] Spot-check manual de 2-3 registros conocidos (un trabajador, un
preupuesto, un préstamo) comparando app vieja vs. Postgres.
Si algo no cuadra: **no cortar**. Volver a levantar la app contra SQLite
(no se tocó), investigar, y repetir desde el paso 1 en otro intento.
## 5. Cutover
- [ ] Actualizar `DATABASE_URL_*`/`REDIS_URL_*` del servicio `api` a los
valores del ambiente Postgres/Redis recién migrado.
- [ ] Levantar `api` -- el fail-fast de arranque (`/v1/health`) debe
responder `{"ok":true,...}` antes de reabrir tráfico.
- [ ] Reabrir tráfico (quitar el 503/levantar el contenedor).
- [ ] Login de prueba con un usuario real del ambiente.
## 6. Después del corte
- [ ] Conservar el volumen SQLite (`data/`) como respaldo frío por un
período de retención definido (ej. 30 días) antes de borrarlo.
- [ ] Correr `./db/provision/verify-isolation.sh` contra el ambiente para
confirmar que el aislamiento de roles/RLS/ACLs sigue intacto.
- [ ] Repetir todo el runbook en el siguiente ambiente (dev → staging →
producción), nunca en paralelo.