Commit 62db8358 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(tournaments): let the manager drive the pairing engine

api/cron.php now accepts the Supabase service key as an alternative
credential, so the management app can run the engine without a second
shared secret having to be provisioned on two CapRover apps first, and
takes an optional tournament_id so an organiser pressing Start gets the
answer now instead of at the next sweep.

The sweep also stops skipping tournaments with no round. Flipping a
tournament to in_progress without pairing round one left it registered,
running and permanently unplayable; the sweep now opens the round.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 929263b6
......@@ -24,17 +24,36 @@ require_once __DIR__ . '/../includes/chess.php';
require_once __DIR__ . '/../includes/tournament-engine.php';
// Authenticated by a shared secret rather than a user token, so a scheduler can
// call it with no session. When no secret is configured the endpoint refuses to
// run rather than defaulting to open.
$secret = $_GET['secret'] ?? ($_SERVER['HTTP_X_CRON_SECRET'] ?? '');
if (CRON_SECRET === '' || !hash_equals(CRON_SECRET, (string)$secret)) {
// call it with no session. Two credentials are accepted, and either alone is
// enough:
//
// CRON_SECRET — for an external pinger, via ?secret= or X-Cron-Secret.
// service key — via X-Service-Key, for the management app. Both apps are
// already issued the same SUPABASE_SERVICE_KEY, so the manager
// can drive the engine without a second secret having to be
// provisioned on two CapRover apps first.
//
// When neither is configured the endpoint refuses rather than defaulting to open.
$cronSecret = (string)($_GET['secret'] ?? ($_SERVER['HTTP_X_CRON_SECRET'] ?? ''));
$serviceKey = (string)($_SERVER['HTTP_X_SERVICE_KEY'] ?? '');
$authorised =
(CRON_SECRET !== '' && $cronSecret !== '' && hash_equals(CRON_SECRET, $cronSecret)) ||
(SUPABASE_SERVICE_KEY !== '' && $serviceKey !== '' && hash_equals(SUPABASE_SERVICE_KEY, $serviceKey));
if (!$authorised) {
http_response_code(403);
echo json_encode(['error' => 'forbidden']);
exit;
}
// A caller may scope the tick to one tournament — the management app does this
// when an organiser presses Start or Generate Round and wants the answer now,
// rather than whenever the next sweep happens to come round.
$onlyTournament = trim((string)($_GET['tournament_id'] ?? ''));
$db = supabaseService();
$report = ['started' => [], 'advanced' => [], 'forfeited' => [], 'flagged' => [], 'errors' => []];
$report = ['started' => [], 'paired' => [], 'advanced' => [], 'forfeited' => [], 'flagged' => [], 'errors' => []];
// How long a player has to appear for their game before losing it by no-show.
$NO_SHOW_SECONDS = tournamentNoShowSeconds();
......@@ -42,12 +61,14 @@ $NO_SHOW_SECONDS = tournamentNoShowSeconds();
// ---------------------------------------------------------------------------
// 1. Start tournaments that are due
// ---------------------------------------------------------------------------
$due = $db->get('el3ab_tournaments', [
$dueFilter = [
'status' => 'in.(' . TOURNAMENT_STATUS_REGISTRATION . ',' . TOURNAMENT_STATUS_DRAFT . ')',
'auto_start' => 'is.true',
'select' => 'id,name,starts_at,min_players',
'limit' => 50,
]);
];
if ($onlyTournament !== '') $dueFilter['id'] = 'eq.' . $onlyTournament;
$due = $db->get('el3ab_tournaments', $dueFilter);
if (is_array($due) && !isset($due['error'])) {
foreach ($due as $t) {
$startsAt = $t['starts_at'] ?? null;
......@@ -65,18 +86,34 @@ if (is_array($due) && !isset($due['error'])) {
// ---------------------------------------------------------------------------
// 2. Running tournaments: flag dead clocks, forfeit no-shows, advance rounds
// ---------------------------------------------------------------------------
$running = $db->get('el3ab_tournaments', [
$runFilter = [
'status' => 'eq.' . TOURNAMENT_STATUS_IN_PROGRESS,
'select' => '*',
'limit' => 50,
]);
];
if ($onlyTournament !== '') $runFilter['id'] = 'eq.' . $onlyTournament;
$running = $db->get('el3ab_tournaments', $runFilter);
if (is_array($running) && !isset($running['error'])) {
foreach ($running as $t) {
$roundNumber = (int)($t['current_round'] ?? 0);
if ($roundNumber < 1) continue;
$round = $roundNumber >= 1 ? tournamentRound($db, $t['id'], $roundNumber) : null;
// Started but never paired. The management app's Start button flips the
// status directly, and for a tournament this engine owns nothing else was
// ever going to pair round 1 — registered, started, and permanently
// unplayable. Open the round rather than skipping past the deadlock.
if (!$round) {
$opened = tournamentOpenRound($db, $t, max(1, $roundNumber));
if (!$opened['ok']) {
$report['errors'][] = ['id' => $t['id'], 'stage' => 'open_round', 'error' => $opened['error']];
continue;
}
$round = $opened['round'];
$roundNumber = (int)$round['round_number'];
$report['paired'][] = ['id' => $t['id'], 'round' => $roundNumber, 'boards' => count($round['pairings'] ?? [])];
}
$round = tournamentRound($db, $t['id'], $roundNumber);
if (!$round || $round['status'] !== ROUND_STATUS_IN_PROGRESS) continue;
if (($round['status'] ?? '') !== ROUND_STATUS_IN_PROGRESS) continue;
$roundStarted = strtotime($round['started_at'] ?? 'now');
......
import puppeteer from 'puppeteer';
const b=await puppeteer.launch({executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',headless:'new',args:['--no-sandbox']});
const p=await b.newPage();
await p.setViewport({width:process.env.W?+process.env.W:390,height:process.env.H?+process.env.H:1400,deviceScaleFactor:2});
await p.goto(process.env.URL,{waitUntil:'networkidle2',timeout:45000});
await new Promise(r=>setTimeout(r,2000));
await p.screenshot({path:process.env.OUT});
await b.close();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment