Build Casual Fan Data
Run Level 1 to compile all schedules, or Level 2 to fetch quick live updates.
0]; if ($isFailure) $mainDb['_meta']['failed_count']++; $cityKeys = array_keys($miniDb); foreach ($cityKeys as $cKey) { if ($cKey === '_major_events' || $cKey === '_all_teams' || $cKey === '_meta') continue; if (!isset($miniDb[$cKey]['leagues'])) continue; foreach ($miniDb[$cKey]['leagues'] as $league => $lData) { if (empty($lData['games']) && empty($lData['upcoming'])) continue; if (!isset($mainDb[$cKey]['leagues'][$league])) { $mainDb[$cKey]['leagues'][$league] = ['latest_timestamp' => 0, 'live' => [], 'games' => [], 'upcoming' => []]; } $mainDb[$cKey]['leagues'][$league]['games'] = array_merge($mainDb[$cKey]['leagues'][$league]['games'], $lData['games']); $mainDb[$cKey]['leagues'][$league]['upcoming'] = array_merge($mainDb[$cKey]['leagues'][$league]['upcoming'], $lData['upcoming']); $mainDb[$cKey]['leagues'][$league]['latest_timestamp'] = max($mainDb[$cKey]['leagues'][$league]['latest_timestamp'], $lData['latest_timestamp']); } } if (!empty($miniDb['_major_events'])) { $mainDb['_major_events'] = array_merge($mainDb['_major_events'] ?? [], $miniDb['_major_events']); } ftruncate($fh, 0); rewind($fh); fwrite($fh, json_encode($mainDb)); fflush($fh); flock($fh, LOCK_UN); fclose($fh); } function merge_live_step_into_build(string $token, string $stepKey, array $data, array $CITIES): void { $path = build_file_path($token); $fh = fopen($path, 'c+'); flock($fh, LOCK_EX); $raw = stream_get_contents($fh); $mainDb = json_decode($raw, true) ?: []; if (empty($mainDb)) { live_log("merge stepKey=$stepKey: baseline build file was EMPTY before overlay (latest_db.json missing/blank?)"); } $liveBefore = live_count_all($mainDb); // Overlay live stats onto memory snapshot apply_live_scoreboards($mainDb, [$stepKey => $data], $CITIES, live_logger()); $liveAfter = live_count_all($mainDb); live_log("merge stepKey=$stepKey: total live records before=$liveBefore after=$liveAfter"); ftruncate($fh, 0); rewind($fh); fwrite($fh, json_encode($mainDb)); fflush($fh); flock($fh, LOCK_UN); fclose($fh); } /** Count every live record across all cities/leagues — used for before/after diagnostics. */ function live_count_all(array $db): int { $n = 0; foreach ($db as $cKey => $cData) { if (!is_array($cData) || empty($cData['leagues'])) continue; foreach ($cData['leagues'] as $lData) { if (!empty($lData['live'])) $n += count($lData['live']); } } return $n; } function json_response(array $payload, int $status = 200): void { http_response_code($status); header('Content-Type: application/json'); echo json_encode($payload); exit; } // --- AJAX actions -------------------------------------------------------- $action = $_GET['action'] ?? ''; $level = $_GET['level'] ?? 'full'; $steps = $level === 'live' ? build_live_step_list() : build_step_list($CITIES, $MAJOR_EVENTS, $SPORT_LABELS); if ($action !== '') { live_log(sprintf( 'ROUTING action=%s level=%s (raw level param=%s) -> %s step list with %d steps', $action, $level, var_export($_GET['level'] ?? null, true), $level === 'live' ? 'LIVE' : 'FULL', count($steps) )); } if ($action === 'start') { ensure_build_dir(); cleanup_stale_builds(); $token = bin2hex(random_bytes(16)); if ($level === 'live') { $masterDbPath = __DIR__ . '/latest_db.json'; if (file_exists($masterDbPath)) { $db = json_decode(file_get_contents($masterDbPath), true) ?: []; file_put_contents(build_file_path($token), json_encode($db)); } else { json_response(['ok' => false, 'error' => 'No Full Build exists yet. Run Level 1 (Full) first to create the baseline.']); } } else { $emptyDb = aggregate_database([], $CITIES, $MAJOR_EVENTS, $SPORT_LABELS); file_put_contents(build_file_path($token), json_encode($emptyDb)); } json_response(['ok' => true, 'token' => $token, 'total' => count($steps)]); } if ($action === 'step') { $token = (string) ($_GET['token'] ?? ''); $index = (int) ($_GET['step'] ?? -1); try { build_file_path($token); } catch (InvalidArgumentException $e) { json_response(['ok' => false, 'error' => 'Invalid build token'], 400); } if ($index < 0 || $index >= count($steps)) { json_response(['ok' => false, 'error' => 'Invalid step index'], 400); } $step = $steps[$index]; $started = microtime(true); live_log("STEP #$index key={$step['key']} label=\"{$step['label']}\" starting fetch"); $data = fetch_step($step, live_logger()); $elapsed = (int) round((microtime(true) - $started) * 1000); live_log(sprintf( 'STEP #%d key=%s fetch done in %dms — data=%s%s', $index, $step['key'], $elapsed, $data === null ? 'NULL (treated as failure)' : 'ok', (is_array($data) ? ' events=' . count($data['events'] ?? []) : '') )); if ($level === 'live') { if ($data !== null) { merge_live_step_into_build($token, $step['key'], $data, $CITIES); } else { live_log("STEP #$index key={$step['key']}: no data, recording as failed (no live overlay applied)"); merge_step_into_build($token, [], true); } } else { if ($data !== null) { $miniDb = aggregate_database([$step['key'] => $data], $CITIES, $MAJOR_EVENTS, $SPORT_LABELS); merge_step_into_build($token, $miniDb, false); } else { merge_step_into_build($token, [], true); } } json_response([ 'ok' => true, 'step' => $index, 'label' => $step['label'], 'success' => $data !== null, 'elapsed_ms' => $elapsed, ]); } if ($action === 'finalize') { $token = (string) ($_GET['token'] ?? ''); try { build_file_path($token); } catch (InvalidArgumentException $e) { json_response(['ok' => false, 'error' => 'Invalid build token'], 400); } $database = read_build($token); $failedCount = $database['_meta']['failed_count'] ?? 0; unset($database['_meta']); // Only full builds require sorting the completed/upcoming arrays. Live builds inherit // the pre-sorted structure directly from latest_db.json if ($level === 'full') { $cityKeys = array_keys($CITIES); $cityKeys[] = 'all'; foreach ($cityKeys as $cKey) { if (!isset($database[$cKey]['leagues'])) continue; uasort($database[$cKey]['leagues'], function ($a, $b) { return $b['latest_timestamp'] <=> $a['latest_timestamp']; }); foreach ($database[$cKey]['leagues'] as &$lData) { usort($lData['games'], function ($a, $b) { return $b['timestamp'] <=> $a['timestamp']; }); usort($lData['upcoming'], function ($a, $b) { return $a['timestamp'] <=> $b['timestamp']; }); } unset($lData); } usort($database['_major_events'], function ($a, $b) { return strtotime($b['date_raw'] ?? 'now') <=> strtotime($a['date_raw'] ?? 'now'); }); // Save the compiled, sorted, fresh baseline schedule for future live runs file_put_contents(__DIR__ . '/latest_db.json', json_encode($database)); } $timestamp = date('l, F j, Y g:i:s A T'); $html = render_index_html($database, $CITIES, $timestamp); $targetFile = __DIR__ . '/index.php'; $wrote = write_index_file($html, $targetFile); @unlink(build_file_path($token)); if (!$wrote) { json_response([ 'ok' => false, 'error' => "Build finished but writing {$targetFile} failed. Check that the web server can write to that path (file permissions, or the file is locked).", ]); } json_response([ 'ok' => true, 'timestamp' => $timestamp, 'team_count' => count($database['_all_teams'] ?? []), 'city_count' => count($CITIES), 'failed_count' => $failedCount, ]); } // --- CRON actions -------------------------------------------------------- if ($action === 'cron_run') { ensure_build_dir(); cleanup_stale_builds(); $token = bin2hex(random_bytes(16)); if ($level === 'live') { $masterDbPath = __DIR__ . '/latest_db.json'; if (file_exists($masterDbPath)) { $db = json_decode(file_get_contents($masterDbPath), true) ?: []; file_put_contents(build_file_path($token), json_encode($db)); } else { exit("Error: No Full Build exists. Run Level 1 before running Level 2 cron.\n"); } } else { $emptyDb = aggregate_database([], $CITIES, $MAJOR_EVENTS, $SPORT_LABELS); file_put_contents(build_file_path($token), json_encode($emptyDb)); } $failedCount = 0; foreach ($steps as $step) { live_log("CRON STEP key={$step['key']} label=\"{$step['label']}\""); $data = fetch_step($step, live_logger()); live_log(sprintf('CRON STEP key=%s data=%s%s', $step['key'], $data === null ? 'NULL' : 'ok', is_array($data) ? ' events=' . count($data['events'] ?? []) : '')); if ($data !== null) { if ($level === 'live') { merge_live_step_into_build($token, $step['key'], $data, $CITIES); } else { $miniDb = aggregate_database([$step['key'] => $data], $CITIES, $MAJOR_EVENTS, $SPORT_LABELS); merge_step_into_build($token, $miniDb, false); unset($miniDb); } } else { merge_step_into_build($token, [], true); $failedCount++; } unset($data); } $database = read_build($token); unset($database['_meta']); if ($level === 'full') { $cityKeys = array_keys($CITIES); $cityKeys[] = 'all'; foreach ($cityKeys as $cKey) { if (!isset($database[$cKey]['leagues'])) continue; uasort($database[$cKey]['leagues'], function ($a, $b) { return $b['latest_timestamp'] <=> $a['latest_timestamp']; }); foreach ($database[$cKey]['leagues'] as &$lData) { usort($lData['games'], function ($a, $b) { return $b['timestamp'] <=> $a['timestamp']; }); usort($lData['upcoming'], function ($a, $b) { return $a['timestamp'] <=> $b['timestamp']; }); } unset($lData); } usort($database['_major_events'], function ($a, $b) { return strtotime($b['date_raw'] ?? 'now') <=> strtotime($a['date_raw'] ?? 'now'); }); file_put_contents(__DIR__ . '/latest_db.json', json_encode($database)); } $timestamp = date('l, F j, Y g:i:s A T'); $html = render_index_html($database, $CITIES, $timestamp); $wrote = write_index_file($html, __DIR__ . '/index.php'); @unlink(build_file_path($token)); echo "Cron build ($level) complete at {$timestamp}.\n"; echo "Failed requests: {$failedCount}\n"; if (!$wrote) { echo "WARNING: could not write index.php — check that the web server can write to " . __DIR__ . ".\n"; } exit; } // --- Default: render the build page -------------------------------------- $stepsFullJs = json_encode(array_map(fn($s) => ['label' => $s['label']], build_step_list($CITIES, $MAJOR_EVENTS, $SPORT_LABELS)), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); $stepsLiveJs = json_encode(array_map(fn($s) => ['label' => $s['label']], build_live_step_list()), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); $keyParam = isset($_GET['key']) ? '&key=' . urlencode((string) $_GET['key']) : ''; ?>
Run Level 1 to compile all schedules, or Level 2 to fetch quick live updates.