From ba970cd5716d9288e3eda2b2680fc32475189cfd Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 31 May 2026 23:42:35 -0700 Subject: [PATCH 1/2] Fix simulate route crash and surface actual error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../admin.sports-seasons.$id.simulate.tsx | 21 +++++-------------- app/routes/admin.sports-seasons.$id.tsx | 20 +++++++++++++++++- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/app/routes/admin.sports-seasons.$id.simulate.tsx b/app/routes/admin.sports-seasons.$id.simulate.tsx index 5dace5c..49dce26 100644 --- a/app/routes/admin.sports-seasons.$id.simulate.tsx +++ b/app/routes/admin.sports-seasons.$id.simulate.tsx @@ -1,24 +1,13 @@ /** - * Admin: Run EV Simulation for a Sports Season + * Admin: Simulate action endpoint (stub) * - * Action-only route. POSTing here: - * 1. Delegates validation + execution to the shared simulator runner - * 6. Redirects back to the sports season admin page + * Simulation is now handled by intent="simulate" on the season admin page. + * This stub redirects any direct GET navigation to that page. */ import { redirect } from "react-router"; import type { Route } from "./+types/admin.sports-seasons.$id.simulate"; -import { runSportsSeasonSimulation } from "~/services/simulations/runner"; -export async function action({ params }: Route.ActionArgs) { - const sportsSeasonId = 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`); +export async function loader({ params }: Route.LoaderArgs) { + return redirect(`/admin/sports-seasons/${params.id}`); } diff --git a/app/routes/admin.sports-seasons.$id.tsx b/app/routes/admin.sports-seasons.$id.tsx index fb84899..43aab95 100644 --- a/app/routes/admin.sports-seasons.$id.tsx +++ b/app/routes/admin.sports-seasons.$id.tsx @@ -11,6 +11,7 @@ import { database } from "~/database/context"; import { participantEvSnapshots, seasonSports } from "~/database/schema"; import { eq, desc } from "drizzle-orm"; import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry"; +import { runSportsSeasonSimulation } from "~/services/simulations/runner"; import { syncStandings } from "~/services/standings-sync/index"; import { 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 const name = formData.get("name"); const year = formData.get("year"); @@ -669,7 +681,8 @@ export default function EditSportsSeason({ loaderData, actionData }: Route.Compo )} {simulatorInfo && ( -
+ +