From 5f7085299818900db3e5f5bc513a7659cc3a02a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 26 Jun 2026 23:17:54 +0000 Subject: [PATCH] Address review: free-typing blend inputs, honest blend label, fix stale comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Simulator setup blend fields now keep a draft text buffer per field so an admin can clear/retype a weight without it snapping to 0/100; the committed futuresPct only re-syncs on a parseable number and normalizes on blur. - futuresBlendLabel clamps a genuine blend to 1–99% so a near-extreme weight (e.g. 0.999) never reads as "0% Elo / 100% Futures"; the 0/1 extremes stay reserved for the "Elo only" / "overrides Elo" labels. Added test coverage. - Refresh the stale comment in the simulate stub route, which referenced the removed intent="simulate". Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_017RvJnQPEJNRxGipSad7q2T --- app/routes/__tests__/admin.simulators.test.ts | 5 +++ app/routes/admin.simulators.tsx | 5 ++- .../admin.sports-seasons.$id.simulate.tsx | 5 +-- .../admin.sports-seasons.$id.simulator.tsx | 33 +++++++++++++------ 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/app/routes/__tests__/admin.simulators.test.ts b/app/routes/__tests__/admin.simulators.test.ts index 52f5df1..fb46500 100644 --- a/app/routes/__tests__/admin.simulators.test.ts +++ b/app/routes/__tests__/admin.simulators.test.ts @@ -112,6 +112,11 @@ describe("futuresBlendLabel", () => { expect(futuresBlendLabel(0)).toBe("Elo only"); expect(futuresBlendLabel(1)).toBe("overrides Elo"); }); + + it("keeps a near-extreme blend within 1–99% so it never reads as 0/100", () => { + expect(futuresBlendLabel(0.999)).toBe("1% Elo / 99% Futures"); + expect(futuresBlendLabel(0.004)).toBe("99% Elo / 1% Futures"); + }); }); describe("admin simulators action", () => { diff --git a/app/routes/admin.simulators.tsx b/app/routes/admin.simulators.tsx index a702363..73082c9 100644 --- a/app/routes/admin.simulators.tsx +++ b/app/routes/admin.simulators.tsx @@ -80,7 +80,10 @@ export async function action({ request }: Route.ActionArgs): Promise export function futuresBlendLabel(oddsWeight: number): string { if (oddsWeight >= 1) return "overrides Elo"; if (oddsWeight <= 0) return "Elo only"; - const futuresPct = Math.round(oddsWeight * 100); + // A genuine blend (0 < oddsWeight < 1) should never read as a 0/100 split: + // keep the displayed futures share within [1, 99] so the extremes stay + // reserved for the "Elo only" / "overrides Elo" labels above. + const futuresPct = Math.min(99, Math.max(1, Math.round(oddsWeight * 100))); return `${100 - futuresPct}% Elo / ${futuresPct}% Futures`; } diff --git a/app/routes/admin.sports-seasons.$id.simulate.tsx b/app/routes/admin.sports-seasons.$id.simulate.tsx index 49dce26..e399f22 100644 --- a/app/routes/admin.sports-seasons.$id.simulate.tsx +++ b/app/routes/admin.sports-seasons.$id.simulate.tsx @@ -1,8 +1,9 @@ /** * Admin: Simulate action endpoint (stub) * - * Simulation is now handled by intent="simulate" on the season admin page. - * This stub redirects any direct GET navigation to that page. + * Simulation is now handled by the "Run Simulation" button on the Simulator + * setup page (/admin/sports-seasons/:id/simulator). This stub redirects any + * direct GET navigation to the season admin page. */ import { redirect } from "react-router"; diff --git a/app/routes/admin.sports-seasons.$id.simulator.tsx b/app/routes/admin.sports-seasons.$id.simulator.tsx index 885b160..85cba45 100644 --- a/app/routes/admin.sports-seasons.$id.simulator.tsx +++ b/app/routes/admin.sports-seasons.$id.simulator.tsx @@ -264,15 +264,26 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone // Two synced blend weights that always total 100%. `oddsWeight` (0–1) stays // the single stored source of truth; we present it (and its complement) as - // percentages here. Editing either field updates the other. - const [futuresPct, setFuturesPct] = useState(() => - Math.round(Math.min(1, Math.max(0, inputPolicy.oddsWeight)) * 100) - ); - const basePct = 100 - futuresPct; + // percentages here. `futuresPct` is the committed value (drives the hidden + // field and the complementary input); `draft` holds the raw text of whichever + // field is being typed in, so it can be cleared/edited freely and only + // re-syncs on a parseable number — then normalizes away on blur. function clampPct(value: number): number { - if (!Number.isFinite(value)) return 0; return Math.min(100, Math.max(0, Math.round(value))); } + const [futuresPct, setFuturesPct] = useState(() => + clampPct(Math.min(1, Math.max(0, inputPolicy.oddsWeight)) * 100) + ); + const [draft, setDraft] = useState<{ field: "base" | "futures"; text: string } | null>(null); + const basePct = 100 - futuresPct; + + function handlePctChange(field: "base" | "futures", text: string) { + setDraft({ field, text }); + const parsed = Number(text); + if (text.trim() !== "" && Number.isFinite(parsed)) { + setFuturesPct(field === "futures" ? clampPct(parsed) : 100 - clampPct(parsed)); + } + } return (
@@ -393,8 +404,9 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone step={5} min={0} max={100} - value={basePct} - onChange={(e) => setFuturesPct(100 - clampPct(e.target.valueAsNumber))} + value={draft?.field === "base" ? draft.text : String(basePct)} + onChange={(e) => handlePctChange("base", e.target.value)} + onBlur={() => setDraft(null)} />
@@ -405,8 +417,9 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone step={5} min={0} max={100} - value={futuresPct} - onChange={(e) => setFuturesPct(clampPct(e.target.valueAsNumber))} + value={draft?.field === "futures" ? draft.text : String(futuresPct)} + onChange={(e) => handlePctChange("futures", e.target.value)} + onBlur={() => setDraft(null)} />