* Add oxlint and fix all lint errors - Install oxlint, add .oxlintrc.json with rules for TypeScript/React - Add npm run lint / lint:fix scripts - Add Claude PostToolUse hook to run oxlint on every edited file - Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array - Fix no-array-index-key (use stable keys or suppress positional cases) - Fix exhaustive-deps missing dependency in useEffect - Promote exhaustive-deps and no-array-index-key to errors - Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix no-explicit-any warnings and upgrade tsconfig to ES2023 - Replace all `any` types with proper types or `unknown` across ~20 files - Add typed socket payload interfaces in draft route and useDraftSocket - Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch) - Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed - Fix cascading type errors uncovered by removing any: Map.get narrowing, participant relation types, ChartDataPoint, Partial<NewSeason> indexing - Add ParticipantResultWithParticipant type to participant-result model - Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult) - Fix duplicate getQPStandings import in sportsSeasonId.server.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Promote no-explicit-any to error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
162 lines
7.1 KiB
TypeScript
162 lines
7.1 KiB
TypeScript
import { Link } from "react-router";
|
||
import type { Route } from "./+types/admin.sports-seasons.$id.expected-values";
|
||
|
||
import { loader } from "./admin.sports-seasons.$id.expected-values.server";
|
||
import { Button } from "~/components/ui/button";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "~/components/ui/card";
|
||
import {
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableHeader,
|
||
TableRow,
|
||
} from "~/components/ui/table";
|
||
import { ArrowLeft, Calculator } from "lucide-react";
|
||
|
||
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
||
return [{ title: `Expected Values — ${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
|
||
}
|
||
|
||
export { loader };
|
||
|
||
// DEFAULT scoring values — must match DEFAULT_SCORING_RULES in the simulate route.
|
||
// Scoring: 1st=100, 2nd=70, 3rd/4th (FF losers)=45 each, 5th–8th (E8 losers)=20 each.
|
||
// Sum = 100+70+45+45+20+20+20+20 = 340.
|
||
//
|
||
// Total EV invariant: Σ EV across all participants = Σ scoring values = 340,
|
||
// because each probability column sums to 1.0 across all participants.
|
||
// If total drifts from 340, likely causes:
|
||
// 1. Stale EV records from a prior simulation run (fix: re-run simulation, which now
|
||
// zeros non-bracket participants automatically)
|
||
// 2. DB precision truncation (numeric(6,4) = 4dp; max drift ≈ ±1 for 68 teams)
|
||
const SCORING = [100, 70, 45, 45, 20, 20, 20, 20] as const;
|
||
|
||
function evFromProbs(ev: {
|
||
probFirst: string; probSecond: string; probThird: string; probFourth: string;
|
||
probFifth: string; probSixth: string; probSeventh: string; probEighth: string;
|
||
}): number {
|
||
return parseFloat(ev.probFirst) * SCORING[0]
|
||
+ parseFloat(ev.probSecond) * SCORING[1]
|
||
+ parseFloat(ev.probThird) * SCORING[2]
|
||
+ parseFloat(ev.probFourth) * SCORING[3]
|
||
+ parseFloat(ev.probFifth) * SCORING[4]
|
||
+ parseFloat(ev.probSixth) * SCORING[5]
|
||
+ parseFloat(ev.probSeventh) * SCORING[6]
|
||
+ parseFloat(ev.probEighth) * SCORING[7];
|
||
}
|
||
|
||
function fmt(val: string | number) {
|
||
return (parseFloat(val as string) * 100).toFixed(1) + "%";
|
||
}
|
||
|
||
export default function ExpectedValuesPage({ loaderData }: Route.ComponentProps) {
|
||
const { sportsSeason, participants, existingEVs } = loaderData;
|
||
|
||
// Compute total and sort from stored prob columns, not stored expectedValue.
|
||
// The simulator normalizes per-position column sums to exactly 1.0 (step 10),
|
||
// so the total EV should always equal the sum of scoring values (340).
|
||
// Sum only over participants shown in the table — excludes orphan EV records
|
||
// from prior simulation runs for participants no longer in this season.
|
||
const sorted = [...participants].toSorted((a, b) => {
|
||
const evA = existingEVs.has(a.id) ? evFromProbs(existingEVs.get(a.id)!) : 0;
|
||
const evB = existingEVs.has(b.id) ? evFromProbs(existingEVs.get(b.id)!) : 0;
|
||
return evB - evA;
|
||
});
|
||
|
||
const totalEV = sorted.reduce((sum, p) => {
|
||
const ev = existingEVs.get(p.id);
|
||
return ev ? sum + evFromProbs(ev) : sum;
|
||
}, 0);
|
||
|
||
return (
|
||
<div className="container mx-auto p-6 space-y-6">
|
||
<div className="flex items-center gap-4">
|
||
<Link to={`/admin/sports-seasons/${sportsSeason.id}`}>
|
||
<Button variant="ghost" size="sm">
|
||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||
Back to Sports Season
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Calculator className="h-5 w-5" />
|
||
Expected Values: {sportsSeason.sport.name} {sportsSeason.year}
|
||
</CardTitle>
|
||
<CardDescription>
|
||
Probability distributions from the last simulation run. Sorted by EV descending.
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{participants.length === 0 ? (
|
||
<p className="text-center text-muted-foreground py-8">
|
||
No participants found.
|
||
</p>
|
||
) : existingEVs.size === 0 ? (
|
||
<p className="text-center text-muted-foreground py-8">
|
||
No EV data yet. Run a simulation from the sports season page.
|
||
</p>
|
||
) : (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>Participant</TableHead>
|
||
<TableHead className="text-center">1st</TableHead>
|
||
<TableHead className="text-center">2nd</TableHead>
|
||
<TableHead className="text-center">3rd</TableHead>
|
||
<TableHead className="text-center">4th</TableHead>
|
||
<TableHead className="text-center">5th</TableHead>
|
||
<TableHead className="text-center">6th</TableHead>
|
||
<TableHead className="text-center">7th</TableHead>
|
||
<TableHead className="text-center">8th</TableHead>
|
||
<TableHead className="text-center">EV</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{sorted.map((participant) => {
|
||
const ev = existingEVs.get(participant.id);
|
||
return (
|
||
<TableRow key={participant.id}>
|
||
<TableCell className="font-medium">{participant.name}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probFirst) : "—"}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probSecond) : "—"}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probThird) : "—"}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probFourth) : "—"}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probFifth) : "—"}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probSixth) : "—"}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probSeventh) : "—"}</TableCell>
|
||
<TableCell className="text-center font-mono text-sm">{ev ? fmt(ev.probEighth) : "—"}</TableCell>
|
||
<TableCell className="text-center font-semibold">{ev ? evFromProbs(ev).toFixed(2) : "—"}</TableCell>
|
||
</TableRow>
|
||
);
|
||
})}
|
||
{existingEVs.size > 0 && (
|
||
<TableRow className="border-t-2 font-bold bg-muted/50">
|
||
<TableCell colSpan={9} className="text-right">
|
||
Total EV
|
||
{Math.abs(totalEV - 340) > 1 && (
|
||
<span className="ml-2 text-xs font-normal text-destructive">
|
||
(expected ~340; re-run simulation to fix stale data)
|
||
</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell className="text-center">{totalEV.toFixed(2)}</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|