Emit standings-updated socket event after recalculate-floors so league pages refresh automatically

When the admin runs "Recalculate Floors" on the bracket page, the league
sports-season homepage was showing stale elimination data because there was
no mechanism to notify it of the change.

Fix: after recalculate-floors updates participant_results, emit a
standings-updated socket event to all fantasy-season draft rooms linked to
the sports season. The sports-season page now joins its draft room and
revalidates its loader whenever it receives that event for the matching
sports season.

https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB
This commit is contained in:
Claude 2026-04-15 17:28:08 +00:00
parent f356bdce03
commit 90a4039cba
No known key found for this signature in database
3 changed files with 37 additions and 1 deletions

View file

@ -52,6 +52,7 @@ import { logger } from "~/lib/logger";
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { getSocketIO } from "../../../server/socket";
export async function loader({ params }: Route.LoaderArgs) { export async function loader({ params }: Route.LoaderArgs) {
const sportsSeason = await findSportsSeasonById(params.id); 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. // skipDiscord: recalculate-floors is a data-correction tool, not a result announcement.
await recalculateAffectedLeagues(event.sportsSeasonId, undefined, { skipDiscord: true }); 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)` }; return { success: `Recalculated floors from ${sortedMatches.length} completed match(es)` };
} catch (error) { } catch (error) {
logger.error("Error recalculating floors:", error); logger.error("Error recalculating floors:", error);

View file

@ -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 type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId";
import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.server"; import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.server";
@ -69,6 +71,25 @@ export default function SportSeasonDetail({
groupStandings, groupStandings,
} = loaderData; } = 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 hasBracket = playoffMatches && playoffMatches.length > 0;
const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0; const hasStandings = regularSeasonStandings && regularSeasonStandings.length > 0;
const showOtLosses = hasStandings && regularSeasonStandings.some((s) => s.otLosses !== null); const showOtLosses = hasStandings && regularSeasonStandings.some((s) => s.otLosses !== null);

View file

@ -37,6 +37,7 @@ interface ServerToClientEvents {
"draft-resumed": (data: { seasonId: string; paused: boolean }) => void; "draft-resumed": (data: { seasonId: string; paused: boolean }) => void;
"participant-removed-from-queues": (data: { participantId: string }) => 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; "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: { "draft-state-sync": (data: {
currentPickNumber: number; currentPickNumber: number;
isPaused: boolean; isPaused: boolean;