diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index 763d614..e52e709 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -52,6 +52,7 @@ import { logger } from "~/lib/logger"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq } from "drizzle-orm"; +import { getSocketIO } from "../../../server/socket"; export async function loader({ params }: Route.LoaderArgs) { const sportsSeason = await findSportsSeasonById(params.id); @@ -622,6 +623,19 @@ export async function action({ request, params }: Route.ActionArgs) { // skipDiscord: recalculate-floors is a data-correction tool, not a result announcement. await recalculateAffectedLeagues(event.sportsSeasonId, undefined, { skipDiscord: true }); + // Notify connected league pages to refresh their elimination/standings data. + try { + const seasonSports = await db.query.seasonSports.findMany({ + where: eq(schema.seasonSports.sportsSeasonId, event.sportsSeasonId), + }); + const io = getSocketIO(); + for (const ss of seasonSports) { + io.to(`draft-${ss.seasonId}`).emit("standings-updated", { sportsSeasonId: event.sportsSeasonId }); + } + } catch (socketError) { + logger.warn("[recalculate-floors] Could not emit standings-updated:", socketError); + } + return { success: `Recalculated floors from ${sortedMatches.length} completed match(es)` }; } catch (error) { logger.error("Error recalculating floors:", error); diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx index 9746f77..95121ce 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx @@ -1,4 +1,6 @@ -import { Link } from "react-router"; +import { Link, useRevalidator } from "react-router"; +import { useEffect } from "react"; +import { io } from "socket.io-client"; import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId"; import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.server"; @@ -69,6 +71,25 @@ export default function SportSeasonDetail({ groupStandings, } = loaderData; + const { revalidate } = useRevalidator(); + + // Revalidate when the admin updates standings (e.g. recalculate-floors). + useEffect(() => { + const socket = io({ path: "/socket.io", transports: ["websocket", "polling"] }); + socket.on("connect", () => { + socket.emit("join-draft", season.id); + }); + socket.on("standings-updated", (data: { sportsSeasonId: string }) => { + if (data.sportsSeasonId === sportsSeason.id) { + revalidate(); + } + }); + return () => { + socket.emit("leave-draft", season.id); + socket.disconnect(); + }; + }, [season.id, sportsSeason.id, revalidate]); + const hasBracket = playoffMatches && playoffMatches.length > 0; const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0; const showOtLosses = hasStandings && regularSeasonStandings.some((s) => s.otLosses !== null); diff --git a/server/socket.ts b/server/socket.ts index 8bdea68..b2d6e01 100644 --- a/server/socket.ts +++ b/server/socket.ts @@ -37,6 +37,7 @@ interface ServerToClientEvents { "draft-resumed": (data: { seasonId: string; paused: boolean }) => void; "participant-removed-from-queues": (data: { participantId: string }) => void; "queue-updated": (data: { queue: Array<{ id: string; teamId: string; seasonId: string; participantId: string; queuePosition: number }> }) => void; + "standings-updated": (data: { sportsSeasonId: string }) => void; "draft-state-sync": (data: { currentPickNumber: number; isPaused: boolean;