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
The admin group-stage schedule form submitted the raw datetime-local
string, which the server parsed with new Date() in the server's timezone
(UTC), so an admin's local wall-clock time was stored as if it were UTC.
Downstream displays then correctly converted UTC->local, shifting times
by the admin's UTC offset (e.g. 7 PM PDT showed as 12 PM).
Mirror the existing playoff "add-game" form: convert local->UTC on the
client via localDateTimeToUtcIso into a hidden field on submit, and
render the stored UTC value back into the input in the browser's local
timezone via a client-only effect (avoids SSR hydration mismatch).
Adds utcIsoToLocalDateTime helper (inverse of localDateTimeToUtcIso)
with unit tests covering nullish/invalid input and a tz-independent
round-trip.
Note: matches saved before this fix must be re-saved once in admin to
correct their stored values.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125nZohVD2Cpoq3Q9f4jeds
- getUpcomingScoringEvents: fall back to eventStartsAt.toISOString() when no
game-level scheduledAt exists, matching the same pattern already used in
getUpcomingEventsForDraftedParticipants — prevents F1/golf events from
showing a date with no time in SportSeasonCard and similar consumers
- EventSchedule: replace bare toLocaleTimeString call with an isNaN-guarded
block so a malformed timestamp renders nothing instead of "Invalid Date"
- database/schema.ts: add index on tournament_groups(scoring_event_id) to
support the inArray filter added in the previous commit
(run npm run db:generate && npm run db:migrate to apply)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiVGo8gSBXe3WuRniVNKsd
The previous fix only queried playoffMatchGames.scheduledAt for bracket
events. FIFA World Cup group stage matches live in groupStageMatches
(linked via tournamentGroups), not playoffMatchGames. Now
getUpcomingScoringEvents fetches both tables in parallel and uses
the earliest scheduledAt from either source as earliestGameTime,
matching what UpcomingCalendarPanel already shows on the Upcoming Events page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiVGo8gSBXe3WuRniVNKsd
getUpcomingScoringEvents already fetched per-game scheduledAt times from
playoffMatchGames for sorting, but discarded them from the return value.
EventSchedule was showing eventStartsAt (event-level) instead of the
accurate per-game times. Now getUpcomingScoringEvents includes earliestGameTime
in each returned event, and EventSchedule prefers earliestGameTime over
eventStartsAt — matching how UpcomingCalendarPanel works on the Upcoming Events page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiVGo8gSBXe3WuRniVNKsd