Address review: free-typing blend inputs, honest blend label, fix stale comment

- 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RvJnQPEJNRxGipSad7q2T
This commit is contained in:
Claude 2026-06-26 23:17:54 +00:00
parent 23904c9e84
commit 5f70852998
No known key found for this signature in database
4 changed files with 35 additions and 13 deletions

View file

@ -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 199% 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", () => {

View file

@ -80,7 +80,10 @@ export async function action({ request }: Route.ActionArgs): Promise<ActionData>
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`;
}

View file

@ -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";

View file

@ -264,15 +264,26 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone
// Two synced blend weights that always total 100%. `oddsWeight` (01) 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 (
<div className="container mx-auto p-6 space-y-6">
@ -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)}
/>
</div>
<div className="space-y-1">
@ -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)}
/>
</div>
</div>