brackt/scripts/fix-group-match-times-utc.mjs
Claude 746bbcf531 Migrate existing group-match times and localize day headers
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
2026-06-18 02:19:31 +00:00

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);
}