From ac33e9e2232b65c4d84b19cbf01296a97068f552 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 21:53:06 +0000 Subject: [PATCH] fix: address 10 code review issues in match sync system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix 1 (CRITICAL): CS2 "Sync from API" button was always 401 because it POSTed to /admin/jobs/sync-matches which requires a cron secret header that browsers can't send. Added sync-matches intent to cs2-setup action. - Fix 2 (CRITICAL): loserAdvances was hardcoded false in match-sync/index.ts, breaking NBA Play-In scoring. Now calls doesLoserAdvance(). - Fix 3: ESPN score "0" was falsy → stored as null. Non-numeric strings like "F/OT" produced NaN. Added parseScore() helper. - Fix 4: ESPN limit=1000 silently truncated MLB/NBA playoff games. Added seasonType param to EspnScheduleAdapter; all *_bracket types pass 3 (postseason only). - Fix 5: Cron job was syncing completed seasons unnecessarily. Added active status filter. - Fix 6: hasLiveMatches triggered perpetual 30s polling for unseeded brackets. Added participant presence check. - Fix 7: autoCompleteRoundIfDone was duplicated between bracket.server.ts and scoring-calculator.ts. Removed inline copy, imported shared version. - Fix 8: MatchSchedule date grouping never ran for ESPN sports (no matchday field). Removed hasMatchdays gate, always call groupByMatchday with stable UTC date key. - Fix 9: Silent failure when bracket matches exist but no scoring events. Added warning log. - Fix 10: Added 3 tests — zero score preservation, NaN from non-numeric score, seasonType in URL. https://claude.ai/code/session_01WUUM7uWzFoSkGcZRhnEKG6 --- app/components/scoring/MatchSchedule.tsx | 42 +++++------- ...sons.$id.events.$eventId.bracket.server.ts | 34 +--------- ...-seasons.$id.events.$eventId.cs2-setup.tsx | 15 ++++- app/routes/admin/jobs.sync-matches.ts | 7 +- ...rts-seasons.$sportsSeasonId.tournament.tsx | 5 +- .../__tests__/espn-schedule.test.ts | 65 +++++++++++++++++++ app/services/match-sync/espn-schedule.ts | 21 ++++-- app/services/match-sync/index.ts | 20 ++++-- 8 files changed, 131 insertions(+), 78 deletions(-) diff --git a/app/components/scoring/MatchSchedule.tsx b/app/components/scoring/MatchSchedule.tsx index b6da163..ed6edf2 100644 --- a/app/components/scoring/MatchSchedule.tsx +++ b/app/components/scoring/MatchSchedule.tsx @@ -89,10 +89,14 @@ function MatchRow({ match }: { match: MatchWithRelations }) { ); } +// Groups by matchday number when available, or by UTC calendar date (YYYY-MM-DD) for date-based sports. +// UTC date keys ensure consistent grouping regardless of the viewer's timezone. function groupByMatchday(matches: MatchWithRelations[]) { const groups = new Map(); for (const m of matches) { - const key = m.matchday != null ? `Matchday ${m.matchday}` : formatDate(m.scheduledAt)?.split(",")[0] ?? "TBD"; + const key = m.matchday != null + ? `Matchday ${m.matchday}` + : m.scheduledAt?.toISOString().slice(0, 10) ?? "TBD"; if (!groups.has(key)) groups.set(key, []); groups.get(key)!.push(m); } @@ -108,35 +112,21 @@ export function MatchSchedule({ matches, title = "Schedule" }: MatchScheduleProp ); } - const hasMatchdays = matches.some((m) => m.matchday != null); - const grouped = hasMatchdays ? groupByMatchday(matches) : null; - - if (grouped) { - return ( -
- {title &&

{title}

} - {[...grouped.entries()].map(([label, group]) => ( -
-
- {label} -
- {group.map((m) => ( - - ))} -
- ))} -
- ); - } + const grouped = groupByMatchday(matches); return (
{title &&

{title}

} -
- {matches.map((m) => ( - - ))} -
+ {[...grouped.entries()].map(([label, group]) => ( +
+
+ {label} +
+ {group.map((m) => ( + + ))} +
+ ))}
); } diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index 7a10943..1dab669 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -27,6 +27,7 @@ import { processMatchResult, recalculateAffectedLeagues, recalculateStandings, + autoCompleteRoundIfDone, } from "~/models/scoring-calculator"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; import { getBracketTemplate, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; @@ -211,39 +212,6 @@ export async function action({ request, params }: Route.ActionArgs) { } } - /** - * Auto-complete a round when every match in it is marked complete. - * Updates scoringEvents.playoffRound and calls processPlayoffEvent so that - * round placements are recorded and probabilities are refreshed. - * This replaces the manual "Complete Round" button. - * - * skipRecalculate=true because the caller already sent a Discord notification - * scoped to the current batch of matches — we don't want a second one that - * would show all previously-completed matches in the event. - */ - async function autoCompleteRoundIfDone( - eventId: string, - round: string, - sportsSeasonId: string, - db: ReturnType - ): Promise { - const allMatches = await findPlayoffMatchesByEventId(eventId); - const roundMatches = allMatches.filter((m) => m.round === round); - if (roundMatches.length === 0) return; - const allDone = roundMatches.every((m) => m.isComplete); - if (!allDone) return; - - await db - .update(schema.scoringEvents) - .set({ playoffRound: round, updatedAt: new Date() }) - .where(eq(schema.scoringEvents.id, eventId)); - - // skipProbabilities=true: callers (set-winner / set-round-winners) already ran - // updateProbabilitiesAfterResult before reaching here, so re-running would be wasted. - await processPlayoffEvent(eventId, db, { skipRecalculate: true, skipProbabilities: true }); - logger.log(`[AutoComplete] Round "${round}" auto-completed for event ${eventId}`); - } - if (intent === "set-winner") { const matchId = formData.get("matchId"); const winnerId = formData.get("winnerId"); diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx index 4ba21d1..ed9e90b 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.cs2-setup.tsx @@ -11,6 +11,7 @@ import { clearCs2StageAssignments, } from '~/models/cs2-major-stage'; import { findSeasonMatchesByScoringEventId } from '~/models/season-match'; +import { syncMatches } from '~/services/match-sync'; import { Button } from '~/components/ui/button'; import { Card, @@ -100,6 +101,17 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: true, message: `Marked ${eliminations.length} eliminations.` }; } + if (intent === 'sync-matches') { + try { + const result = await syncMatches(params.id); + const total = result.swissCreated + result.swissUpdated; + const summary = `Synced: ${total} Swiss match(es), ${result.playoffUpdated} playoff match(es).`; + return { success: true, message: summary }; + } catch (err) { + return { success: false, message: err instanceof Error ? err.message : 'Sync failed.' }; + } + } + return { success: false, message: 'Unknown action.' }; } @@ -336,7 +348,8 @@ export default function AdminCs2Setup() { Swiss Rounds - + +