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'; ...@@ -24,17 +24,36 @@ require_once __DIR__ . '/../includes/chess.php';
require_once __DIR__ . '/../includes/tournament-engine.php'; require_once __DIR__ . '/../includes/tournament-engine.php';
// Authenticated by a shared secret rather than a user token, so a scheduler can // 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 // call it with no session. Two credentials are accepted, and either alone is
// run rather than defaulting to open. // enough:
$secret = $_GET['secret'] ?? ($_SERVER['HTTP_X_CRON_SECRET'] ?? ''); //
if (CRON_SECRET === '' || !hash_equals(CRON_SECRET, (string)$secret)) { // 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); http_response_code(403);
echo json_encode(['error' => 'forbidden']); echo json_encode(['error' => 'forbidden']);
exit; 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(); $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. // How long a player has to appear for their game before losing it by no-show.
$NO_SHOW_SECONDS = tournamentNoShowSeconds(); $NO_SHOW_SECONDS = tournamentNoShowSeconds();
...@@ -42,12 +61,14 @@ $NO_SHOW_SECONDS = tournamentNoShowSeconds(); ...@@ -42,12 +61,14 @@ $NO_SHOW_SECONDS = tournamentNoShowSeconds();
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 1. Start tournaments that are due // 1. Start tournaments that are due
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
$due = $db->get('el3ab_tournaments', [ $dueFilter = [
'status' => 'in.(' . TOURNAMENT_STATUS_REGISTRATION . ',' . TOURNAMENT_STATUS_DRAFT . ')', 'status' => 'in.(' . TOURNAMENT_STATUS_REGISTRATION . ',' . TOURNAMENT_STATUS_DRAFT . ')',
'auto_start' => 'is.true', 'auto_start' => 'is.true',
'select' => 'id,name,starts_at,min_players', 'select' => 'id,name,starts_at,min_players',
'limit' => 50, 'limit' => 50,
]); ];
if ($onlyTournament !== '') $dueFilter['id'] = 'eq.' . $onlyTournament;
$due = $db->get('el3ab_tournaments', $dueFilter);
if (is_array($due) && !isset($due['error'])) { if (is_array($due) && !isset($due['error'])) {
foreach ($due as $t) { foreach ($due as $t) {
$startsAt = $t['starts_at'] ?? null; $startsAt = $t['starts_at'] ?? null;
...@@ -65,18 +86,34 @@ if (is_array($due) && !isset($due['error'])) { ...@@ -65,18 +86,34 @@ if (is_array($due) && !isset($due['error'])) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 2. Running tournaments: flag dead clocks, forfeit no-shows, advance rounds // 2. Running tournaments: flag dead clocks, forfeit no-shows, advance rounds
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
$running = $db->get('el3ab_tournaments', [ $runFilter = [
'status' => 'eq.' . TOURNAMENT_STATUS_IN_PROGRESS, 'status' => 'eq.' . TOURNAMENT_STATUS_IN_PROGRESS,
'select' => '*', 'select' => '*',
'limit' => 50, 'limit' => 50,
]); ];
if ($onlyTournament !== '') $runFilter['id'] = 'eq.' . $onlyTournament;
$running = $db->get('el3ab_tournaments', $runFilter);
if (is_array($running) && !isset($running['error'])) { if (is_array($running) && !isset($running['error'])) {
foreach ($running as $t) { foreach ($running as $t) {
$roundNumber = (int)($t['current_round'] ?? 0); $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['status'] ?? '') !== ROUND_STATUS_IN_PROGRESS) continue;
if (!$round || $round['status'] !== ROUND_STATUS_IN_PROGRESS) continue;
$roundStarted = strtotime($round['started_at'] ?? 'now'); $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