0) { @ob_end_flush(); } ob_implicit_flush(true); // If any installer/session code might lock the session, release it if (session_status() === PHP_SESSION_ACTIVE) { @session_write_close(); } function sse_send(array $payload) { echo "data: " . json_encode($payload, JSON_UNESCAPED_SLASHES) . "\n\n"; @flush(); @ob_flush(); } function sse_ping() { // Comment line per SSE spec, keeps connection alive echo ":\n\n"; @flush(); @ob_flush(); } global $database; // 1) Count total croppers try { $total = $database->TotalCroppers(); } catch (Throwable $e) { sse_send(['pct'=>0,'done'=>0,'total'=>0,'msg'=>'Count failed: '.$e->getMessage()]); exit; } sse_send(['pct'=>0,'done'=>0,'total'=>$total,'msg'=>"Starting croppers build (found $total tiles)…"]); // 2) Build with live reporter (pings to keep proxies happy) $lastPing = time(); $reporter = function($done, $target, $pct) use (&$lastPing) { sse_send(['pct'=>(int)$pct,'done'=>(int)$done,'total'=>(int)$target]); // send keep-alive every ~10s if (time() - $lastPing >= 10) { sse_ping(); $lastPing = time(); } if (connection_aborted()) { exit; } // client left }; // Run it (fresh world => truncateFirst=true). Slightly smaller batch keeps progress updates more frequent. $out = $database->populateCroppers($total, true, 2000, $reporter); if (!empty($out['ok'])) { sse_send([ 'pct'=>100, 'done'=>(int)$out['processed'], 'total'=>(int)$out['target'], 'msg'=>'Done building croppers.' ]); } else { sse_send([ 'error'=>true, 'pct'=>0, 'done'=>0, 'total'=>(int)$total, 'msg'=>'Error: '.($out['msg'] ?? 'unknown') ]); } exit;