From caefb0dcc08aa954e6c9e01174b59e1e0342cb3a Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Fri, 1 May 2026 16:39:50 -0700 Subject: [PATCH] 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 --- docs/TROUBLESHOOTING_SCHEDULE_GENERATION.md | 343 ++++++++++++++++++ e2e/cucumber/step-definitions/auth-steps.ts | 52 ++- e2e/cucumber/step-definitions/common-steps.ts | 2 +- e2e/cucumber/support/hooks.ts | 2 +- e2e/cucumber/support/world.ts | 2 + justfile | 8 +- .../admin/tournaments/[id]/schedule/page.tsx | 5 + .../api/tournaments/[id]/schedule/route.ts | 46 ++- src/components/MatchEditor.tsx | 6 +- src/components/RecalculateEloButton.tsx | 4 +- src/components/ScheduleGenerator.tsx | 7 +- 11 files changed, 454 insertions(+), 23 deletions(-) create mode 100644 docs/TROUBLESHOOTING_SCHEDULE_GENERATION.md diff --git a/docs/TROUBLESHOOTING_SCHEDULE_GENERATION.md b/docs/TROUBLESHOOTING_SCHEDULE_GENERATION.md new file mode 100644 index 0000000..ed614d0 --- /dev/null +++ b/docs/TROUBLESHOOTING_SCHEDULE_GENERATION.md @@ -0,0 +1,343 @@ +# Technical Findings: Next.js App Router Data Staleness in Production + +## Issue Summary + +**Problem**: Freshly generated database data (TournamentRound and BracketMatchup records) created via POST `/api/tournaments/[id]/schedule` fails to appear immediately after a browser refresh in production builds, despite the server component having `revalidate = 0` and `dynamic = "force-dynamic"`. + +**Context**: The test suite `schedule-tab.test.ts` shows that data is created successfully in the database but the page refresh doesn't immediately display the new data in production builds. + +--- + +## Root Cause Analysis + +### 1. Next.js Data Cache Behavior + +**Finding**: Next.js App Router caches `fetch` responses by default in production. While `revalidate = 0` and `dynamic = "force-dynamic"` disable full-route caching, they do not automatically disable the Data Cache for individual `fetch` requests. + +**Evidence from codebase**: +- `src/app/admin/tournaments/[id]/schedule/page.tsx` sets: + ```typescript + export const dynamic = "force-dynamic" + export const revalidate = 0 + ``` +- However, the page uses Prisma directly, not `fetch`. The page query `prisma.event.findUnique` is not subject to Next.js fetch caching, but the **browser/client router cache** may still cause issues. + +**Relevant Code Locations**: +- `src/app/admin/tournaments/[id]/schedule/page.tsx:14-16` +- `src/app/api/tournaments/[id]/schedule/route.ts:191-222` (POST transaction) + +### 2. Prisma Client and Transaction Isolation + +**Finding**: The POST endpoint uses `prisma.$transaction` to create rounds and matchups. In production with PostgreSQL, transaction isolation levels and connection pooling can cause visibility delays. + +**Evidence**: +```typescript +// src/app/api/tournaments/[id]/schedule/route.ts:191 +const created = await prisma.$transaction( + schedule.map((round) => + prisma.tournamentRound.create({...}) + ) +) +``` + +**Potential Issues**: +- **Read Committed Isolation**: PostgreSQL's default `READ COMMITTED` isolation level ensures that once a transaction commits, subsequent queries see the new data. However, if the browser refresh happens immediately after the POST response, there might be a race condition. +- **Connection Pooling**: The Prisma client uses connection pooling. If the GET request (page load) uses a different connection than the POST request, and there's a replication delay (unlikely with SQLite/PostgreSQL single instance), it could see stale data. + +**Evidence Locations**: +- `src/lib/prisma.ts:13-35` (Prisma client initialization) +- `src/app/api/tournaments/[id]/schedule/route.ts:191-222` (Transaction block) + +### 3. Client-Side Router Cache + +**Finding**: The Next.js App Router maintains a client-side cache for visited routes. Even when the server component revalidates, the client might serve a cached version from the client-side navigation cache. + +**Evidence from research**: +- The GitHub discussion #51612 shows that `router.push` and browser refresh can still serve stale data due to client-side caching. +- The `ScheduleGenerator` component uses `fetch` to POST data but doesn't trigger a router refresh or invalidate the client cache. + +**Code Locations**: +- `src/components/ScheduleGenerator.tsx:27-29` (POST request) +- `src/components/ScheduleGenerator.tsx:84` (Only calls `window.location.reload()` on DELETE, not POST) + +### 4. Production vs Development Differences + +**Finding**: Development mode (`next dev`) has more lenient caching behavior. Production builds (`next start`) aggressively cache by default. + +**Evidence**: +- The test `schedule-tab.test.ts` passes in development but fails in production. +- The `ScheduleGenerator` component doesn't use `revalidatePath` or `revalidateTag` after successful POST. + +--- + +## Specific Technical Findings + +### Finding 1: Missing Cache Invalidation After POST + +**Location**: `src/components/ScheduleGenerator.tsx:43-49` + +**Issue**: After a successful POST request, the component updates local state (`result`) but doesn't: +1. Call `revalidatePath` (requires Server Action) +2. Call `revalidateTag` (requires Server Action) +3. Trigger a router refresh +4. Force a page reload + +**Current Behavior**: +```typescript +const handleGenerate = async () => { + // ... POST request ... + const data = await response.json() + setResult({ + roundsCreated: data.roundsCreated, + matchupsCreated: data.matchupsCreated, + }) + setIsGenerating(false) + // ❌ No cache invalidation +} +``` + +**Expected Behavior**: After POST, the page should re-fetch data to show newly created rounds. + +### Finding 2: Prisma Client Singleton Pattern + +**Location**: `src/lib/prisma.ts:37-39` + +**Issue**: The Prisma client is a singleton, which is correct. However, in production with connection pooling, there might be delays in visibility across connections. + +**Current Code**: +```typescript +export const prisma = globalForPrisma.prisma ?? createPrismaClient() + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma +``` + +**Note**: This is correct pattern, but production connection pooling behavior differs from development. + +### Finding 3: Server Component Data Fetching + +**Location**: `src/app/admin/tournaments/[id]/schedule/page.tsx:26-50` + +**Issue**: The server component fetches data directly with Prisma. While `revalidate = 0` ensures the server re-renders on each request, the client might cache the response. + +**Current Code**: +```typescript +export const dynamic = "force-dynamic" +export const revalidate = 0 + +export default async function TournamentSchedulePage({ params }: PageProps) { + const tournament = await prisma.event.findUnique({ + where: { id: tournamentId }, + include: { rounds: { ... } } + }) + // ... +} +``` + +**Note**: This should work correctly, but client-side router cache might interfere. + +--- + +## Potential Fixes + +### Fix 1: Implement Server Actions for Cache Invalidation + +**Approach**: Convert the schedule generation to use Server Actions with `revalidatePath`. + +**Implementation**: +```typescript +// src/app/actions/schedule.ts +'use server' + +import { revalidatePath } from 'next/cache' +import { prisma } from '@/lib/prisma' +import { generateRoundRobin, /* ... */ } from '@/lib/schedule-generator' + +export async function generateSchedule(tournamentId: number) { + // ... existing logic from route.ts ... + + // After successful creation + await prisma.$transaction(/* ... */) + + // Revalidate the schedule page + revalidatePath(`/admin/tournaments/${tournamentId}/schedule`) + revalidatePath(`/admin/tournaments/${tournamentId}`) + + return { success: true, roundsCreated: created.length } +} +``` + +**Update ScheduleGenerator component**: +```typescript +// src/components/ScheduleGenerator.tsx +import { generateSchedule } from '@/app/actions/schedule' + +const handleGenerate = async () => { + const result = await generateSchedule(tournamentId) + if (result.success) { + setResult({ + roundsCreated: result.roundsCreated, + matchupsCreated: /* calculate from result */, + }) + // Router automatically revalidates due to revalidatePath + } +} +``` + +### Fix 2: Force Router Refresh After POST + +**Approach**: Use `router.refresh()` after successful POST to invalidate client cache. + +**Implementation**: +```typescript +// src/components/ScheduleGenerator.tsx +'use client' + +import { useRouter } from 'next/navigation' + +export function ScheduleGenerator({ tournamentId, /* ... */ }) { + const router = useRouter() + + const handleGenerate = async () => { + // ... POST request ... + + if (response.ok) { + // Force router to re-fetch server component data + router.refresh() + + // Or force full page reload as fallback + // window.location.reload() + } + } +} +``` + +### Fix 3: Disable Fetch Caching Explicitly + +**Approach**: Even though we use Prisma, ensure any internal fetches don't cache. + +**Implementation**: +```typescript +// src/app/api/tournaments/[id]/schedule/route.ts +export async function GET(request: Request, { params }: RouteParams) { + // Add cache control headers + const response = NextResponse.json({ rounds: tournament.rounds }) + response.headers.set('Cache-Control', 'no-store, max-age=0') + return response +} +``` + +### Fix 4: Add Delay/Retry Logic in Tests + +**Approach**: For Playwright tests, add explicit wait for data visibility. + +**Implementation**: +```typescript +// e2e/schedule-tab.test.ts +test('Schedule page displays generated rounds and matchups', async ({ page }) => { + // ... navigate to schedule page ... + + // Wait for rounds to be visible with retry logic + await expect(page.locator('text=Round 1')).toBeVisible({ timeout: 10000 }) + + // Additional verification + await expect(page.locator('text=Alice + Bob')).toBeVisible() +}) +``` + +### Fix 5: Database Transaction Optimization + +**Approach**: Ensure transaction commits fully before returning response. + +**Implementation**: +```typescript +// src/app/api/tournaments/[id]/schedule/route.ts +const created = await prisma.$transaction( + schedule.map((round) => + prisma.tournamentRound.create({ + data: { /* ... */ }, + include: { /* ... */ } // Eager load to ensure data is available + }) + ), + { + isolationLevel: 'ReadCommitted', // Explicit isolation level + maxWait: 5000, // Increase wait time + timeout: 10000, // Increase timeout + } +) +``` + +--- + +## Recommended Solution + +### Immediate Fix (Quick) + +1. **Update `ScheduleGenerator.tsx`** to use `router.refresh()` after POST: + ```typescript + import { useRouter } from 'next/navigation' + + const router = useRouter() + + const handleGenerate = async () => { + // ... POST logic ... + if (response.ok) { + router.refresh() + } + } + ``` + +2. **Add cache control headers** to the GET endpoint: + ```typescript + // In GET handler + const response = NextResponse.json({ rounds: tournament.rounds }) + response.headers.set('Cache-Control', 'no-store, max-age=0') + return response + ``` + +### Long-term Fix (Recommended) + +1. **Migrate to Server Actions** for schedule generation: + - Use `'use server'` directive + - Call `revalidatePath` after mutations + - Eliminate need for separate API route + +2. **Implement proper cache tagging**: + - Tag fetch requests with `next: { tags: ['schedule'] }` + - Use `revalidateTag('schedule')` after mutations + +3. **Update test patterns**: + - Ensure tests wait for server component revalidation + - Use `page.waitForLoadState('networkidle')` after mutations + +--- + +## Verification Steps + +1. **Test in production build**: + ```bash + npm run build + npm run start + ``` + +2. **Verify data flow**: + - Create schedule via UI + - Refresh page immediately + - Verify rounds display correctly + +3. **Check server logs**: + - Look for revalidation messages + - Verify Prisma query execution + +4. **Run acceptance tests**: + ```bash + npm run test:acceptance + ``` + +--- + +## References + +- Next.js App Router Caching: https://nextjs.org/docs/app/building-your-application/data-fetching/caching +- Server Actions: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations +- GitHub Discussion #51612: https://github.com/vercel/next.js/discussions/51612 +- Prisma Transactions: https://www.prisma.io/docs/orm/prisma-client/queries/transactions diff --git a/e2e/cucumber/step-definitions/auth-steps.ts b/e2e/cucumber/step-definitions/auth-steps.ts index e23d2cd..7764c06 100644 --- a/e2e/cucumber/step-definitions/auth-steps.ts +++ b/e2e/cucumber/step-definitions/auth-steps.ts @@ -108,16 +108,10 @@ Given('I am logged in as a player', async function () { /** * Precondition: I am logged in as a tournament admin * Note: In the actual app, admin roles are assigned by club admins or via API. - * For acceptance tests, we'll use the default player role and test admin features - * as the dev site would handle them. + * For acceptance tests, we'll assign the tournament_admin role directly via Prisma. */ Given('I am logged in as a tournament admin', async function () { - console.log('🌍 Creating and logging in as a player (tournament admin role is assigned via UI/API)...'); - // For now, use the same flow as player - // In real usage, the admin would either: - // 1. Be pre-created on the dev site - // 2. Have role assigned via API - // 3. Use the admin dashboard to manage users + console.log('🌍 Creating and logging in as a tournament admin...'); const credentials = generateTestCredentials(); world.user = credentials; @@ -133,6 +127,34 @@ Given('I am logged in as a tournament admin', async function () { // Wait for redirect await world.page.waitForURL(/\/players\/\d+\/profile/, { timeout: 15000 }); + // Extract user ID from the URL (e.g., /players/2147/profile) + const currentUrl = world.page.url(); + const match = currentUrl.match(/\/players\/(\d+)\/profile/); + if (match) { + const playerId = match[1]; + world.playerId = playerId; + + // Get the user ID from the database + const prisma = await world.getPrisma(); + const player = await prisma.player.findUnique({ + where: { id: parseInt(playerId) }, + include: { user: true } + }); + + if (player && player.user) { + const userId = player.user.id; + (world.user as any).id = userId; + console.log(`🌍 User ID extracted: ${userId}`); + + // Assign tournament_admin role to the user + await prisma.user.update({ + where: { id: userId }, + data: { role: 'tournament_admin' } + }); + console.log(`🌍 Assigned tournament_admin role to user: ${userId}`); + } + } + console.log(`🌍 User created: ${credentials.email}`); }); @@ -359,11 +381,18 @@ Given('a tournament exists with {int} teams', async function (teamCount: number) const prisma = await world.getPrisma(); const timestamp = Date.now(); + // Get the current user ID for ownership + const userId = world.user?.id; + if (!userId) { + throw new Error('User ID not found. Ensure user is logged in before creating tournament.'); + } + // Always create a new tournament for test isolation const tournament = await prisma.event.create({ data: { name: `Test Tournament ${timestamp}`, createdAt: new Date(), + ownerId: userId, // Set the owner to the current user }, }); @@ -409,11 +438,18 @@ Given('a tournament has a generated schedule', async function () { const prisma = await world.getPrisma(); const timestamp = Date.now(); + // Get the current user ID for ownership + const userId = world.user?.id; + if (!userId) { + throw new Error('User ID not found. Ensure user is logged in before creating tournament.'); + } + // Create a tournament const tournament = await prisma.event.create({ data: { name: `Test Schedule Tournament ${timestamp}`, createdAt: new Date(), + ownerId: userId, // Set the owner to the current user }, }); diff --git a/e2e/cucumber/step-definitions/common-steps.ts b/e2e/cucumber/step-definitions/common-steps.ts index aee9bf2..8164b2d 100644 --- a/e2e/cucumber/step-definitions/common-steps.ts +++ b/e2e/cucumber/step-definitions/common-steps.ts @@ -104,7 +104,7 @@ When('I go back', async function () { When('I refresh the page', async function () { console.log('🌍 About to refresh page from URL:', world.page.url()); - await world.page.reload({ waitUntil: 'domcontentloaded' }); + await world.page.reload({ waitUntil: 'networkidle' }); console.log('🌍 Page refreshed, new URL:', world.page.url()); // Wait extra time for full render await world.page.waitForTimeout(2000); diff --git a/e2e/cucumber/support/hooks.ts b/e2e/cucumber/support/hooks.ts index 382de8a..e4fcbf6 100644 --- a/e2e/cucumber/support/hooks.ts +++ b/e2e/cucumber/support/hooks.ts @@ -141,7 +141,7 @@ After(async function () { select: { id: true } }); - const tournamentIds = testTournaments.map(t => t.id); + const tournamentIds = testTournaments.map((t: { id: number }) => t.id); if (tournamentIds.length > 0) { // Delete bracket matchups via Prisma diff --git a/e2e/cucumber/support/world.ts b/e2e/cucumber/support/world.ts index 66f4001..f1c5bd6 100644 --- a/e2e/cucumber/support/world.ts +++ b/e2e/cucumber/support/world.ts @@ -11,6 +11,7 @@ export interface WorldState { prisma: any; // Lazy-loaded PrismaClient baseURL: string; user?: { + id?: string; email: string; name: string; password: string; @@ -32,6 +33,7 @@ export class World implements WorldState { prisma: any; baseURL: string; user?: { + id?: string; email: string; name: string; password: string; diff --git a/justfile b/justfile index eb0a972..2f188af 100644 --- a/justfile +++ b/justfile @@ -85,17 +85,23 @@ test-acceptance-postgres: # Run Cucumber e2e tests with SQLite test-cucumber-sqlite: + @echo "Clearing Next.js cache..." + rm -rf .next/ @echo "Running Cucumber e2e tests with SQLite..." DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance:cucumber # Run Cucumber e2e tests with PostgreSQL (uses .env.development) test-cucumber-postgres: + @echo "Clearing Next.js cache..." + rm -rf .next/ @echo "Running Cucumber e2e tests with PostgreSQL..." npm run test:acceptance:cucumber # Run Cucumber e2e tests with PostgreSQL against production build # This is more reliable than dev server (no HMR, faster API responses) test-cucumber-postgres-prod: + @echo "Clearing Next.js cache..." + rm -rf .next/ @echo "Building application for production..." bun run build @echo "Starting production server in background..." @@ -211,7 +217,7 @@ help: # Clean up project (remove node_modules, build artifacts) clean: @echo "Cleaning project..." - rm -rf node_modules .next dist + rm -rf node_modules .next dist .turbo # Generate Prisma client prisma-generate: diff --git a/src/app/admin/tournaments/[id]/schedule/page.tsx b/src/app/admin/tournaments/[id]/schedule/page.tsx index 0980578..77482dc 100644 --- a/src/app/admin/tournaments/[id]/schedule/page.tsx +++ b/src/app/admin/tournaments/[id]/schedule/page.tsx @@ -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() diff --git a/src/app/api/tournaments/[id]/schedule/route.ts b/src/app/api/tournaments/[id]/schedule/route.ts index cd8866e..b5599b1 100644 --- a/src/app/api/tournaments/[id]/schedule/route.ts +++ b/src/app/api/tournaments/[id]/schedule/route.ts @@ -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, diff --git a/src/components/MatchEditor.tsx b/src/components/MatchEditor.tsx index 368bc55..74383ce 100644 --- a/src/components/MatchEditor.tsx +++ b/src/components/MatchEditor.tsx @@ -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.") diff --git a/src/components/RecalculateEloButton.tsx b/src/components/RecalculateEloButton.tsx index dded632..a125f45 100644 --- a/src/components/RecalculateEloButton.tsx +++ b/src/components/RecalculateEloButton.tsx @@ -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}`) } diff --git a/src/components/ScheduleGenerator.tsx b/src/components/ScheduleGenerator.tsx index 02c7b3f..8622593 100644 --- a/src/components/ScheduleGenerator.tsx +++ b/src/components/ScheduleGenerator.tsx @@ -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)