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:
2026-05-01 16:39:50 -07:00
parent 9353ab1edc
commit caefb0dcc0
11 changed files with 454 additions and 23 deletions
+343
View File
@@ -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
+44 -8
View File
@@ -108,16 +108,10 @@ Given('I am logged in as a player', async function () {
/** /**
* Precondition: I am logged in as a tournament admin * Precondition: I am logged in as a tournament admin
* Note: In the actual app, admin roles are assigned by club admins or via API. * 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 * For acceptance tests, we'll assign the tournament_admin role directly via Prisma.
* as the dev site would handle them.
*/ */
Given('I am logged in as a tournament admin', async function () { 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)...'); console.log('🌍 Creating and logging in as a tournament admin...');
// 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
const credentials = generateTestCredentials(); const credentials = generateTestCredentials();
world.user = credentials; world.user = credentials;
@@ -133,6 +127,34 @@ Given('I am logged in as a tournament admin', async function () {
// Wait for redirect // Wait for redirect
await world.page.waitForURL(/\/players\/\d+\/profile/, { timeout: 15000 }); 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}`); 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 prisma = await world.getPrisma();
const timestamp = Date.now(); 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 // Always create a new tournament for test isolation
const tournament = await prisma.event.create({ const tournament = await prisma.event.create({
data: { data: {
name: `Test Tournament ${timestamp}`, name: `Test Tournament ${timestamp}`,
createdAt: new Date(), 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 prisma = await world.getPrisma();
const timestamp = Date.now(); 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 // Create a tournament
const tournament = await prisma.event.create({ const tournament = await prisma.event.create({
data: { data: {
name: `Test Schedule Tournament ${timestamp}`, name: `Test Schedule Tournament ${timestamp}`,
createdAt: new Date(), createdAt: new Date(),
ownerId: userId, // Set the owner to the current user
}, },
}); });
@@ -104,7 +104,7 @@ When('I go back', async function () {
When('I refresh the page', async function () { When('I refresh the page', async function () {
console.log('🌍 About to refresh page from URL:', world.page.url()); 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()); console.log('🌍 Page refreshed, new URL:', world.page.url());
// Wait extra time for full render // Wait extra time for full render
await world.page.waitForTimeout(2000); await world.page.waitForTimeout(2000);
+1 -1
View File
@@ -141,7 +141,7 @@ After(async function () {
select: { id: true } select: { id: true }
}); });
const tournamentIds = testTournaments.map(t => t.id); const tournamentIds = testTournaments.map((t: { id: number }) => t.id);
if (tournamentIds.length > 0) { if (tournamentIds.length > 0) {
// Delete bracket matchups via Prisma // Delete bracket matchups via Prisma
+2
View File
@@ -11,6 +11,7 @@ export interface WorldState {
prisma: any; // Lazy-loaded PrismaClient prisma: any; // Lazy-loaded PrismaClient
baseURL: string; baseURL: string;
user?: { user?: {
id?: string;
email: string; email: string;
name: string; name: string;
password: string; password: string;
@@ -32,6 +33,7 @@ export class World implements WorldState {
prisma: any; prisma: any;
baseURL: string; baseURL: string;
user?: { user?: {
id?: string;
email: string; email: string;
name: string; name: string;
password: string; password: string;
+7 -1
View File
@@ -85,17 +85,23 @@ test-acceptance-postgres:
# Run Cucumber e2e tests with SQLite # Run Cucumber e2e tests with SQLite
test-cucumber-sqlite: test-cucumber-sqlite:
@echo "Clearing Next.js cache..."
rm -rf .next/
@echo "Running Cucumber e2e tests with SQLite..." @echo "Running Cucumber e2e tests with SQLite..."
DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance:cucumber DATABASE_PROVIDER=sqlite DATABASE_URL=file:./prisma/ci.db npm run test:acceptance:cucumber
# Run Cucumber e2e tests with PostgreSQL (uses .env.development) # Run Cucumber e2e tests with PostgreSQL (uses .env.development)
test-cucumber-postgres: test-cucumber-postgres:
@echo "Clearing Next.js cache..."
rm -rf .next/
@echo "Running Cucumber e2e tests with PostgreSQL..." @echo "Running Cucumber e2e tests with PostgreSQL..."
npm run test:acceptance:cucumber npm run test:acceptance:cucumber
# Run Cucumber e2e tests with PostgreSQL against production build # Run Cucumber e2e tests with PostgreSQL against production build
# This is more reliable than dev server (no HMR, faster API responses) # This is more reliable than dev server (no HMR, faster API responses)
test-cucumber-postgres-prod: test-cucumber-postgres-prod:
@echo "Clearing Next.js cache..."
rm -rf .next/
@echo "Building application for production..." @echo "Building application for production..."
bun run build bun run build
@echo "Starting production server in background..." @echo "Starting production server in background..."
@@ -211,7 +217,7 @@ help:
# Clean up project (remove node_modules, build artifacts) # Clean up project (remove node_modules, build artifacts)
clean: clean:
@echo "Cleaning project..." @echo "Cleaning project..."
rm -rf node_modules .next dist rm -rf node_modules .next dist .turbo
# Generate Prisma client # Generate Prisma client
prisma-generate: prisma-generate:
@@ -23,6 +23,7 @@ export default async function TournamentSchedulePage({ params }: PageProps) {
notFound() notFound()
} }
console.log(`[Schedule Page] Fetching tournament ${tournamentId}`);
const tournament = await prisma.event.findUnique({ const tournament = await prisma.event.findUnique({
where: { id: tournamentId }, where: { id: tournamentId },
include: { 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) { if (!tournament) {
notFound() notFound()
+38 -8
View File
@@ -1,4 +1,5 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { canManageTournament } from "@/lib/permissions"; import { canManageTournament } from "@/lib/permissions";
import { generateRoundRobin, validateScheduleInput, generateVariableRoundRobin, expectedRounds } from "@/lib/schedule-generator"; 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. * Creates TournamentRound and BracketMatchup records.
*/ */
export async function POST(_request: Request, { params }: RouteParams) { export async function POST(_request: Request, { params }: RouteParams) {
console.log(`[Schedule API] POST handler started`);
try { try {
const { id } = await params; const { id } = await params;
const tournamentId = parseInt(id); const tournamentId = parseInt(id);
console.log(`[Schedule API] POST /api/tournaments/${tournamentId}/schedule`);
if (isNaN(tournamentId)) { if (isNaN(tournamentId)) {
console.log(`[Schedule API] Invalid tournament ID: ${id}`);
return NextResponse.json( return NextResponse.json(
{ error: "Invalid tournament ID" }, { error: "Invalid tournament ID" },
{ status: 400 } { status: 400 }
@@ -90,6 +95,7 @@ export async function POST(_request: Request, { params }: RouteParams) {
} }
const permission = await canManageTournament(tournamentId); const permission = await canManageTournament(tournamentId);
console.log(`[Schedule API] Permission check: ${permission.allowed}, reason: ${permission.reason}`);
if (!permission.allowed) { if (!permission.allowed) {
return NextResponse.json( return NextResponse.json(
{ error: permission.reason || "Not authorized to manage this tournament" }, { error: permission.reason || "Not authorized to manage this tournament" },
@@ -98,6 +104,7 @@ export async function POST(_request: Request, { params }: RouteParams) {
} }
// Check tournament exists // Check tournament exists
console.log(`[Schedule API] Looking up tournament ${tournamentId}`);
const tournament = await prisma.event.findUnique({ const tournament = await prisma.event.findUnique({
where: { id: tournamentId }, where: { id: tournamentId },
include: { include: {
@@ -111,14 +118,17 @@ export async function POST(_request: Request, { params }: RouteParams) {
}); });
if (!tournament) { if (!tournament) {
console.log(`[Schedule API] Tournament ${tournamentId} not found`);
return NextResponse.json( return NextResponse.json(
{ error: "Tournament not found" }, { error: "Tournament not found" },
{ status: 404 } { 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 // Check if schedule already exists and delete it
if (tournament.rounds.length > 0) { if (tournament.rounds.length > 0) {
console.log(`[Schedule API] Deleting ${tournament.rounds.length} existing rounds`);
// Delete existing rounds and matchups before regenerating // Delete existing rounds and matchups before regenerating
await prisma.bracketMatchup.deleteMany({ await prisma.bracketMatchup.deleteMany({
where: { eventId: tournamentId }, where: { eventId: tournamentId },
@@ -135,6 +145,8 @@ export async function POST(_request: Request, { params }: RouteParams) {
currentElo: p.player.currentElo, currentElo: p.player.currentElo,
})); }));
console.log(`[Schedule API] Got ${participants.length} participants`);
// Check minimum participants // Check minimum participants
if (participants.length < 2) { if (participants.length < 2) {
return NextResponse.json( return NextResponse.json(
@@ -147,20 +159,24 @@ export async function POST(_request: Request, { params }: RouteParams) {
const teamDurability = tournament.teamDurability || "permanent"; const teamDurability = tournament.teamDurability || "permanent";
const partnerRotation = (tournament.partnerRotation || "none") as 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based'; const partnerRotation = (tournament.partnerRotation || "none") as 'none' | 'minimize_repeat' | 'maximize_even' | 'elo_based';
const allowByes = tournament.allowByes ?? true; const allowByes = tournament.allowByes ?? true;
console.log(`[Schedule API] Team durability: ${teamDurability}, partner rotation: ${partnerRotation}, allow byes: ${allowByes}`);
// Determine number of teams from participants // Determine number of teams from participants
const tempResult = generateTeams(participants, partnerRotation, allowByes); const tempResult = generateTeams(participants, partnerRotation, allowByes);
const teamCount = tempResult.teams.length; const teamCount = tempResult.teams.length;
console.log(`[Schedule API] Generated ${teamCount} teams from ${participants.length} participants`);
if (teamCount < 2) { if (teamCount < 2) {
console.log(`[Schedule API] Not enough teams: ${teamCount}`);
return NextResponse.json( 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 } { status: 400 }
); );
} }
// Calculate number of rounds needed // Calculate expected rounds
const numRounds = expectedRounds(teamCount); const expectedRounds = expectedRounds(teamCount);
console.log(`[Schedule API] Expected rounds: ${expectedRounds}`);
if (teamDurability === "permanent") { if (teamDurability === "permanent") {
// ============================================ // ============================================
@@ -186,11 +202,14 @@ export async function POST(_request: Request, { params }: RouteParams) {
// Generate schedule using fixed teams // Generate schedule using fixed teams
const schedule = generateRoundRobin(teamPairings); const schedule = generateRoundRobin(teamPairings);
console.log(`[Schedule API] Generated ${schedule.length} rounds for ${teamCount} teams (fixed)`);
// Create rounds and matchups in a transaction // Create rounds and matchups in a transaction
console.log(`[Schedule API] About to create ${schedule.length} rounds in transaction`);
const created = await prisma.$transaction( const created = await prisma.$transaction(
schedule.map((round) => schedule.map((round) => {
prisma.tournamentRound.create({ console.log(`[Schedule API] Creating round ${round.roundNumber} with ${round.matchups.length} matchups`);
return prisma.tournamentRound.create({
data: { data: {
eventId: tournamentId, eventId: tournamentId,
roundNumber: round.roundNumber, 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({ return NextResponse.json({
success: true, success: true,
@@ -257,7 +285,7 @@ export async function POST(_request: Request, { params }: RouteParams) {
const schedule = generateVariableRoundRobin( const schedule = generateVariableRoundRobin(
participants, participants,
teamCount, teamCount,
numRounds, expectedRounds,
generateTeamWithRotation generateTeamWithRotation
); );
@@ -295,6 +323,8 @@ export async function POST(_request: Request, { params }: RouteParams) {
) )
); );
revalidatePath(`/admin/tournaments/${tournamentId}/schedule`);
return NextResponse.json({ return NextResponse.json({
success: true, success: true,
roundsCreated: created.length, roundsCreated: created.length,
+4 -2
View File
@@ -1,6 +1,7 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { useRouter } from "next/navigation"
import type { Player, Match } from "@prisma/client" import type { Player, Match } from "@prisma/client"
interface MatchEditorProps { interface MatchEditorProps {
@@ -40,6 +41,7 @@ export default function MatchEditor({
prefilledP4, prefilledP4,
prefilledRound, prefilledRound,
}: MatchEditorProps) { }: MatchEditorProps) {
const router = useRouter()
// Check if players are prefilled from URL params // Check if players are prefilled from URL params
const hasPrefilledPlayers = prefilledP1 && prefilledP2 && prefilledP3 && prefilledP4; const hasPrefilledPlayers = prefilledP1 && prefilledP2 && prefilledP3 && prefilledP4;
@@ -170,9 +172,9 @@ export default function MatchEditor({
isCasual: false, isCasual: false,
}) })
// Reload the page to show updated matches // Refresh the page to show updated matches
setTimeout(() => { setTimeout(() => {
window.location.reload() router.refresh()
}, 1000) }, 1000)
} catch { } catch {
setError("An error occurred. Please try again.") setError("An error occurred. Please try again.")
+3 -1
View File
@@ -1,8 +1,10 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { useRouter } from "next/navigation"
export function RecalculateEloButton() { export function RecalculateEloButton() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const handleClick = async () => { const handleClick = async () => {
@@ -33,7 +35,7 @@ export function RecalculateEloButton() {
if (data.success) { if (data.success) {
alert(`Recalculation completed: ${JSON.stringify(data.data)}`) alert(`Recalculation completed: ${JSON.stringify(data.data)}`)
window.location.reload() router.refresh()
} else { } else {
alert(`Error: ${data.error}`) alert(`Error: ${data.error}`)
} }
+6 -1
View File
@@ -1,6 +1,7 @@
"use client" "use client"
import { useState } from "react" import { useState } from "react"
import { useRouter } from "next/navigation"
interface ScheduleGeneratorProps { interface ScheduleGeneratorProps {
tournamentId: number tournamentId: number
@@ -9,6 +10,7 @@ interface ScheduleGeneratorProps {
} }
export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: ScheduleGeneratorProps) { export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: ScheduleGeneratorProps) {
const router = useRouter()
const [isGenerating, setIsGenerating] = useState(false) const [isGenerating, setIsGenerating] = useState(false)
const [error, setError] = useState("") const [error, setError] = useState("")
const [result, setResult] = useState<{ const [result, setResult] = useState<{
@@ -47,6 +49,9 @@ export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: S
matchupsCreated: data.matchupsCreated, matchupsCreated: data.matchupsCreated,
}) })
setIsGenerating(false) setIsGenerating(false)
// Re-fetch the schedule data from the server
router.refresh()
} catch { } catch {
setError("An error occurred. Please try again.") setError("An error occurred. Please try again.")
setIsGenerating(false) setIsGenerating(false)
@@ -81,7 +86,7 @@ export function ScheduleGenerator({ tournamentId, teamCount, existingRounds }: S
return return
} }
window.location.reload() router.refresh()
} catch { } catch {
setError("An error occurred. Please try again.") setError("An error occurred. Please try again.")
setIsGenerating(false) setIsGenerating(false)