Move round scheduler in-app via instrumentation.ts, remove cron endpoint
All checks were successful
Build and Push Docker Image / build (push) Successful in 18s
All checks were successful
Build and Push Docker Image / build (push) Successful in 18s
Round open/close scheduling now runs as a 60s setInterval inside the app process (via instrumentation.ts register hook) instead of needing an external crontab. Removed the /api/cron/round-scheduler endpoint. - DRAFT rounds auto-activate when windowOpenAt arrives - ACTIVE rounds auto-close when windowCloseAt passes - Uses existing activateRound/closeRound from round-engine Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<NextResponse> {
|
|
||||||
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 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,5 +25,19 @@ export async function register() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.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(() => {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
67
src/server/services/round-scheduler.ts
Normal file
67
src/server/services/round-scheduler.ts
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user