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:
parent
23904c9e84
commit
5f70852998
4 changed files with 35 additions and 13 deletions
|
|
@ -112,6 +112,11 @@ describe("futuresBlendLabel", () => {
|
||||||
expect(futuresBlendLabel(0)).toBe("Elo only");
|
expect(futuresBlendLabel(0)).toBe("Elo only");
|
||||||
expect(futuresBlendLabel(1)).toBe("overrides Elo");
|
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", () => {
|
describe("admin simulators action", () => {
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,10 @@ export async function action({ request }: Route.ActionArgs): Promise<ActionData>
|
||||||
export function futuresBlendLabel(oddsWeight: number): string {
|
export function futuresBlendLabel(oddsWeight: number): string {
|
||||||
if (oddsWeight >= 1) return "overrides Elo";
|
if (oddsWeight >= 1) return "overrides Elo";
|
||||||
if (oddsWeight <= 0) return "Elo only";
|
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`;
|
return `${100 - futuresPct}% Elo / ${futuresPct}% Futures`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Admin: Simulate action endpoint (stub)
|
* Admin: Simulate action endpoint (stub)
|
||||||
*
|
*
|
||||||
* Simulation is now handled by intent="simulate" on the season admin page.
|
* Simulation is now handled by the "Run Simulation" button on the Simulator
|
||||||
* This stub redirects any direct GET navigation to that page.
|
* setup page (/admin/sports-seasons/:id/simulator). This stub redirects any
|
||||||
|
* direct GET navigation to the season admin page.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { redirect } from "react-router";
|
import { redirect } from "react-router";
|
||||||
|
|
|
||||||
|
|
@ -264,15 +264,26 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone
|
||||||
|
|
||||||
// Two synced blend weights that always total 100%. `oddsWeight` (0–1) stays
|
// 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
|
// the single stored source of truth; we present it (and its complement) as
|
||||||
// percentages here. Editing either field updates the other.
|
// percentages here. `futuresPct` is the committed value (drives the hidden
|
||||||
const [futuresPct, setFuturesPct] = useState(() =>
|
// field and the complementary input); `draft` holds the raw text of whichever
|
||||||
Math.round(Math.min(1, Math.max(0, inputPolicy.oddsWeight)) * 100)
|
// 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.
|
||||||
const basePct = 100 - futuresPct;
|
|
||||||
function clampPct(value: number): number {
|
function clampPct(value: number): number {
|
||||||
if (!Number.isFinite(value)) return 0;
|
|
||||||
return Math.min(100, Math.max(0, Math.round(value)));
|
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 (
|
return (
|
||||||
<div className="container mx-auto p-6 space-y-6">
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
|
@ -393,8 +404,9 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone
|
||||||
step={5}
|
step={5}
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
value={basePct}
|
value={draft?.field === "base" ? draft.text : String(basePct)}
|
||||||
onChange={(e) => setFuturesPct(100 - clampPct(e.target.valueAsNumber))}
|
onChange={(e) => handlePctChange("base", e.target.value)}
|
||||||
|
onBlur={() => setDraft(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -405,8 +417,9 @@ export default function AdminSportsSeasonSimulator({ loaderData }: Route.Compone
|
||||||
step={5}
|
step={5}
|
||||||
min={0}
|
min={0}
|
||||||
max={100}
|
max={100}
|
||||||
value={futuresPct}
|
value={draft?.field === "futures" ? draft.text : String(futuresPct)}
|
||||||
onChange={(e) => setFuturesPct(clampPct(e.target.valueAsNumber))}
|
onChange={(e) => handlePctChange("futures", e.target.value)}
|
||||||
|
onBlur={() => setDraft(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue