From f949338ed78ac1af645e3324b9779002c4bccf21 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Apr 2026 19:19:34 +0000 Subject: [PATCH] Consolidate Elo + ranking input into generic elo-ratings page The darts-elo and cs-elo pages were unreachable from the admin nav, which always links to the generic elo-ratings page. Extended elo-ratings to conditionally show world ranking fields for simulator types that need it (darts_bracket, cs2_major_qualifying_points), then deleted the redundant sport-specific pages. https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR --- app/routes.ts | 8 - .../admin.sports-seasons.$id.cs-elo.tsx | 457 ----------------- .../admin.sports-seasons.$id.darts-elo.tsx | 470 ------------------ .../admin.sports-seasons.$id.elo-ratings.tsx | 217 +++++--- 4 files changed, 154 insertions(+), 998 deletions(-) delete mode 100644 app/routes/admin.sports-seasons.$id.cs-elo.tsx delete mode 100644 app/routes/admin.sports-seasons.$id.darts-elo.tsx diff --git a/app/routes.ts b/app/routes.ts index 158fcd4..5d586d0 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -99,14 +99,6 @@ export default [ "sports-seasons/:id/surface-elo", "routes/admin.sports-seasons.$id.surface-elo.tsx" ), - route( - "sports-seasons/:id/darts-elo", - "routes/admin.sports-seasons.$id.darts-elo.tsx" - ), - route( - "sports-seasons/:id/cs-elo", - "routes/admin.sports-seasons.$id.cs-elo.tsx" - ), route( "sports-seasons/:id/golf-skills", "routes/admin.sports-seasons.$id.golf-skills.tsx" diff --git a/app/routes/admin.sports-seasons.$id.cs-elo.tsx b/app/routes/admin.sports-seasons.$id.cs-elo.tsx deleted file mode 100644 index 7b6a226..0000000 --- a/app/routes/admin.sports-seasons.$id.cs-elo.tsx +++ /dev/null @@ -1,457 +0,0 @@ -import { Form, redirect, useLoaderData, useActionData, useNavigation } from 'react-router'; -import type { Route } from './+types/admin.sports-seasons.$id.cs-elo'; - -import { logger } from '~/lib/logger'; -import { findSportsSeasonById, updateSportsSeason } from '~/models/sports-season'; -import { findParticipantsBySportsSeasonId } from '~/models/participant'; -import { - getAllParticipantEVsForSeason, - batchSaveSourceElos, - batchUpsertParticipantEVs, -} from '~/models/participant-expected-value'; -import { batchUpsertParticipantEvSnapshots } from '~/models/ev-snapshot'; -import { getSimulator, type SimulatorType } from '~/services/simulations/registry'; -import { calculateEV } from '~/services/ev-calculator'; -import { DEFAULT_SCORING_RULES } from '~/lib/scoring-types'; -import { recalculateStandings } from '~/models/scoring-calculator'; -import { database } from '~/database/context'; -import * as schema from '~/database/schema'; -import { eq } from 'drizzle-orm'; -import { Button } from '~/components/ui/button'; -import { Input } from '~/components/ui/input'; -import { Label } from '~/components/ui/label'; -import { Textarea } from '~/components/ui/textarea'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '~/components/ui/card'; -import { useState } from 'react'; -import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react'; -import { normalizeName } from '~/lib/fuzzy-match'; - -export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { - return [{ title: `CS2 Elo & HLTV Rankings — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }]; -} - -export async function loader({ params }: Route.LoaderArgs) { - const sportsSeasonId = params.id; - - const sportsSeason = await findSportsSeasonById(sportsSeasonId); - if (!sportsSeason) { - throw new Response('Sports season not found', { status: 404 }); - } - - const participants = await findParticipantsBySportsSeasonId(sportsSeasonId); - const existingEVs = await getAllParticipantEVsForSeason(sportsSeasonId); - - const existingData: Record = {}; - for (const ev of existingEVs) { - existingData[ev.participantId] = { - elo: ev.sourceElo ?? null, - ranking: ev.worldRanking ?? null, - }; - } - - return { sportsSeason, participants, existingData }; -} - -interface ActionData { - success?: boolean; - message?: string; -} - -export async function action({ request, params }: Route.ActionArgs) { - const sportsSeasonId = params.id; - const formData = await request.formData(); - - const sportsSeason = await findSportsSeasonById(sportsSeasonId); - if (!sportsSeason) { - return { success: false, message: 'Sports season not found' }; - } - - const participants = await findParticipantsBySportsSeasonId(sportsSeasonId); - - const eloInputs: Array<{ participantId: string; sportsSeasonId: string; sourceElo: number; worldRanking?: number | null }> = []; - for (const participant of participants) { - const eloVal = formData.get(`elo_${participant.id}`) as string; - const rankVal = formData.get(`rank_${participant.id}`) as string; - - if (eloVal && eloVal.trim() !== '') { - const elo = Number(eloVal); - if (!isNaN(elo) && elo > 0) { - const ranking = rankVal && rankVal.trim() !== '' ? Number(rankVal) : null; - eloInputs.push({ - participantId: participant.id, - sportsSeasonId, - sourceElo: elo, - worldRanking: ranking && !isNaN(ranking) && ranking > 0 ? ranking : null, - }); - } - } - } - - if (eloInputs.length === 0) { - return { success: false, message: 'Please enter an Elo rating for at least one team' }; - } - - if (!sportsSeason.sport?.simulatorType) { - return { success: false, message: 'This sport has no simulator type configured. Set one on the sport in the admin panel.' }; - } - - if (sportsSeason.simulationStatus === 'running') { - return { success: false, message: 'A simulation is already running. Please wait.' }; - } - - await batchSaveSourceElos(eloInputs); - - await updateSportsSeason(sportsSeasonId, { simulationStatus: 'running' }); - - try { - const simulator = getSimulator(sportsSeason.sport.simulatorType as SimulatorType); - const results = await simulator.simulate(sportsSeasonId); - - if (results.length === 0) { - throw new Error('Simulation returned no results.'); - } - - const simulatedIds = new Set(results.map(r => r.participantId)); - const ZERO_PROBS = { - probFirst: 0, probSecond: 0, probThird: 0, probFourth: 0, - probFifth: 0, probSixth: 0, probSeventh: 0, probEighth: 0, - }; - const evInputs = [ - ...results.map(r => ({ - participantId: r.participantId, - sportsSeasonId, - probabilities: r.probabilities, - scoringRules: DEFAULT_SCORING_RULES, - source: 'elo_simulation' as const, - })), - ...participants - .filter(p => !simulatedIds.has(p.id)) - .map(p => ({ - participantId: p.id, - sportsSeasonId, - probabilities: ZERO_PROBS, - scoringRules: DEFAULT_SCORING_RULES, - source: 'elo_simulation' as const, - })), - ]; - - await batchUpsertParticipantEVs(evInputs); - - const seasonSports = await database().query.seasonSports.findMany({ - where: eq(schema.seasonSports.sportsSeasonId, sportsSeasonId), - }); - await Promise.all(seasonSports.map(({ seasonId }) => recalculateStandings(seasonId))); - - const today = new Date().toISOString().slice(0, 10); - await batchUpsertParticipantEvSnapshots( - results.map(r => ({ - participantId: r.participantId, - sportsSeasonId, - snapshotDate: today, - probFirst: r.probabilities.probFirst, - probSecond: r.probabilities.probSecond, - probThird: r.probabilities.probThird, - probFourth: r.probabilities.probFourth, - probFifth: r.probabilities.probFifth, - probSixth: r.probabilities.probSixth, - probSeventh: r.probabilities.probSeventh, - probEighth: r.probabilities.probEighth, - calculatedEV: calculateEV(r.probabilities, DEFAULT_SCORING_RULES), - source: r.source, - })) - ); - - await updateSportsSeason(sportsSeasonId, { simulationStatus: 'idle' }); - } catch (error) { - await updateSportsSeason(sportsSeasonId, { simulationStatus: 'failed' }); - logger.error('Error running CS2 major simulation:', error); - return { - success: false, - message: error instanceof Error ? error.message : 'Simulation failed', - }; - } - - return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`); -} - -export default function AdminSportsSeasonCsElo() { - const { sportsSeason, participants, existingData } = useLoaderData(); - const actionData = useActionData(); - const navigation = useNavigation(); - - const [eloValues, setEloValues] = useState>(() => { - const initial: Record = {}; - participants.forEach(p => { - const d = existingData[p.id]; - if (d?.elo !== null && d?.elo !== undefined) initial[p.id] = d.elo.toString(); - }); - return initial; - }); - - const [rankValues, setRankValues] = useState>(() => { - const initial: Record = {}; - participants.forEach(p => { - const d = existingData[p.id]; - if (d?.ranking !== null && d?.ranking !== undefined) initial[p.id] = d.ranking.toString(); - }); - return initial; - }); - - const [bulkText, setBulkText] = useState(''); - const [parseResults, setParseResults] = useState<{ - matched: Array<{ participantId: string; name: string; elo: number; ranking: number | null; inputName: string }>; - unmatched: Array<{ inputName: string; elo: number; ranking: number | null }>; - } | null>(null); - - function findParticipantMatch(inputName: string) { - const normalizedInput = normalizeName(inputName); - const normalized = participants.map(p => ({ p, n: normalizeName(p.name) })); - - const exact = normalized.find(({ n }) => n === normalizedInput); - if (exact) return exact.p; - - const contains = normalized.find(({ n }) => n.includes(normalizedInput) || normalizedInput.includes(n)); - if (contains) return contains.p; - - const inputWords = normalizedInput.split(' ').filter(w => w.length > 2); - const overlap = normalized.find(({ n }) => { - const pWords = n.split(' ').filter(w => w.length > 2); - const shared = inputWords.filter(w => pWords.includes(w)); - return shared.length > 0 && shared.length >= Math.min(inputWords.length, pWords.length) * 0.5; - }); - - return overlap?.p ?? null; - } - - function parseBulkText() { - const lines = bulkText.split('\n'); - const matched: typeof parseResults extends null ? never : NonNullable['matched'] = []; - const unmatched: typeof parseResults extends null ? never : NonNullable['unmatched'] = []; - const seen = new Set(); - - for (const line of lines) { - const trimmed = line.trim(); - if (!trimmed) continue; - - const match = /^(.+?)[\s,:\t]+(\d{3,5})(?:[\s,:\t]+(\d{1,3}))?\s*$/.exec(trimmed); - if (!match) continue; - - const inputName = match[1].trim(); - const elo = parseInt(match[2], 10); - const ranking = match[3] ? parseInt(match[3], 10) : null; - - if (isNaN(elo) || elo < 500 || elo > 5000) continue; - if (ranking !== null && (isNaN(ranking) || ranking < 1 || ranking > 256)) continue; - - const participant = findParticipantMatch(inputName); - if (participant && !seen.has(participant.id)) { - seen.add(participant.id); - matched.push({ participantId: participant.id, name: participant.name, elo, ranking, inputName }); - } else if (!participant) { - unmatched.push({ inputName, elo, ranking }); - } - } - - setParseResults({ matched, unmatched }); - } - - function applyMatches() { - if (!parseResults) return; - const newElos = { ...eloValues }; - const newRanks = { ...rankValues }; - for (const m of parseResults.matched) { - newElos[m.participantId] = m.elo.toString(); - if (m.ranking !== null) { - newRanks[m.participantId] = m.ranking.toString(); - } - } - setEloValues(newElos); - setRankValues(newRanks); - setParseResults(null); - setBulkText(''); - } - - const isSubmitting = navigation.state === 'submitting'; - - const sortedParticipants = [...participants].toSorted((a, b) => { - const rankA = rankValues[a.id] ? parseInt(rankValues[a.id], 10) : null; - const rankB = rankValues[b.id] ? parseInt(rankValues[b.id], 10) : null; - if (rankA !== null && rankB !== null) return rankA - rankB; - if (rankA !== null) return -1; - if (rankB !== null) return 1; - return a.name.localeCompare(b.name); - }); - - return ( -
-
-

CS2 Team Elo & HLTV Rankings

-

- {sportsSeason.sport.name} — {sportsSeason.name} -

-
- - - - Bulk Import - - Paste one team per line. Format:{' '} - Team Name, 1850, 5 (name, Elo, HLTV ranking). - HLTV ranking is optional — omit it and the simulator will use Elo order for stage seeding. - Team names are fuzzy-matched to participants. - - - -