Add a one-off data migration (scripts/fix-group-match-times-utc.mjs) that shifts group_stage_matches.scheduled_at +7h, converting the pre-fix local-wall-time-as-UTC (all entered in PDT) into true UTC. Scoped to group-stage matches only; playoff games were already stored correctly. Defaults to a dry-run preview and only writes with --apply. Run: npx dotenv -- node scripts/fix-group-match-times-utc.mjs [--apply] Fix GroupStageStandings day-header grouping/labels, which keyed and formatted by UTC date. With times now stored as true UTC, an evening PDT match rolled into the next UTC day and appeared under the wrong header. Group and label by the viewer's local date after hydration, keeping the UTC representation on SSR/first paint for deterministic markup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0125nZohVD2Cpoq3Q9f4jeds
78 lines
2.5 KiB
JavaScript
78 lines
2.5 KiB
JavaScript
/**
|
|
* One-off data migration: fix group-stage match times stored as local-wall-time-as-UTC.
|
|
*
|
|
* Before the schedule-form fix, the admin's local wall-clock time (PDT, UTC-7) was
|
|
* stored as if it were UTC, so every `group_stage_matches.scheduled_at` is 7 hours
|
|
* behind the true UTC instant. This shifts them +7 hours.
|
|
*
|
|
* Scope: ONLY `group_stage_matches.scheduled_at` (where not null). Playoff games
|
|
* (`playoff_match_games.scheduled_at`) were saved correctly and are left untouched.
|
|
*
|
|
* NOT idempotent — running with --apply twice double-shifts. Defaults to a dry-run;
|
|
* pass --apply to write.
|
|
*
|
|
* Usage (loads .env via dotenv-cli):
|
|
* npx dotenv -- node scripts/fix-group-match-times-utc.mjs # preview
|
|
* npx dotenv -- node scripts/fix-group-match-times-utc.mjs --apply # apply
|
|
*/
|
|
|
|
import postgres from "postgres";
|
|
|
|
const SHIFT = "7 hours"; // PDT (UTC-7) wall-clock stored as UTC -> true UTC
|
|
const apply = process.argv.includes("--apply");
|
|
|
|
const DATABASE_URL = process.env.DATABASE_DIRECT_URL || process.env.DATABASE_URL;
|
|
if (!DATABASE_URL) {
|
|
console.error("ERROR: DATABASE_DIRECT_URL (or DATABASE_URL) is required");
|
|
process.exit(1);
|
|
}
|
|
|
|
const sql = postgres(DATABASE_URL, { max: 1, onnotice: () => {} });
|
|
|
|
try {
|
|
const [{ count }] = await sql`
|
|
SELECT COUNT(*)::int AS count
|
|
FROM group_stage_matches
|
|
WHERE scheduled_at IS NOT NULL
|
|
`;
|
|
console.log(`Group-stage matches with a scheduled time: ${count}`);
|
|
|
|
if (count === 0) {
|
|
console.log("Nothing to do.");
|
|
await sql.end();
|
|
process.exit(0);
|
|
}
|
|
|
|
const samples = await sql`
|
|
SELECT id,
|
|
scheduled_at AS before,
|
|
scheduled_at + ${sql.unsafe(`interval '${SHIFT}'`)} AS after
|
|
FROM group_stage_matches
|
|
WHERE scheduled_at IS NOT NULL
|
|
ORDER BY scheduled_at
|
|
LIMIT 10
|
|
`;
|
|
console.log(`\nSample before -> after (+${SHIFT}):`);
|
|
for (const r of samples) {
|
|
console.log(` ${r.id}: ${r.before.toISOString()} -> ${r.after.toISOString()}`);
|
|
}
|
|
|
|
if (!apply) {
|
|
console.log(`\nDRY RUN — no changes written. Re-run with --apply to shift ${count} row(s) +${SHIFT}.`);
|
|
await sql.end();
|
|
process.exit(0);
|
|
}
|
|
|
|
const result = await sql`
|
|
UPDATE group_stage_matches
|
|
SET scheduled_at = scheduled_at + ${sql.unsafe(`interval '${SHIFT}'`)}
|
|
WHERE scheduled_at IS NOT NULL
|
|
`;
|
|
console.log(`\nApplied: shifted ${result.count} row(s) +${SHIFT}.`);
|
|
await sql.end();
|
|
process.exit(0);
|
|
} catch (err) {
|
|
console.error("Migration failed:", err);
|
|
await sql.end().catch(() => {});
|
|
process.exit(1);
|
|
}
|