diff --git a/src/app/api/cron/round-scheduler/route.ts b/src/app/api/cron/round-scheduler/route.ts deleted file mode 100644 index 256f583..0000000 --- a/src/app/api/cron/round-scheduler/route.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { NextResponse } from 'next/server' -import type { NextRequest } from 'next/server' -import { prisma } from '@/lib/prisma' -import { activateRound, closeRound } from '@/server/services/round-engine' - -export async function GET(request: NextRequest): Promise { - const cronSecret = request.headers.get('x-cron-secret') - - if (!cronSecret || cronSecret !== process.env.CRON_SECRET) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - try { - const now = new Date() - const results: Array<{ roundId: string; action: string; success: boolean; errors?: string[] }> = [] - - // Find a SUPER_ADMIN to use as the actor for audit logging - const systemActor = await prisma.user.findFirst({ - where: { role: 'SUPER_ADMIN' }, - select: { id: true }, - }) - - if (!systemActor) { - return NextResponse.json({ - ok: false, - error: 'No SUPER_ADMIN user found for system actions', - }, { status: 500 }) - } - - // 1. Activate DRAFT rounds whose windowOpenAt has arrived - const roundsToOpen = await prisma.round.findMany({ - where: { - status: 'ROUND_DRAFT', - windowOpenAt: { lte: now }, - competition: { status: { not: 'ARCHIVED' } }, - }, - select: { id: true, name: true }, - }) - - for (const round of roundsToOpen) { - const result = await activateRound(round.id, systemActor.id, prisma) - results.push({ - roundId: round.id, - action: `activate: ${round.name}`, - success: result.success, - errors: result.errors, - }) - } - - // 2. Close ACTIVE rounds whose windowCloseAt has passed - const roundsToClose = await prisma.round.findMany({ - where: { - status: 'ROUND_ACTIVE', - windowCloseAt: { lte: now }, - }, - select: { id: true, name: true }, - }) - - for (const round of roundsToClose) { - const result = await closeRound(round.id, systemActor.id, prisma) - results.push({ - roundId: round.id, - action: `close: ${round.name}`, - success: result.success, - errors: result.errors, - }) - } - - return NextResponse.json({ - ok: true, - processed: results.length, - results, - timestamp: now.toISOString(), - }) - } catch (error) { - console.error('Cron round-scheduler failed:', error) - return NextResponse.json( - { error: 'Internal server error' }, - { status: 500 } - ) - } -} diff --git a/src/instrumentation.ts b/src/instrumentation.ts index 50ee69d..5aa4bd1 100644 --- a/src/instrumentation.ts +++ b/src/instrumentation.ts @@ -25,5 +25,19 @@ export async function register() { }) }) .catch(() => {}) + + // Round scheduler: check every 60s for rounds that need to open/close + import('./server/services/round-scheduler') + .then(({ processScheduledRounds }) => { + console.log('[Startup] Round scheduler started (60s interval)') + // Run once immediately, then every 60 seconds + processScheduledRounds().catch(() => {}) + setInterval(() => { + processScheduledRounds().catch((err) => { + console.error('[RoundScheduler] Error:', err) + }) + }, 60_000) + }) + .catch(() => {}) } } diff --git a/src/server/services/round-scheduler.ts b/src/server/services/round-scheduler.ts new file mode 100644 index 0000000..de94754 --- /dev/null +++ b/src/server/services/round-scheduler.ts @@ -0,0 +1,67 @@ +import { prisma } from '@/lib/prisma' +import { activateRound, closeRound } from './round-engine' + +/** + * Check for rounds that need to be automatically opened or closed + * based on their windowOpenAt / windowCloseAt timestamps. + * Called on a 60-second interval from instrumentation.ts. + */ +export async function processScheduledRounds(): Promise<{ + activated: number + closed: number +}> { + const now = new Date() + let activated = 0 + let closed = 0 + + // Find a SUPER_ADMIN to use as the actor for audit logging + const systemActor = await prisma.user.findFirst({ + where: { role: 'SUPER_ADMIN' }, + select: { id: true }, + }) + + if (!systemActor) { + return { activated, closed } + } + + // 1. Activate DRAFT rounds whose windowOpenAt has arrived + const roundsToOpen = await prisma.round.findMany({ + where: { + status: 'ROUND_DRAFT', + windowOpenAt: { lte: now }, + competition: { status: { not: 'ARCHIVED' } }, + }, + select: { id: true, name: true }, + }) + + for (const round of roundsToOpen) { + const result = await activateRound(round.id, systemActor.id, prisma) + if (result.success) { + activated++ + console.log(`[RoundScheduler] Activated round: ${round.name}`) + } else { + console.warn(`[RoundScheduler] Failed to activate "${round.name}":`, result.errors) + } + } + + // 2. Close ACTIVE rounds whose windowCloseAt has passed + const roundsToClose = await prisma.round.findMany({ + where: { + status: 'ROUND_ACTIVE', + windowCloseAt: { lte: now }, + }, + select: { id: true, name: true }, + }) + + for (const round of roundsToClose) { + const result = await closeRound(round.id, systemActor.id, prisma) + if (result.success) { + closed++ + console.log(`[RoundScheduler] Closed round: ${round.name}`) + } else { + console.warn(`[RoundScheduler] Failed to close "${round.name}":`, result.errors) + } + } + + return { activated, closed } +}