2 Commits

Author SHA1 Message Date
Tyler eabc709076 Fix admin stops page: replace mock supabase with direct pool query
Deploy to route.crispygoat.com / deploy (push) Successful in 4m2s
The page was importing from @/lib/supabase which is a mock client that
returns empty results. Replaced with pool.query() against the real
Postgres stops table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:17:39 -06:00
Tyler 4bd2ed0db2 Remove Tuxedo Corn tour seed step from deploy workflow
The seed step on every deploy was inserting duplicate stops each run.
The cleanup script (scripts/cleanup-duplicate-stops.ts) can be used to
dedupe existing rows when needed — run manually via:
  DATABASE_URL="..." npx tsx scripts/cleanup-duplicate-stops.ts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:17:34 -06:00
3 changed files with 129 additions and 55 deletions
-9
View File
@@ -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
+78
View File
@@ -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);
});
+50 -45
View File
@@ -1,27 +1,11 @@
import StopsDashboardClient from "@/components/admin/stops/StopsDashboardClient";
import StopsHeaderActions from "@/components/admin/StopsHeaderActions";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
import { PageHeader } from "@/components/admin/design-system";
import { redirect } from "next/navigation";
interface Stop {
id: string;
city: string;
state: string;
date: string;
time: string;
location: string;
active: boolean;
deleted_at: string | null;
brand_id: string;
address: string | null;
zip: string | null;
cutoff_time: string | null;
status: string;
brands: { name: string } | { name: string }[];
}
import { pool } from "@/lib/db";
import { type Stop } from "@/components/admin/stops/types";
const StopIcon = () => (
<svg className="h-5 w-5 sm:h-6 sm:w-6 text-current" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@@ -39,35 +23,56 @@ export default async function AdminStopsPage() {
redirect("/admin/pickup");
}
let query = supabase
.from("stops")
.select(`
id,
city,
state,
date,
time,
location,
active,
deleted_at,
brand_id,
address,
zip,
cutoff_time,
status,
brands (
name
)
`)
.is("deleted_at", null)
.order("date", { ascending: true });
let stops: Stop[] = [];
let error: { message: string } | null = null;
if (adminUser?.brand_id) {
query = query.eq("brand_id", adminUser.brand_id);
try {
const brandFilter = adminUser.brand_id
? `AND brand_id = '${adminUser.brand_id}'`
: "";
const { rows } = await pool.query<Stop& { brand_name: string }>(`
SELECT
s.id,
s.city,
s.state,
s.date,
s.time,
s.location,
s.status,
s.deleted_at,
s.brand_id,
s.address,
s.zip,
s.cutoff_time,
b.name as brand_name
FROM stops s
LEFT JOIN brands b ON b.id = s.brand_id
WHERE s.deleted_at IS NULL
${brandFilter}
ORDER BY s.date ASC
`);
stops = rows.map((r) => ({
id: r.id,
city: r.city ?? "",
state: r.state ?? "",
date: r.date ? String(r.date) : "",
time: r.time ?? "",
location: r.location ?? "",
active: r.status === "active",
deleted_at: r.deleted_at,
brand_id: r.brand_id,
address: r.address,
zip: r.zip,
cutoff_time: r.cutoff_time,
status: r.status ?? "active",
brands: r.brand_name ? { name: r.brand_name } : { name: "" },
}));
} catch (e) {
error = { message: e instanceof Error ? e.message : String(e) };
}
const { data: stops, error } = await query as unknown as { data: Stop[] | null; error: { message: string } | null };
if (error) {
return (
<main className="min-h-screen bg-[var(--admin-bg)]">
@@ -107,7 +112,7 @@ export default async function AdminStopsPage() {
{/* Content */}
<div className="px-4 sm:px-6 md:px-8 py-4 sm:py-6">
<StopsDashboardClient stops={stops ?? []} />
<StopsDashboardClient stops={stops} />
</div>
</main>
);