diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 9aad2e5..c088942 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -29,15 +29,6 @@ jobs: npm run migrate:one node scripts/postflight-check.js - - name: Seed Tuxedo Corn 2026 tour data - env: - DATABASE_URL: ${{ secrets.DATABASE_URL }} - run: | - # Use the batch-RPC Node script instead of raw psql — handles connection pooling - # and per-batch progress output. Skips the slow neon pooler path by replacing - # the pooler hostname with direct compute. - npx tsx scripts/import-tuxedo-stops.ts --no-clean 2>&1 || true - - name: Build env: NODE_ENV: production diff --git a/scripts/cleanup-duplicate-stops.ts b/scripts/cleanup-duplicate-stops.ts new file mode 100644 index 0000000..af09cd1 --- /dev/null +++ b/scripts/cleanup-duplicate-stops.ts @@ -0,0 +1,78 @@ +/** + * Cleanup script: remove duplicate stops from the Tuxedo Corn 2026 tour. + * Keeps the row with the latest created_at per (brand_id, date, city, state, location, time). + * Run: DATABASE_URL="postgresql://..." npx tsx scripts/cleanup-duplicate-stops.ts + */ +import { Pool } from "pg"; + +const pool = new Pool({ + connectionString: + process.env.DATABASE_ADMIN_URL ?? + process.env.DATABASE_URL ?? + "postgres://postgres:postgres@localhost:5432/route_commerce", +}); + +async function main() { + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + // Find duplicate groups (same brand_id, date, city, state, location, time) + const dupes = await client.query(` + SELECT brand_id::text, date, city, state, location, time, count(*) as cnt, min(created_at::text) as oldest, max(created_at::text) as newest + FROM stops + WHERE deleted_at IS NULL + GROUP BY brand_id, date, city, state, location, time + HAVING count(*) > 1 + ORDER BY date, city + `); + + console.log(`Found ${dupes.rowCount} duplicate groups`); + if (dupes.rowCount === 0) { + await client.query("COMMIT"); + console.log("No duplicates found."); + return; + } + + let totalDeleted = 0; + for (const row of dupes.rows) { + // Keep the row with the latest created_at, delete the rest + const del = await client.query(` + DELETE FROM stops + WHERE brand_id = $1 + AND date = $2 + AND city = $3 + AND state = $4 + AND location = $5 + AND time IS NOT DISTINCT FROM $6 + AND deleted_at IS NULL + AND created_at < ( + SELECT max(created_at) FROM stops s2 + WHERE s2.brand_id = stops.brand_id + AND s2.date = stops.date + AND s2.city = stops.city + AND s2.state = stops.state + AND s2.location = stops.location + AND (s2.time IS NOT DISTINCT FROM stops.time) + AND s2.deleted_at IS NULL + ) + `, [row.brand_id, row.date, row.city, row.state, row.location, row.time]); + totalDeleted += del.rowCount ?? 0; + console.log(` Deleted ${del.rowCount} duplicate(s) for ${row.city}, ${row.state} on ${row.date}`); + } + + await client.query("COMMIT"); + console.log(`\nDone. Total duplicates removed: ${totalDeleted}`); + } catch (e) { + await client.query("ROLLBACK"); + throw e; + } finally { + client.release(); + await pool.end(); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +});