brackt/app/routes/sports-seasons.$sportsSeasonId.tournament.tsx
Claude ac33e9e223
Some checks failed
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m50s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Failing after 51s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
fix: address 10 code review issues in match sync system
- 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
2026-06-12 21:53:06 +00:00

138 lines
4.9 KiB
TypeScript

import { useEffect } from "react";
import { useRevalidator } from "react-router";
import { eq, inArray } from "drizzle-orm";
import type { Route } from "./+types/sports-seasons.$sportsSeasonId.tournament";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { type PlayoffMatch } from "~/models/playoff-match";
import { findSeasonMatchesBySportsSeasonId } from "~/models/season-match";
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
import { Cs2TournamentBracket } from "~/components/scoring/Cs2TournamentBracket";
import { MatchSchedule } from "~/components/scoring/MatchSchedule";
import { ArrowLeft } from "lucide-react";
import { Link } from "react-router";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `${data?.sportsSeason?.name ?? "Tournament"} — Schedule & Results` }];
}
export async function loader({ params }: Route.LoaderArgs) {
const { sportsSeasonId } = params;
const db = database();
const sportsSeason = await db.query.sportsSeasons.findFirst({
where: eq(schema.sportsSeasons.id, sportsSeasonId),
with: { sport: true },
});
if (!sportsSeason) throw new Response("Sports season not found", { status: 404 });
const simulatorType = sportsSeason.sport?.simulatorType ?? null;
const seasonMatches = await findSeasonMatchesBySportsSeasonId(sportsSeasonId);
// Load playoff matches for bracket sports
let playoffMatches: (PlayoffMatch & { participant1: { id: string; name: string } | null; participant2: { id: string; name: string } | null; winner: { id: string; name: string } | null; loser: { id: string; name: string } | null; })[] = [];
let playoffRounds: string[] = [];
let bracketTemplateId: string | null = null;
if (simulatorType?.endsWith("_bracket") || simulatorType === "cs2_major_qualifying_points") {
const events = await db.query.scoringEvents.findMany({
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
});
if (events.length > 0) {
const eventIds = events.map((e) => e.id);
const matches = await db.query.playoffMatches.findMany({
where: (pm) => inArray(pm.scoringEventId, eventIds),
with: {
participant1: true,
participant2: true,
winner: true,
loser: true,
},
});
playoffMatches = matches as typeof playoffMatches;
const templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
bracketTemplateId = templateId ?? null;
const template = templateId ? getBracketTemplate(templateId) : undefined;
playoffRounds = getOrderedRoundsFromMatches(matches, template);
}
}
const hasLiveMatches =
seasonMatches.some((m) => m.status === "in_progress") ||
playoffMatches.some((m) => !m.isComplete && (m.participant1Id != null || m.participant2Id != null));
return {
sportsSeason,
simulatorType,
seasonMatches,
playoffMatches,
playoffRounds,
bracketTemplateId,
hasLiveMatches,
};
}
type LoaderData = Awaited<ReturnType<typeof loader>>;
export default function TournamentPage({ loaderData }: Route.ComponentProps) {
const {
sportsSeason,
simulatorType,
seasonMatches,
playoffMatches,
playoffRounds,
bracketTemplateId,
hasLiveMatches,
} = loaderData;
const { revalidate } = useRevalidator();
// Poll every 30 seconds when any match is live
useEffect(() => {
if (!hasLiveMatches) return;
const interval = setInterval(() => revalidate(), 30_000);
return () => clearInterval(interval);
}, [hasLiveMatches, revalidate]);
const isCs2 = simulatorType === "cs2_major_qualifying_points";
return (
<div className="container mx-auto px-4 py-8 max-w-6xl">
<div className="mb-6">
<Link
to="/"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-4"
>
<ArrowLeft className="h-4 w-4" />
Back
</Link>
<h1 className="text-3xl font-bold">{sportsSeason.name}</h1>
{sportsSeason.sport && (
<p className="text-muted-foreground mt-1">{sportsSeason.sport.name}</p>
)}
{hasLiveMatches && (
<div className="mt-2 flex items-center gap-2 text-sm text-amber-400">
<span className="inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse" />
Live updating every 30 seconds
</div>
)}
</div>
{isCs2 ? (
<Cs2TournamentBracket
swissMatches={seasonMatches as Parameters<typeof Cs2TournamentBracket>[0]["swissMatches"]}
playoffMatches={playoffMatches as Parameters<typeof Cs2TournamentBracket>[0]["playoffMatches"]}
playoffRounds={playoffRounds}
bracketTemplateId={bracketTemplateId}
/>
) : (
<MatchSchedule matches={seasonMatches as Parameters<typeof MatchSchedule>[0]["matches"]} />
)}
</div>
);
}