fix: resolve schedule data staleness in production builds
- Replace window.location.reload() with router.refresh() in ScheduleGenerator, MatchEditor, RecalculateEloButton for proper Next.js cache invalidation - Add revalidatePath() call after schedule generation in POST handler - Add ownerId to tournament creation in cucumber tests for proper permission checks - Assign tournament_admin role via Prisma after user creation in cucumber tests - Fix TypeScript type annotations in hooks.ts (tournament id map) - Update page reload to use networkidle in common-steps.ts - Clear .next/ cache before cucumber tests in justfile - Add .turbo to clean target - Add comprehensive debug logging to schedule API route - Document findings in docs/TROUBLESHOOTING_SCHEDULE_GENERATION.md
This commit is contained in:
@@ -23,6 +23,7 @@ export default async function TournamentSchedulePage({ params }: PageProps) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
console.log(`[Schedule Page] Fetching tournament ${tournamentId}`);
|
||||
const tournament = await prisma.event.findUnique({
|
||||
where: { id: tournamentId },
|
||||
include: {
|
||||
@@ -48,6 +49,10 @@ export default async function TournamentSchedulePage({ params }: PageProps) {
|
||||
},
|
||||
},
|
||||
})
|
||||
console.log(`[Schedule Page] Tournament ${tournamentId} has ${tournament?.rounds?.length || 0} rounds`);
|
||||
if (tournament?.rounds && tournament.rounds.length > 0) {
|
||||
console.log(`[Schedule Page] First round:`, JSON.stringify(tournament.rounds[0]));
|
||||
}
|
||||
|
||||
if (!tournament) {
|
||||
notFound()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { canManageTournament } from "@/lib/permissions";
|
||||
import { generateRoundRobin, validateScheduleInput, generateVariableRoundRobin, expectedRounds } from "@/lib/schedule-generator";
|
||||
@@ -78,11 +79,15 @@ export async function GET(_request: Request, { params }: RouteParams) {
|
||||
* Creates TournamentRound and BracketMatchup records.
|
||||
*/
|
||||
export async function POST(_request: Request, { params }: RouteParams) {
|
||||
console.log(`[Schedule API] POST handler started`);
|
||||
try {
|
||||
const { id } = await params;
|
||||
const tournamentId = parseInt(id);
|
||||
|
||||
console.log(`[Schedule API] POST /api/tournaments/${tournamentId}/schedule`);
|
||||
|
||||
if (isNaN(tournamentId)) {
|
||||
console.log(`[Schedule API] Invalid tournament ID: ${id}`);
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid tournament ID" },
|
||||
{ status: 400 }
|
||||
@@ -90,6 +95,7 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
}
|
||||
|
||||
const permission = await canManageTournament(tournamentId);
|
||||
console.log(`[Schedule API] Permission check: ${permission.allowed}, reason: ${permission.reason}`);
|
||||
if (!permission.allowed) {
|
||||
return NextResponse.json(
|
||||
{ error: permission.reason || "Not authorized to manage this tournament" },
|
||||
@@ -98,6 +104,7 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
}
|
||||
|
||||
// Check tournament exists
|
||||
console.log(`[Schedule API] Looking up tournament ${tournamentId}`);
|
||||
const tournament = await prisma.event.findUnique({
|
||||
where: { id: tournamentId },
|
||||
include: {
|
||||
@@ -111,14 +118,17 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
});
|
||||
|
||||
if (!tournament) {
|
||||
console.log(`[Schedule API] Tournament ${tournamentId} not found`);
|
||||
return NextResponse.json(
|
||||
{ error: "Tournament not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
console.log(`[Schedule API] Found tournament ${tournamentId} with ${tournament.participants.length} participants and ${tournament.rounds.length} existing rounds`);
|
||||
|
||||
// Check if schedule already exists and delete it
|
||||
if (tournament.rounds.length > 0) {
|
||||
console.log(`[Schedule API] Deleting ${tournament.rounds.length} existing rounds`);
|
||||
// Delete existing rounds and matchups before regenerating
|
||||
await prisma.bracketMatchup.deleteMany({
|
||||
where: { eventId: tournamentId },
|
||||
@@ -135,6 +145,8 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
currentElo: p.player.currentElo,
|
||||
}));
|
||||
|
||||
console.log(`[Schedule API] Got ${participants.length} participants`);
|
||||
|
||||
// Check minimum participants
|
||||
if (participants.length < 2) {
|
||||
return NextResponse.json(
|
||||
@@ -147,20 +159,24 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
const teamDurability = tournament.teamDurability || "permanent";
|
||||
const partnerRotation = (tournament.partnerRotation || "none") as 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based';
|
||||
const allowByes = tournament.allowByes ?? true;
|
||||
console.log(`[Schedule API] Team durability: ${teamDurability}, partner rotation: ${partnerRotation}, allow byes: ${allowByes}`);
|
||||
|
||||
// Determine number of teams from participants
|
||||
const tempResult = generateTeams(participants, partnerRotation, allowByes);
|
||||
const teamCount = tempResult.teams.length;
|
||||
|
||||
console.log(`[Schedule API] Generated ${teamCount} teams from ${participants.length} participants`);
|
||||
|
||||
if (teamCount < 2) {
|
||||
console.log(`[Schedule API] Not enough teams: ${teamCount}`);
|
||||
return NextResponse.json(
|
||||
{ error: "At least 2 teams (4 players) are required to generate a schedule" },
|
||||
{ error: "At least 2 teams are required to generate a schedule" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate number of rounds needed
|
||||
const numRounds = expectedRounds(teamCount);
|
||||
// Calculate expected rounds
|
||||
const expectedRounds = expectedRounds(teamCount);
|
||||
console.log(`[Schedule API] Expected rounds: ${expectedRounds}`);
|
||||
|
||||
if (teamDurability === "permanent") {
|
||||
// ============================================
|
||||
@@ -186,11 +202,14 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
|
||||
// Generate schedule using fixed teams
|
||||
const schedule = generateRoundRobin(teamPairings);
|
||||
console.log(`[Schedule API] Generated ${schedule.length} rounds for ${teamCount} teams (fixed)`);
|
||||
|
||||
// Create rounds and matchups in a transaction
|
||||
console.log(`[Schedule API] About to create ${schedule.length} rounds in transaction`);
|
||||
const created = await prisma.$transaction(
|
||||
schedule.map((round) =>
|
||||
prisma.tournamentRound.create({
|
||||
schedule.map((round) => {
|
||||
console.log(`[Schedule API] Creating round ${round.roundNumber} with ${round.matchups.length} matchups`);
|
||||
return prisma.tournamentRound.create({
|
||||
data: {
|
||||
eventId: tournamentId,
|
||||
roundNumber: round.roundNumber,
|
||||
@@ -218,8 +237,17 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`[Schedule API] Transaction complete. Created ${created.length} rounds`);
|
||||
console.log(`[Schedule API] Verifying in database:`);
|
||||
for (const round of created) {
|
||||
console.log(`[Schedule API] Round ${round.roundNumber}: id=${round.id}, eventId=${round.eventId}`);
|
||||
}
|
||||
|
||||
revalidatePath(`/admin/tournaments/${tournamentId}/schedule`);
|
||||
console.log(`[Schedule API] revalidatePath called for /admin/tournaments/${tournamentId}/schedule`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
@@ -257,7 +285,7 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
const schedule = generateVariableRoundRobin(
|
||||
participants,
|
||||
teamCount,
|
||||
numRounds,
|
||||
expectedRounds,
|
||||
generateTeamWithRotation
|
||||
);
|
||||
|
||||
@@ -295,6 +323,8 @@ export async function POST(_request: Request, { params }: RouteParams) {
|
||||
)
|
||||
);
|
||||
|
||||
revalidatePath(`/admin/tournaments/${tournamentId}/schedule`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
roundsCreated: created.length,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import type { Player, Match } from "@prisma/client"
|
||||
|
||||
interface MatchEditorProps {
|
||||
@@ -40,6 +41,7 @@ export default function MatchEditor({
|
||||
prefilledP4,
|
||||
prefilledRound,
|
||||
}: MatchEditorProps) {
|
||||
const router = useRouter()
|
||||
// Check if players are prefilled from URL params
|
||||
const hasPrefilledPlayers = prefilledP1 && prefilledP2 && prefilledP3 && prefilledP4;
|
||||
|
||||
@@ -170,9 +172,9 @@ export default function MatchEditor({
|
||||
isCasual: false,
|
||||
})
|
||||
|
||||
// Reload the page to show updated matches
|
||||
// Refresh the page to show updated matches
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
router.refresh()
|
||||
}, 1000)
|
||||
} catch {
|
||||
setError("An error occurred. Please try again.")
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export function RecalculateEloButton() {
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleClick = async () => {
|
||||
@@ -33,7 +35,7 @@ export function RecalculateEloButton() {
|
||||
|
||||
if (data.success) {
|
||||
alert(`Recalculation completed: ${JSON.stringify(data.data)}`)
|
||||
window.location.reload()
|
||||
router.refresh()
|
||||
} else {
|
||||
alert(`Error: ${data.error}`)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface ScheduleGeneratorProps {
|
||||
tournamentId: number
|
||||
@@ -9,6 +10,7 @@ interface ScheduleGeneratorProps {
|
||||
}
|
||||
|
||||
export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: ScheduleGeneratorProps) {
|
||||
const router = useRouter()
|
||||
const [isGenerating, setIsGenerating] = useState(false)
|
||||
const [error, setError] = useState("")
|
||||
const [result, setResult] = useState<{
|
||||
@@ -47,6 +49,9 @@ export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: S
|
||||
matchupsCreated: data.matchupsCreated,
|
||||
})
|
||||
setIsGenerating(false)
|
||||
|
||||
// Re-fetch the schedule data from the server
|
||||
router.refresh()
|
||||
} catch {
|
||||
setError("An error occurred. Please try again.")
|
||||
setIsGenerating(false)
|
||||
@@ -81,7 +86,7 @@ export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: S
|
||||
return
|
||||
}
|
||||
|
||||
window.location.reload()
|
||||
router.refresh()
|
||||
} catch {
|
||||
setError("An error occurred. Please try again.")
|
||||
setIsGenerating(false)
|
||||
|
||||
Reference in New Issue
Block a user