Fix simulate route crash and surface actual simulation errors (#64)
## Problem
Running a simulation on MLB (and any sport where the readiness check fails) showed a confusing React Router 400 error instead of the real failure reason. The cause: the simulate route had only an \`action\` export. When the action **threw** instead of redirecting, React Router tried to render the route via GET to display the error — hit the missing loader — and replaced the real error with its own internal 400.
A secondary issue: the interim fix used \`?simulationError=\` query params, which had three problems identified in code review:
- Stale errors re-appeared when pressing browser Back after fixing the issue
- The param reflected arbitrary text into the UI (social engineering surface)
- The styling didn't match the established \`bg-destructive/15\` error pattern
## Changes
**\`admin.sports-seasons.\$id.simulate.tsx\`** — strip the action; keep a loader-only redirect stub so bookmarked URLs degrade gracefully to the season page.
**\`admin.sports-seasons.\$id.tsx\`** — add \`intent="simulate"\` to the action dispatcher (matching \`delete\`, \`rescore\`, \`sync-standings\`, \`finalize-standings\`). On success: redirect to expected-values (unchanged). On failure: return \`{ simulateError: message }\` displayed inline with the correct \`bg-destructive/15\` styling.
## Test plan
- [ ] Click Run Simulation on a season with a missing readiness input → error message appears inline below the description text, styled consistently with other errors on the page
- [ ] Click Run Simulation on a fully-configured season → redirects to expected-values as before
- [ ] Navigate directly to \`/admin/sports-seasons/<id>/simulate\` in browser → redirects to season page (no 400)
- [ ] Run simulation, see error, fix the issue, run again successfully → no stale error message persists
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #64
This commit is contained in:
parent
ac22ff3a12
commit
ad910d4f40
2 changed files with 24 additions and 17 deletions
|
|
@ -1,24 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* Admin: Run EV Simulation for a Sports Season
|
* Admin: Simulate action endpoint (stub)
|
||||||
*
|
*
|
||||||
* Action-only route. POSTing here:
|
* Simulation is now handled by intent="simulate" on the season admin page.
|
||||||
* 1. Delegates validation + execution to the shared simulator runner
|
* This stub redirects any direct GET navigation to that page.
|
||||||
* 6. Redirects back to the sports season admin page
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { redirect } from "react-router";
|
import { redirect } from "react-router";
|
||||||
import type { Route } from "./+types/admin.sports-seasons.$id.simulate";
|
import type { Route } from "./+types/admin.sports-seasons.$id.simulate";
|
||||||
import { runSportsSeasonSimulation } from "~/services/simulations/runner";
|
|
||||||
|
|
||||||
export async function action({ params }: Route.ActionArgs) {
|
export async function loader({ params }: Route.LoaderArgs) {
|
||||||
const sportsSeasonId = params.id;
|
return redirect(`/admin/sports-seasons/${params.id}`);
|
||||||
try {
|
|
||||||
await runSportsSeasonSimulation(sportsSeasonId);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Response(error instanceof Error ? error.message : "Simulation failed", {
|
|
||||||
status: error instanceof Error && error.message.includes("already running") ? 409 : 400,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { database } from "~/database/context";
|
||||||
import { participantEvSnapshots, seasonSports } from "~/database/schema";
|
import { participantEvSnapshots, seasonSports } from "~/database/schema";
|
||||||
import { eq, desc } from "drizzle-orm";
|
import { eq, desc } from "drizzle-orm";
|
||||||
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
|
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
|
||||||
|
import { runSportsSeasonSimulation } from "~/services/simulations/runner";
|
||||||
import { syncStandings } from "~/services/standings-sync/index";
|
import { syncStandings } from "~/services/standings-sync/index";
|
||||||
import {
|
import {
|
||||||
getPendingStandingsMappings,
|
getPendingStandingsMappings,
|
||||||
|
|
@ -280,6 +281,17 @@ export async function action(args: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "simulate") {
|
||||||
|
try {
|
||||||
|
await runSportsSeasonSimulation(params.id);
|
||||||
|
return redirect(`/admin/sports-seasons/${params.id}/expected-values`);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
simulateError: error instanceof Error ? error.message : "Simulation failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
const name = formData.get("name");
|
const name = formData.get("name");
|
||||||
const year = formData.get("year");
|
const year = formData.get("year");
|
||||||
|
|
@ -669,7 +681,8 @@ export default function EditSportsSeason({ loaderData, actionData }: Route.Compo
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{simulatorInfo && (
|
{simulatorInfo && (
|
||||||
<Form method="post" action={`/admin/sports-seasons/${sportsSeason.id}/simulate`}>
|
<Form method="post">
|
||||||
|
<input type="hidden" name="intent" value="simulate" />
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -702,6 +715,11 @@ export default function EditSportsSeason({ loaderData, actionData }: Route.Compo
|
||||||
</>
|
</>
|
||||||
: "Import futures odds to set probability distributions."}
|
: "Import futures odds to set probability distributions."}
|
||||||
</p>
|
</p>
|
||||||
|
{"simulateError" in (actionData ?? {}) && actionData?.simulateError && (
|
||||||
|
<div className="mt-3 bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
|
||||||
|
{actionData.simulateError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue