Fix simulate route crash and surface actual error messages
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 2m24s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m19s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped

When a simulation action threw (e.g. readiness check failure), React Router
tried to render the simulate route via GET to display the error — but the
route had no loader, so the real error was completely swallowed by React
Router's own 400 "no loader" message.

Four issues fixed:
- Move simulate into the intent-dispatch pattern on the season page (matching
  delete/rescore/sync-standings/finalize-standings), so errors surface via
  actionData instead of being lost
- Eliminate the ?simulationError= query param approach, which left stale error
  messages in browser history and reflected user-controlled text in the UI
- Match the existing bg-destructive/15 error styling used elsewhere on the page
- Strip the now-unused action from simulate.tsx; keep a loader-only redirect
  stub so any bookmarked URLs degrade gracefully to the season page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-05-31 23:42:35 -07:00
parent ac22ff3a12
commit ba970cd571
2 changed files with 24 additions and 17 deletions

View file

@ -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`);
} }

View file

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