From 2984185e6b65ceb1e89bbbaf5e7d0f8818ad7557 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Wed, 9 Sep 2026 11:47:53 -0500 Subject: [PATCH] docs(homelab): pricing migration order fix and cutover checklist Document homelab Neon teardown steps, add fix-pricing-pgmigration-order script, and refresh migrate-neon convoy status for Dokploy. Co-authored-by: Cursor --- .convoys/migrate-neon-to-homelab.md | 6 +-- docs/HOMELAB_DATABASE.md | 20 +++++++- package.json | 1 + scripts/fix-pricing-pgmigration-order.js | 61 ++++++++++++++++++++++++ scripts/migrate-neon-to-homelab.js | 2 +- 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 scripts/fix-pricing-pgmigration-order.js diff --git a/.convoys/migrate-neon-to-homelab.md b/.convoys/migrate-neon-to-homelab.md index 159ea05..448bb6b 100644 --- a/.convoys/migrate-neon-to-homelab.md +++ b/.convoys/migrate-neon-to-homelab.md @@ -46,9 +46,9 @@ Vercel cannot connect to `192.168.68.102`. Production cutover requires | 3 | `docs/HOMELAB_DATABASE.md` runbook | code | done | | 4 | Operator: provision DB, `npm run migrate up` | operator | done | | 5 | MinIO + Redis wiring, Dockerfile, Dokploy docs | code | done | -| 6 | `npm run migrate-neon-to-homelab` data copy | operator | pending | -| 7 | Dokploy app + Traefik route | operator | in_progress | -| 8 | Decommission Neon + Vercel | operator | pending | +| 6 | `npm run migrate-neon-to-homelab` data copy | operator | **done** (homelab has prod data; ~69k cards) | +| 7 | Dokploy app + Traefik route | operator | **done** (`deckhearth.stillwell.cloud`; `POSTGRES_URL` → CT 102) | +| 8 | Decommission Neon + Vercel | operator | **pending** — safe after Dokploy soak + local `.env.local` switched | ## Env contract diff --git a/docs/HOMELAB_DATABASE.md b/docs/HOMELAB_DATABASE.md index b5813a5..bb5ec2e 100644 --- a/docs/HOMELAB_DATABASE.md +++ b/docs/HOMELAB_DATABASE.md @@ -116,6 +116,22 @@ Copies **data only** (schema from Phase 2 migrations). Harmless restore errors: After Dokploy prod is healthy for 24–48 h, pause/delete the Neon project and disable the Vercel deployment. +### Migration complete checklist (2026-09) + +Use this when Neon sends storage alerts but production already runs on homelab: + +| Step | Status (verify) | Action | +| --- | --- | --- | +| 1. Prod DB | Dokploy `POSTGRES_URL` → `192.168.68.102:5432/deckhearth` | CT 112 deckhearth container env | +| 2. Data on homelab | `SELECT count(*) FROM cards` ≈ 69k | CT 102 `psql` | +| 3. Pricing schema | `1783000000001_add-pricing-schema` in `pgmigrations` | Pricing convoy / PR #158 | +| 4. Migrate order fix | `npm run fix-pricing-pgmigration-order` then `npm run migrate up` | Mac on LAN with homelab `.env.local` | +| 5. Local dev | `.env.local` `POSTGRES_URL` + `POSTGRES_URL_DIRECT` → homelab (copy password from Dokploy) | Do not commit | +| 6. Dokploy cleanup | Remove env vars whose values contain `neon.tech` (`DATABASE_URL`, `NEON_*`, legacy `PGHOST*`) | Redeploy deckhearth | +| 7. Neon | Pause or delete project in Neon console | Frees storage; keep `NEON_DATABASE_URL` in `.env.local` until step 6 verified | + +Optional: Neon had **6 users** vs homelab **5** at last check — run `npm run migrate-neon-to-homelab` only if you need that missing account (uses `postgres:17` Docker if local `pg_dump` is old). + --- ## Code changes (this convoy) @@ -132,4 +148,6 @@ After Dokploy prod is healthy for 24–48 h, pause/delete the Neon project and d | `connection refused` to 192.168.68.102 | Mac not on LAN / VPN; CT 102 down | | `SSL required` against homelab | Fixed in `lib/sql.js` for private IPs | | `type "vector" does not exist` | Run `CREATE EXTENSION vector` in `deckhearth` DB | -| Vercel preview still on Neon | Retired — CI smoke targets homelab URL | +| `npm run migrate up` fails "preceding already run migration" on homelab | Pricing branch backfill: `npm run fix-pricing-pgmigration-order` then `npm run migrate up`. Or one-off: `npx node-pg-migrate … --check-order false up` | +| Neon still in Dokploy env but app uses homelab | Remove `DATABASE_URL`, `NEON_*`, `PGHOST*` pointing at `*.neon.tech`; keep only `POSTGRES_URL` + `POSTGRES_URL_DIRECT` → `192.168.68.102` | +| Mac `.env.local` still Neon | Copy homelab `POSTGRES_URL` from Dokploy CT 112; set `POSTGRES_URL_DIRECT` to match; keep `NEON_DATABASE_URL` until Neon deleted | diff --git a/package.json b/package.json index d6117fd..60792b8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "bulk-import-pokemon": "node --env-file=.env.local scripts/bulk-import-pokemon.js", "backfill-embeddings": "node --env-file=.env.local scripts/backfill-card-embeddings.js", "migrate-neon-to-homelab": "node --env-file=.env.local scripts/migrate-neon-to-homelab.js", + "fix-pricing-pgmigration-order": "node --env-file=.env.local scripts/fix-pricing-pgmigration-order.js", "import-tags": "node --env-file=.env.local scripts/import-scryfall-tags.js", "test": "vitest", "test:run": "vitest run", diff --git a/scripts/fix-pricing-pgmigration-order.js b/scripts/fix-pricing-pgmigration-order.js new file mode 100644 index 0000000..7a34b41 --- /dev/null +++ b/scripts/fix-pricing-pgmigration-order.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/** + * Backfill pgmigrations.run_on for 1783000000001 so `npm run migrate up` + * passes node-pg-migrate order check on homelab DBs that applied pricing + * DDL out of timestamp sequence (pricing convoy branch merged after frame migrations). + * + * Requires POSTGRES_URL_DIRECT (homelab deckhearth URL). + * + * Usage: + * node --env-file=.env.local scripts/fix-pricing-pgmigration-order.js + */ + +import postgres from 'postgres'; + +function requireEnv(name) { + const value = process.env[name]?.trim(); + if (!value) { + console.error(`❌ ${name} is required`); + process.exit(1); + } + return value; +} + +const MIGRATION_NAME = '1783000000001_add-pricing-schema'; +/** Between 1782000000001 (embeddings) and 1786757312884 (visibility index). */ +const RUN_ON = '2026-06-15 12:00:00+00'; + +async function main() { + const url = requireEnv('POSTGRES_URL_DIRECT'); + const isPrivateLan = /(?:^|@)(?:localhost|127\.0\.0\.1|192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.)/.test(url); + const sql = postgres(url, { ssl: isPrivateLan ? false : 'require', max: 1 }); + + try { + const existing = await sql` + SELECT name, run_on FROM pgmigrations WHERE name = ${MIGRATION_NAME} + `; + + if (existing.length === 0) { + console.error(`❌ No pgmigrations row for ${MIGRATION_NAME}. Run pricing migration SQL first.`); + process.exit(1); + } + + const before = existing[0].run_on; + await sql` + UPDATE pgmigrations + SET run_on = ${RUN_ON}::timestamptz + WHERE name = ${MIGRATION_NAME} + `; + + console.log(`✅ Updated ${MIGRATION_NAME}`); + console.log(` run_on: ${before} → ${RUN_ON}`); + console.log('Next: npm run migrate up'); + } finally { + await sql.end({ timeout: 5 }); + } +} + +main().catch((err) => { + console.error(err.message || err); + process.exit(1); +}); diff --git a/scripts/migrate-neon-to-homelab.js b/scripts/migrate-neon-to-homelab.js index 0130f65..72effbe 100644 --- a/scripts/migrate-neon-to-homelab.js +++ b/scripts/migrate-neon-to-homelab.js @@ -87,7 +87,7 @@ async function main() { run('psql', [targetUrl, '-c', 'SELECT pg_size_pretty(pg_database_size(current_database())) AS db_size;']); console.log('\n✅ Data copy finished.'); - console.log('Next: set POSTGRES_URL to the homelab URL and deploy (Coolify on CT 107).'); + console.log('Next: set POSTGRES_URL to the homelab URL and deploy (Dokploy on CT 112).'); } finally { rmSync(tempDir, { recursive: true, force: true }); }