From c0f9d9712be80125c1772375d5f04fc72a6494de Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 11 Aug 2026 12:17:14 -0400 Subject: [PATCH] =?UTF-8?q?MSIN-4574=20=E2=80=94=20Enhance=20ChronoTrack?= =?UTF-8?q?=20API=20synchronization=20by=20implementing=20retry=20logic=20?= =?UTF-8?q?for=20HTTP=20requests,=20adjusting=20batch=20sizes=20for=20entr?= =?UTF-8?q?ies,=20and=20improving=20error=20handling.=20Update=20version?= =?UTF-8?q?=20code=20to=204.72.974=20to=20reflect=20these=20changes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/chronotrack_api/fx_chronotrack_admin.php | 223 ++++++++-- php/chronotrack_api/fx_chronotrack_http.php | 64 ++- php/chronotrack_api/fx_chronotrack_oauth.php | 6 + php/chronotrack_api/fx_chronotrack_sync.php | 412 ++++++++++++++----- php/inc_settings.php | 2 +- superadm/ajax_chronotrack_api.php | 27 +- 6 files changed, 580 insertions(+), 154 deletions(-) diff --git a/php/chronotrack_api/fx_chronotrack_admin.php b/php/chronotrack_api/fx_chronotrack_admin.php index ba2a2cdc..e89ffaf8 100644 --- a/php/chronotrack_api/fx_chronotrack_admin.php +++ b/php/chronotrack_api/fx_chronotrack_admin.php @@ -1799,9 +1799,45 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed }); // MSIN-4574 — vérifier alignement MS1 ↔ CT (orphelins) + function setReconcileStatus(strTitle, strHint, strKind) { + var $status = $('#ct_api_push_status'); + if (!$status.length) { + return; + } + strKind = strKind || 'loading'; + var strColor = (strKind === 'ok') ? 'text-success' + : ((strKind === 'error') ? 'text-danger' + : ((strKind === 'todo') ? 'text-warning' : 'text-primary')); + $status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading'); + if (strKind === 'loading') { + $status.addClass('ct-api-push-status--loading'); + } else if (strKind === 'ok') { + $status.addClass('ct-api-push-status--ok'); + } else if (strKind === 'todo') { + $status.addClass('ct-api-push-status--todo'); + } + $status.html( + '

' + + (strKind === 'loading' + ? ' ' + : '') + + escHtml(strTitle) + '

' + + '

' + escHtml(strHint || '') + '

' + ); + try { + if ($status[0] && $status[0].scrollIntoView) { + $status[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + } catch (eScroll) { /* ignore */ } + } + function renderReconcilePanel(res) { var $wrap = $('#ct_api_reconcile_wrap'); - var intCount = (res && res.orphans) ? res.orphans.length : (res.count || 0); + var intCount = (res && typeof res.count !== 'undefined') + ? parseInt(res.count, 10) + : ((res && res.orphans) ? res.orphans.length : 0); + var tabRows = (res && res.orphans) ? res.orphans : []; + var intShowMax = 80; var html = '
'; html += '
'; html += 'Alignement ChronoTrack'; @@ -1813,23 +1849,35 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed html += '
' + escHtml((res && res.message) ? res.message : 'Erreur') + '
'; html += '
'; $wrap.removeClass('d-none').html(html); + setReconcileStatus( + 'Alignement — erreur', + (res && res.message) ? res.message : 'Erreur', + 'error' + ); return; } html += '

' + intCount + ' orphelin(s)'; if (typeof res.ct_total !== 'undefined' && res.ct_total !== null) { html += ' · CT total ' + escHtml(String(res.ct_total)); } + if (typeof res.eligible_count !== 'undefined') { + html += ' · éligibles MS1 ' + escHtml(String(res.eligible_count)); + } + if (res.elapsed_ms) { + html += ' · ' + escHtml(String(Math.round(res.elapsed_ms / 100) / 10)) + ' s'; + } html += '

'; if (intCount === 0) { html += '
Aucun écart détecté — MS1 et ChronoTrack sont alignés côté entries.
'; html += '
'; $wrap.removeClass('d-none').html(html); + setReconcileStatus('Alignement OK', 'Aucun orphelin CT détecté.', 'ok'); return; } html += '
'; html += ''; html += ''; - $.each(res.orphans || [], function (_, row) { + $.each(tabRows.slice(0, intShowMax), function (_, row) { html += ''; html += ''; html += ''; @@ -1839,24 +1887,41 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed html += ''; }); html += '
Nom CTextbibstatutraison
' + escHtml(row.name || '—') + '' + escHtml(row.external_id || '—') + '
'; + if (tabRows.length > intShowMax) { + html += '

Affichage des ' + intShowMax + + ' premiers — total ' + intCount + '. Le retrait traite toute la liste.

'; + } html += ''; html += '

Action : statut WITHDRAWN + dossard vidé sur ChronoTrack ' + '(hors_eligible / doublons). Les entries sans external ID sont listées mais non retirées automatiquement.

'; html += ''; $wrap.removeClass('d-none').html(html); + setReconcileStatus( + 'Alignement — ' + intCount + ' orphelin(s)', + (res.message || '') + ' Détail sous les boutons.', + 'todo' + ); } function loadReconcile() { var $btn = $('#ct_api_btn_reconcile'); var $wrap = $('#ct_api_reconcile_wrap'); $btn.prop('disabled', true); - $wrap.removeClass('d-none').html('Analyse alignement ChronoTrack…'); + $wrap.removeClass('d-none').html( + ' ' + + 'Analyse alignement ChronoTrack (lecture entries CT)…' + ); + setReconcileStatus( + 'Alignement en cours…', + 'Lecture des entries ChronoTrack — ne fermez pas la page.', + 'loading' + ); $.ajax({ url: strAjaxUrl, type: 'POST', dataType: 'json', - timeout: 180000, + timeout: 240000, data: { a: 'sync_orphans', eve_id: intEveId } }).done(function (res) { if (rejectIfSessionLost(res)) { @@ -1864,12 +1929,16 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed } renderReconcilePanel(res); }).fail(function (jqXHR, strStatus) { - renderReconcilePanel({ - state: 'error', - message: strStatus === 'timeout' - ? 'Délai dépassé (lecture entries CT).' - : 'Erreur réseau (alignement).' - }); + var intHttp = (jqXHR && jqXHR.status) ? parseInt(jqXHR.status, 10) : 0; + var strMsg; + if (strStatus === 'timeout') { + strMsg = 'Délai dépassé (lecture entries CT trop longue). Réessayez.'; + } else if (intHttp === 503 || intHttp === 502) { + strMsg = 'HTTP ' + intHttp + ' serveur MS1 pendant l’alignement — réessayez.'; + } else { + strMsg = 'Erreur réseau (alignement' + (intHttp ? ', HTTP ' + intHttp : '') + ').'; + } + renderReconcilePanel({ state: 'error', message: strMsg }); }).always(function () { $btn.prop('disabled', false); }); @@ -1896,6 +1965,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed } var $btn = $(this); $btn.prop('disabled', true).text('Retrait en cours…'); + setReconcileStatus('Retrait orphelins…', 'WITHDRAWN + clear bib sur ChronoTrack.', 'loading'); $.ajax({ url: strAjaxUrl, type: 'POST', @@ -1911,6 +1981,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed loadPushPreview(true); }).fail(function () { window.alert('Erreur réseau (retrait orphelins).'); + setReconcileStatus('Retrait interrompu', 'Erreur réseau.', 'error'); }).always(function () { $btn.prop('disabled', false); }); @@ -2139,19 +2210,29 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed } } - function finishPush() { + function finishPush(strModeFinish) { stopElapsedTimer(); blnPushRunning = false; - $('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile') + $('#ct_api_btn_push, #ct_api_btn_push_full, #ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile') .prop('disabled', false); - var strFinal = (intErrSum === 0) - ? (intOkSum + ' participant(s) synchronisé(s) (POST ' + intPostSum + ' · PUT ' + intPutSum + ').') - : (intOkSum + ' OK, ' + intErrSum + ' erreur(s).'); - if (blnForceFull && intErrSum === 0) { - strFinal = 'Resync complète — ' + strFinal; + var strMode = strModeFinish || (blnFatal ? 'interrupted' : ((intErrSum === 0) ? 'ok' : 'partial')); + var strFinal; + if (strMode === 'interrupted') { + strFinal = 'Interrompu après ' + intOkSum + ' OK' + + (intErrSum ? (', ' + intErrSum + ' erreur(s)') : '') + + ' (POST ' + intPostSum + '). Relancer Tout renvoyer pour reprendre le reste.'; + } else if (intErrSum === 0) { + strFinal = intOkSum + ' participant(s) synchronisé(s) (POST ' + intPostSum + + ' · dossards ' + intPutSum + ').'; + if (blnForceFull) { + strFinal = 'Resync complète — ' + strFinal; + } + } else { + strFinal = intOkSum + ' OK, ' + intErrSum + ' erreur(s).'; } strFinal += ' · durée ' + formatElapsed(); - var strState = blnFatal ? 'error' : ((intErrSum === 0) ? 'ok' : 'partial'); + var strState = (strMode === 'interrupted') ? 'error' + : ((intErrSum === 0) ? 'ok' : 'partial'); renderPushResult({ state: strState, message: strFinal, @@ -2175,19 +2256,79 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed loadPushPreview(true); } + // MSIN-4574 — passe dossards découpée (après identité) + function pushBibChunk(intBibPendingHint, intBibChunk) { + intBibChunk = intBibChunk || 20; + var intLeft = (intBibPendingHint != null) ? parseInt(intBibPendingHint, 10) : 0; + renderProgress( + Math.min(intOffset, intTotal), + intTotal, + 'passe dossards — reste ~' + intLeft + '…' + ); + $.ajax({ + url: strAjaxUrl, + type: 'POST', + dataType: 'json', + timeout: 180000, + data: { + a: 'sync_push_bibs', + eve_id: intEveId, + limit: intBibChunk + } + }).done(function (res) { + if (rejectIfSessionLost(res)) { + stopElapsedTimer(); + return; + } + if (!res) { + blnFatal = true; + tabErrAll.push('Réponse vide (passe dossards)'); + finishPush('interrupted'); + return; + } + intOkSum += parseInt(res.pushed_ok || 0, 10); + intErrSum += parseInt(res.pushed_error || 0, 10); + intPutSum += parseInt(res.pushed_ok || 0, 10); + if (res.errors && res.errors.length) { + $.each(res.errors, function (_, msg) { + if (tabErrAll.length < 20) { + tabErrAll.push(msg); + } + }); + } + var intPend = parseInt(res.bib_pending != null ? res.bib_pending : 0, 10); + if (res.done || intPend <= 0) { + finishPush(intErrSum === 0 ? 'ok' : 'partial'); + return; + } + window.setTimeout(function () { + pushBibChunk(intPend, intBibChunk); + }, 50); + }).fail(function (jqXHR, strStatus) { + blnFatal = true; + var intHttp = (jqXHR && jqXHR.status) ? parseInt(jqXHR.status, 10) : 0; + var strDetail = (strStatus === 'timeout') + ? 'Délai dépassé sur passe dossards — relancer Tout renvoyer.' + : ('Interruption passe dossards (' + strStatus + ', HTTP ' + intHttp + '). Relancer.'); + tabErrAll.push(strDetail); + finishPush('interrupted'); + }); + } + function pushChunk() { var intLot = Math.floor(intOffset / Math.max(1, intChunk)) + 1; var intLots = Math.max(1, Math.ceil(intTotal / Math.max(1, intChunk))); renderProgress( intOffset, intTotal, - 'lot ' + intLot + '/' + intLots + ' (POST ' + Math.min(intChunk, Math.max(0, intTotal - intOffset)) + ')…' + 'identité lot ' + intLot + '/' + intLots + + ' (POST ' + Math.min(intChunk, Math.max(0, intTotal - intOffset)) + ')…' ); $.ajax({ url: strAjaxUrl, type: 'POST', dataType: 'json', - timeout: 300000, + timeout: 180000, data: { a: 'sync_push', eve_id: intEveId, @@ -2203,7 +2344,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed if (!res) { blnFatal = true; tabErrAll.push('Réponse vide du serveur'); - finishPush(); + finishPush('interrupted'); return; } if (res.state !== 'ok' && res.state !== 'partial' && res.state !== 'error') { @@ -2211,13 +2352,13 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed if (res.message) { tabErrAll.push(res.message); } - finishPush(); + finishPush('interrupted'); return; } if (res.total === 0 && res.state === 'error') { stopElapsedTimer(); renderPushResult(res); - $('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile') + $('#ct_api_btn_push, #ct_api_btn_push_full, #ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs, #ct_api_btn_reconcile') .prop('disabled', false); blnPushRunning = false; loadPushPreview(true); @@ -2244,32 +2385,52 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed renderProgress( Math.min(intOffset, intTotal), intTotal, - 'lot ' + Math.ceil(intOffset / Math.max(1, intChunk)) + ' terminé' + 'lot identité terminé' ); - if (res.done || intOffset >= intTotal) { - finishPush(); + + // Identité finie → passe dossards séparée (évite 503 hébergeur) + if (res.identity_done || res.phase === 'bibs') { + var intBibPend = parseInt(res.bib_pending != null ? res.bib_pending : 0, 10); + var intBibChunk = parseInt(res.bib_chunk_size != null ? res.bib_chunk_size : 20, 10); + if (intBibPend > 0) { + window.setTimeout(function () { + pushBibChunk(intBibPend, intBibChunk); + }, 50); + return; + } + finishPush(intErrSum === 0 ? 'ok' : 'partial'); return; } + + if (res.done || intOffset >= intTotal) { + finishPush(intErrSum === 0 ? 'ok' : 'partial'); + return; + } + // Enchaîner tout de suite (pas de pause artificielle — CT + retry 503 suffisent) pushChunk(); }).fail(function (jqXHR, strStatus) { blnFatal = true; var intHttp = (jqXHR && jqXHR.status) ? parseInt(jqXHR.status, 10) : 0; var strDetail; if (strStatus === 'timeout') { - strDetail = 'Délai dépassé sur un lot — relancer (lots plus petits).'; + strDetail = 'Délai dépassé sur un lot identité — relancer Tout renvoyer (lots plus petits v4.72.972).'; + } else if (intHttp === 503 || intHttp === 502) { + strDetail = 'HTTP ' + intHttp + + ' sur le serveur MS1 (timeout / surcharge) pendant un lot — ' + + 'pas un refus métier CT. Relancer Tout renvoyer ; le modèle découpe maintenant identité + dossards.'; } else if (intHttp === 0) { - strDetail = 'Connexion coupée sur un lot (serveur surchargé / PHP arrêté). Relancer Tout renvoyer.'; + strDetail = 'Connexion coupée sur un lot. Relancer Tout renvoyer.'; } else { strDetail = 'Erreur réseau sur un lot (' + strStatus + ', HTTP ' + intHttp + ').'; } if (jqXHR && jqXHR.responseText) { - var strBody = String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 220); - if (strBody) { + var strBody = String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180); + if (strBody && strBody.indexOf('{') !== 0) { strDetail += ' — ' + strBody; } } tabErrAll.push(strDetail); - finishPush(); + finishPush('interrupted'); }); } diff --git a/php/chronotrack_api/fx_chronotrack_http.php b/php/chronotrack_api/fx_chronotrack_http.php index 0c89995a..56328e92 100644 --- a/php/chronotrack_api/fx_chronotrack_http.php +++ b/php/chronotrack_api/fx_chronotrack_http.php @@ -4,6 +4,43 @@ */ function fxChronotrackApiHttpRequest($strMethod, $strUrl, $arrOptions = array()) { + $intMaxAttempts = max(1, intval($arrOptions['retries'] ?? 1)); + // MSIN-4574 — 502/503/429 = surcharge CT ou proxy : backoff, pas abandon immédiat + $tabRetryCodes = array(429, 502, 503, 504); + $arrLast = null; + + for ($intAttempt = 1; $intAttempt <= $intMaxAttempts; $intAttempt++) { + $arrLast = fxChronotrackApiHttpRequestOnce($strMethod, $strUrl, $arrOptions); + if (($arrLast['state'] ?? '') === 'ok') { + $arrLast['attempts'] = $intAttempt; + return $arrLast; + } + $intCode = intval($arrLast['http_code'] ?? 0); + $blnRetry = ($intAttempt < $intMaxAttempts) && ( + in_array($intCode, $tabRetryCodes, true) + || (!empty($arrLast['curl_error']) && ( + stripos((string)$arrLast['curl_error'], 'timed out') !== false + || stripos((string)$arrLast['curl_error'], 'reset') !== false + )) + ); + if (!$blnRetry) { + $arrLast['attempts'] = $intAttempt; + return $arrLast; + } + // Backoff : 1s, 2s, 4s… + usleep((int)(1000000 * pow(2, $intAttempt - 1))); + } + + if (is_array($arrLast)) { + $arrLast['attempts'] = $intMaxAttempts; + } + return $arrLast; +} + +/** + * Une seule tentative cURL (sans retry). + */ +function fxChronotrackApiHttpRequestOnce($strMethod, $strUrl, $arrOptions = array()) { $arrResult = array( 'state' => 'error', 'http_code' => 0, @@ -19,11 +56,12 @@ function fxChronotrackApiHttpRequest($strMethod, $strUrl, $arrOptions = array()) $ch = curl_init(); $arrHeaders = isset($arrOptions['headers']) && is_array($arrOptions['headers']) ? $arrOptions['headers'] : array(); + $intTimeout = max(30, intval($arrOptions['timeout'] ?? 90)); curl_setopt($ch, CURLOPT_URL, $strUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20); - curl_setopt($ch, CURLOPT_TIMEOUT, 180); + curl_setopt($ch, CURLOPT_TIMEOUT, $intTimeout); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $arrHeaders); @@ -79,31 +117,19 @@ function fxChronotrackApiHttpGetJsonErrorMessage($arrHttp) { $arrParts[] = (string)$arrHttp['json'][$strKey]; } } - if (!empty($arrHttp['json']['error'])) { - if (is_array($arrHttp['json']['error'])) { - foreach ($arrHttp['json']['error'] as $mixErr) { - if (is_string($mixErr) && trim($mixErr) !== '') { - $arrParts[] = $mixErr; - } elseif (is_array($mixErr)) { - if (!empty($mixErr['errorMessage'])) { - $arrParts[] = (string)$mixErr['errorMessage']; - } elseif (!empty($mixErr['message'])) { - $arrParts[] = (string)$mixErr['message']; - } - } - } - } elseif (is_string($arrHttp['json']['error'])) { - $arrParts[] = $arrHttp['json']['error']; - } - } } + $strBody = trim((string)($arrHttp['body'] ?? '')); if (count($arrParts) > 0) { return 'HTTP ' . intval($arrHttp['http_code']) . ' — ' . implode(' | ', array_unique($arrParts)); } - $strBody = trim((string)($arrHttp['body'] ?? '')); if ($strBody !== '') { + // Page HTML 503 CT / proxy + if (stripos($strBody, '503') !== false || stripos($strBody, 'Service Unavailable') !== false) { + return 'HTTP ' . intval($arrHttp['http_code']) + . ' — Service Unavailable (surcharge CT/proxy — lot trop gros ou trop rapide)'; + } return 'HTTP ' . intval($arrHttp['http_code']) . ' — ' . substr($strBody, 0, 400); } diff --git a/php/chronotrack_api/fx_chronotrack_oauth.php b/php/chronotrack_api/fx_chronotrack_oauth.php index af3ec816..3c0afaa5 100644 --- a/php/chronotrack_api/fx_chronotrack_oauth.php +++ b/php/chronotrack_api/fx_chronotrack_oauth.php @@ -133,6 +133,8 @@ function fxChronotrackApiOAuthApiGet($strPath, $arrQuery = array()) { $arrHttp = fxChronotrackApiHttpRequest('GET', $strUrl, array( 'headers' => array('Authorization: Bearer ' . $arrToken['access_token']), + 'retries' => 3, + 'timeout' => 60, )); if ($arrHttp['state'] === 'ok') { @@ -170,6 +172,8 @@ function fxChronotrackApiOAuthApiPost($strPath, $mixBody, $arrQuery = array()) { 'Accept: application/json', ), 'body' => $strBody, + 'retries' => 3, + 'timeout' => 90, )); if ($arrHttp['state'] === 'ok') { @@ -207,6 +211,8 @@ function fxChronotrackApiOAuthApiPut($strPath, $mixBody, $arrQuery = array()) { 'Accept: application/json', ), 'body' => $strBody, + 'retries' => 3, + 'timeout' => 90, )); if ($arrHttp['state'] === 'ok') { diff --git a/php/chronotrack_api/fx_chronotrack_sync.php b/php/chronotrack_api/fx_chronotrack_sync.php index 6e6d6598..eff5e18b 100644 --- a/php/chronotrack_api/fx_chronotrack_sync.php +++ b/php/chronotrack_api/fx_chronotrack_sync.php @@ -3,9 +3,11 @@ * MSIN API ChronoTrack — Phase 2a : lecture MS1, diff, push manuel entries. */ -// Lot POST ChronoTrack — même découpage que le bouton « Envoyer » (barre %). +// MSIN-4574 — vitesse 2026 : 1 POST CT = jusqu’à 100 entries AVEC dossard. +// 2e passe bib uniquement pour conflits (pas 1300 PUT systématiques). define('MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE', 100); define('MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE', 100); +define('MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE', 40); /** * MSIN-4328 — persiste ct_entry_id sur resultats_participants. @@ -1854,8 +1856,8 @@ function fxChronotrackApiSyncPostEntriesAdaptive($intCtEventId, array $tabEntrie return array('ok' => 0, 'err' => 1, 'bib_retry' => array()); } - // Sous-lots fixes : 25 → 5 → 1 - $intSubSize = ($intCount > 25) ? 25 : (($intCount > 5) ? 5 : 1); + // Sous-lots : 100 → 25 → 5 → 1 (échecs / conflits seulement) + $intSubSize = ($intCount > 100) ? 100 : (($intCount > 25) ? 25 : (($intCount > 5) ? 5 : 1)); $intOk = 0; $intErr = 0; $tabBibRetry = array(); @@ -2408,7 +2410,7 @@ function fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId) { $tabAll = array(); $tabSeenEntryIds = array(); $intPage = 1; - $intPageSize = 100; + $intPageSize = 200; $intMaxPages = 500; while ($intPage <= $intMaxPages) { @@ -2980,9 +2982,9 @@ function fxChronotrackApiSyncPushEvent($intEveId, $arrOptions = array()) { // Avant : PUT unitaire si ct_entry_id → ~100 appels/lot = 1–2 min figé à 0/N. // entry_id reste sur le payload pour la 2e passe dossard ; retiré seulement à l’appel POST. // - // MSIN-4574 — LOT multi-entries : ne PAS envoyer les dossards dans le POST bulk. - // CT peut répondre OK sans appliquer un changement de bib (surtout swaps / renuméros - // d’équipe). On pousse identité sans bib, puis 2e passe unitaire avec gestion conflit. + // MSIN-4574 — envoi AVEC dossard dans le POST bulk (rapide). + // Ancien modèle : strip systématique → 2e passe 1 PUT/personne = ~10 min pour 1300. + // Maintenant : conflit bib seulement → adaptive / file bib (minorité des cas). $tabPostEntries = array(); $tabPostMeta = array(); foreach ($tabBatch as $intI => $arrPayload) { @@ -2993,23 +2995,6 @@ function fxChronotrackApiSyncPushEvent($intEveId, $arrOptions = array()) { $tabPostMeta[] = $tabMeta[$intI] ?? array('par_id' => 0, 'external_id' => ''); } - if (count($tabPostEntries) > 1) { - $tabStripped = array(); - foreach ($tabPostEntries as $intI => $arrPayload) { - $strBibWanted = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : ''; - if ($strBibWanted !== '') { - $tabBibRetryChunk[] = array( - 'payload' => $arrPayload, - 'meta' => $tabPostMeta[$intI] ?? array('par_id' => 0, 'external_id' => ''), - ); - $tabStripped[] = fxChronotrackApiSyncPayloadWithoutBib($arrPayload); - } else { - $tabStripped[] = $arrPayload; - } - } - $tabPostEntries = $tabStripped; - } - if (count($tabPostEntries) > 0) { $arrRes = fxChronotrackApiSyncPostEntriesAdaptive( $intCtEventId, @@ -3053,92 +3038,85 @@ function fxChronotrackApiSyncPushEvent($intEveId, $arrOptions = array()) { $arrJob['err_sum'] = $intErrSum; $intNextOffset = $intOffset + $intChunkCount; - $blnDone = ($intNextOffset >= $intTotal); + $blnIdentityDone = ($intNextOffset >= $intTotal); $blnLastPushSaved = false; $intBibRetryOk = 0; $intBibRetryErr = 0; + $intBibPending = count($tabBibRetryJob); - if ($blnDone) { - // 2e passe dossards (swaps) — interne, sans changer offset/total de la barre % - if (count($tabBibRetryJob) > 0) { - $arrBibPass = fxChronotrackApiSyncPushPendingBibs( - $intCtEventId, - $tabBibRetryJob, - $intEveId - ); - $intBibRetryOk = intval($arrBibPass['ok'] ?? 0); - $intBibRetryErr = intval($arrBibPass['err'] ?? 0); - $intOkSum += $intBibRetryOk; - $intErrSum += $intBibRetryErr; - if (!empty($arrBibPass['errors']) && is_array($arrBibPass['errors'])) { - foreach ($arrBibPass['errors'] as $strBe) { - if (count($tabErrors) < 20) { - $tabErrors[] = $strBe; - } + // MSIN-4574 — NE PAS enchaîner 1300 PUT dossards dans la même requête PHP que le dernier lot. + // C’était la cause typique du HTTP 503 HTML (timeout hébergeur) après 8–10 min. + if ($blnIdentityDone) { + $arrJob['phase'] = ($intBibPending > 0) ? 'bibs' : 'done'; + $arrJob['identity_done_at'] = time(); + if ($intBibPending === 0) { + if ($intErrSum === 0) { + $blnLastPushSaved = fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum); + } + // résumé final (sans passe bib) + $intPaysSent = 0; + $intPaysMiss = 0; + foreach ($tabBatchAll as $arrP) { + if (!is_array($arrP)) { + continue; + } + if (fxChronotrackApiSyncCountryFromPayload($arrP) !== '') { + $intPaysSent++; + } else { + $intPaysMiss++; } } + $arrCovJob = is_array($arrJob['country_coverage'] ?? null) ? $arrJob['country_coverage'] : array(); fxChronotrackApiSyncLog( $intEveId, 0, - 'push_bib_retry', - ($intBibRetryErr === 0) ? 'ok' : 'error', - 'fin job — retry dossards n=' . count($tabBibRetryJob) - . ' ok=' . $intBibRetryOk . ' err=' . $intBibRetryErr + 'push_done', + ($intErrSum === 0) ? 'ok' : 'error', + 'fin identité — ok=' . $intOkSum . ' err=' . $intErrSum + . ' exclus=' . $intSkipped + . ' payload_avec_pays=' . $intPaysSent + . ' payload_sans_pays=' . $intPaysMiss + . (intval($arrCovJob['no_pay_id'] ?? 0) > 0 + ? (' ms1_sans_pay_id=' . intval($arrCovJob['no_pay_id'])) + : '') + . (count($tabErrors) > 0 + ? (' — erreurs: ' . implode(' | ', array_slice($tabErrors, 0, 5))) + : '') ); + fxChronotrackApiSyncPushJobClear($intEveId); + } else { + fxChronotrackApiSyncLog( + $intEveId, + 0, + 'push_chunk', + 'ok', + 'identité terminée — passe dossards séparée pending=' . $intBibPending + ); + fxChronotrackApiSyncPushJobSave($intEveId, $arrJob); } - if ($intErrSum === 0) { - $blnLastPushSaved = fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum); - } - // MSIN-4574 — résumé final visible dans « Voir les logs » - $intPaysSent = 0; - $intPaysMiss = 0; - foreach ($tabBatchAll as $arrP) { - if (!is_array($arrP)) { - continue; - } - if (fxChronotrackApiSyncCountryFromPayload($arrP) !== '') { - $intPaysSent++; - } else { - $intPaysMiss++; - } - } - $arrCovJob = is_array($arrJob['country_coverage'] ?? null) ? $arrJob['country_coverage'] : array(); - fxChronotrackApiSyncLog( - $intEveId, - 0, - 'push_done', - ($intErrSum === 0) ? 'ok' : 'error', - 'fin — ok=' . $intOkSum . ' err=' . $intErrSum - . ' exclus=' . $intSkipped - . ' payload_avec_pays=' . $intPaysSent - . ' payload_sans_pays=' . $intPaysMiss - . (intval($arrCovJob['no_pay_id'] ?? 0) > 0 - ? (' ms1_sans_pay_id=' . intval($arrCovJob['no_pay_id'])) - : '') - . (count($tabErrors) > 0 - ? (' — erreurs: ' . implode(' | ', array_slice($tabErrors, 0, 5))) - : '') - ); - fxChronotrackApiSyncPushJobClear($intEveId); } else { + $arrJob['phase'] = 'identity'; fxChronotrackApiSyncPushJobSave($intEveId, $arrJob); } - $strState = ($intErr === 0 && $intBibRetryErr === 0) + $strState = ($intErr === 0) ? 'ok' - : ((($intOk + $intBibRetryOk) > 0) ? 'partial' : 'error'); - $strMessage = ($intErr === 0 && $intBibRetryErr === 0) + : (($intOk > 0) ? 'partial' : 'error'); + $strMessage = ($intErr === 0) ? ($intOk . ' envoyé(s) (offset ' . $intOffset . ').') - : ($intOk . ' OK, ' . $intErr . ' erreur(s) sur ce lot.' - . ($intBibRetryErr > 0 ? (' Retry dossards : ' . $intBibRetryErr . ' échec(s).') : '')); + : ($intOk . ' OK, ' . $intErr . ' erreur(s) sur ce lot.'); + if ($blnIdentityDone && $intBibPending > 0) { + $strMessage .= ' Identité OK — dossards en file (' . $intBibPending . ').'; + } return array( 'state' => $strState, 'message' => $strMessage, - 'pushed_ok' => $intOk + $intBibRetryOk, + 'phase' => ($blnIdentityDone && $intBibPending > 0) ? 'bibs' : (($blnIdentityDone) ? 'done' : 'identity'), + 'pushed_ok' => $intOk, 'pushed_put' => $intPutOk, - 'pushed_post' => $intPostOk + $intBibRetryOk, - 'pushed_error' => $intErr + $intBibRetryErr, + 'pushed_post' => $intPostOk, + 'pushed_error' => $intErr, 'job_ok_sum' => $intOkSum, 'job_err_sum' => $intErrSum, 'last_push_saved' => $blnLastPushSaved, @@ -3150,7 +3128,181 @@ function fxChronotrackApiSyncPushEvent($intEveId, $arrOptions = array()) { 'next_offset' => $intNextOffset, 'chunk_done' => $intChunkCount, 'chunk_size' => $intLimit, - 'done' => $blnDone, + 'done' => ($blnIdentityDone && $intBibPending === 0), + 'identity_done' => $blnIdentityDone, + 'bib_pending' => $intBibPending, + 'bib_chunk_size' => MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE, + ); +} + +/** + * MSIN-4574 — passe dossards découpée (après identité), un sous-lot par requête AJAX. + */ +function fxChronotrackApiSyncPushBibsPhase($intEveId, $arrOptions = array()) { + $intEveId = intval($intEveId); + $intLimit = max(1, intval($arrOptions['limit'] ?? 0)); + if ($intLimit <= 0) { + $intLimit = MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE; + } + + $arrJob = fxChronotrackApiSyncPushJobLoad($intEveId); + if (!is_array($arrJob)) { + return array( + 'state' => 'error', + 'message' => 'Aucun job dossards en cours (relancer Tout renvoyer).', + 'done' => true, + 'bib_pending' => 0, + ); + } + + $intCtEventId = intval($arrJob['ct_event_id'] ?? 0); + $tabBibRetryJob = isset($arrJob['bib_retry']) && is_array($arrJob['bib_retry']) + ? $arrJob['bib_retry'] + : array(); + $intPendingBefore = count($tabBibRetryJob); + + if ($intCtEventId <= 0) { + fxChronotrackApiSyncPushJobClear($intEveId); + return array('state' => 'error', 'message' => 'ct_event_id manquant dans le job', 'done' => true); + } + + if ($intPendingBefore === 0) { + $intOkSum = intval($arrJob['ok_sum'] ?? 0); + $intErrSum = intval($arrJob['err_sum'] ?? 0); + if ($intErrSum === 0) { + fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum); + } + fxChronotrackApiSyncLog($intEveId, 0, 'push_done', ($intErrSum === 0) ? 'ok' : 'error', + 'fin — ok=' . $intOkSum . ' err=' . $intErrSum . ' (pas de dossards en file)'); + fxChronotrackApiSyncPushJobClear($intEveId); + return array( + 'state' => 'ok', + 'message' => 'Aucun dossard en file — terminé.', + 'done' => true, + 'bib_pending' => 0, + 'pushed_ok' => 0, + 'pushed_error' => 0, + 'phase' => 'done', + ); + } + + $tabSlice = array_slice($tabBibRetryJob, 0, $intLimit); + $tabRest = array_slice($tabBibRetryJob, $intLimit); + $tabErrors = array(); + $intOk = 0; + $intErr = 0; + $tabRequeue = array(); + + foreach ($tabSlice as $arrItem) { + if (!is_array($arrItem) || !is_array($arrItem['payload'] ?? null)) { + continue; + } + $arrPayload = $arrItem['payload']; + $arrMeta = is_array($arrItem['meta'] ?? null) ? $arrItem['meta'] : array(); + $strBib = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : ''; + if ($strBib === '') { + continue; + } + $arrRes = fxChronotrackApiSyncPushOneEntryWithBibConflictHandling( + $intCtEventId, + $arrPayload, + $arrMeta, + $intEveId + ); + if (intval($arrRes['ok'] ?? 0) > 0 && empty($arrRes['bib_retry'])) { + $intOk++; + continue; + } + if (!empty($arrRes['bib_retry']) && is_array($arrRes['bib_retry'])) { + foreach ($arrRes['bib_retry'] as $arrR) { + if (is_array($arrR)) { + $tabRequeue[] = $arrR; + } + } + continue; + } + $intErr++; + $strMsg = 'RETRY_BIB external_id=' . ($arrMeta['external_id'] ?? '') + . ' dossard=' . $strBib . ' — échec'; + if (count($tabErrors) < 12) { + $tabErrors[] = $strMsg; + } + } + + // Réessayer les conflits en fin de file (swaps) — max tours gérés par UI via job.bib_round + $intBibRound = intval($arrJob['bib_round'] ?? 0); + $tabNewPending = array_merge($tabRest, $tabRequeue); + if (count($tabRequeue) > 0 && count($tabRest) === 0) { + $intBibRound++; + $arrJob['bib_round'] = $intBibRound; + } + // Après trop de tours sans drain : abandonner le reste comme erreurs + if ($intBibRound >= 5 && count($tabRequeue) > 0 && count($tabRest) === 0) { + foreach ($tabRequeue as $arrLeft) { + $intErr++; + $arrMetaL = is_array($arrLeft['meta'] ?? null) ? $arrLeft['meta'] : array(); + $strBibL = isset($arrLeft['payload']['bib']) ? trim((string)$arrLeft['payload']['bib']) : ''; + $tabErrors[] = 'RETRY_BIB bloqué (swap max) external_id=' + . ($arrMetaL['external_id'] ?? '') . ' dossard=' . $strBibL; + } + $tabNewPending = array(); + } + + $arrJob['bib_retry'] = $tabNewPending; + $arrJob['ok_sum'] = intval($arrJob['ok_sum'] ?? 0) + $intOk; + $arrJob['err_sum'] = intval($arrJob['err_sum'] ?? 0) + $intErr; + $intPendingAfter = count($tabNewPending); + $blnDone = ($intPendingAfter === 0); + + fxChronotrackApiSyncLog( + $intEveId, + 0, + 'push_bib_retry', + ($intErr === 0) ? 'ok' : 'error', + 'slice n=' . count($tabSlice) + . ' ok=' . $intOk . ' err=' . $intErr + . ' requeue=' . count($tabRequeue) + . ' pending=' . $intPendingAfter + . ' round=' . $intBibRound + ); + + if ($blnDone) { + $intOkSum = intval($arrJob['ok_sum'] ?? 0); + $intErrSum = intval($arrJob['err_sum'] ?? 0); + if ($intErrSum === 0) { + fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum); + } + fxChronotrackApiSyncLog( + $intEveId, + 0, + 'push_done', + ($intErrSum === 0) ? 'ok' : 'error', + 'fin — ok=' . $intOkSum . ' err=' . $intErrSum . ' (identité + dossards)' + ); + fxChronotrackApiSyncPushJobClear($intEveId); + } else { + $arrJob['phase'] = 'bibs'; + fxChronotrackApiSyncPushJobSave($intEveId, $arrJob); + } + + return array( + 'state' => ($intErr === 0) ? 'ok' : (($intOk > 0) ? 'partial' : 'error'), + 'message' => 'Dossards : ' . $intOk . ' OK' + . ($intErr > 0 ? (', ' . $intErr . ' erreur(s)') : '') + . ' — reste ' . $intPendingAfter, + 'phase' => $blnDone ? 'done' : 'bibs', + 'pushed_ok' => $intOk, + 'pushed_post' => $intOk, + 'pushed_put' => 0, + 'pushed_error' => $intErr, + 'job_ok_sum' => intval($arrJob['ok_sum'] ?? 0), + 'job_err_sum' => intval($arrJob['err_sum'] ?? 0), + 'errors' => array_slice($tabErrors, 0, 20), + 'done' => $blnDone, + 'identity_done' => true, + 'bib_pending' => $intPendingAfter, + 'bib_chunk_size' => $intLimit, + 'bib_before' => $intPendingBefore, ); } @@ -3216,40 +3368,79 @@ function fxChronotrackApiSyncComputeCtOrphanRows(array $tabEntities, array $tabE /** * Entries CT présentes sur l'événement mais absentes du jeu éligible MS1 (lié, doublon, sans ext). + * MSIN-4574 — version légère : pas de build payloads/notes (sinon gèle à ~1300). */ function fxChronotrackApiSyncListCtOrphans($intEveId) { - $arrPreview = fxChronotrackApiSyncBuildPreview($intEveId); - if ($arrPreview['state'] !== 'ok') { - return $arrPreview; + $intEveId = intval($intEveId); + $arrConfig = fxChronotrackApiConfigGet($intEveId); + if ($arrConfig === null) { + return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack'); + } + if (!fxChronotrackApiSettingsConfigured()) { + return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage()); } - $intCtEventId = intval($arrPreview['ct_event_id']); + $intCtEventId = intval($arrConfig['ct_event_id']); $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); - $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); + // Pas de payloads / entry_notes — uniquement les clés external_id éligibles + $arrClass = fxChronotrackApiSyncClassifyParticipants( + $tabParticipants, + $arrRaceMap, + $intCtEventId, + false + ); $tabEligibleKeys = array(); foreach ($arrClass['transferable'] as $arrItem) { - $tabEligibleKeys[$arrItem['payload']['external_id']] = true; + if (!is_array($arrItem)) { + continue; + } + $strExt = trim((string)($arrItem['summary']['external_id'] ?? '')); + if ($strExt === '' || $strExt === '0') { + $strExt = (string)intval($arrItem['row']['par_id_original'] ?? 0); + } + if ($strExt !== '' && $strExt !== '0') { + $tabEligibleKeys[$strExt] = true; + } } + $floatStart = microtime(true); $arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId); + $floatMs = (int)round((microtime(true) - $floatStart) * 1000); if ($arrFetch['state'] !== 'ok') { return array( 'state' => 'error', 'message' => $arrFetch['message'] ?? 'Erreur lecture entries CT', + 'elapsed_ms' => $floatMs, ); } $tabOrphans = fxChronotrackApiSyncComputeCtOrphanRows($arrFetch['entities'], $tabEligibleKeys); + $intCtTotal = intval($arrFetch['count'] ?? count($arrFetch['entities'])); + + fxChronotrackApiSyncLog( + $intEveId, + 0, + 'reconcile', + 'ok', + 'list orphans=' . count($tabOrphans) + . ' ct_total=' . $intCtTotal + . ' eligible_keys=' . count($tabEligibleKeys) + . ' ms=' . $floatMs + ); return array( - 'state' => 'ok', - 'ct_event_id' => $intCtEventId, - 'ct_total' => intval($arrPreview['ct_total'] ?? 0), - 'linked_count' => intval($arrPreview['already_in_ct'] ?? 0), - 'count' => count($tabOrphans), - 'orphans' => $tabOrphans, + 'state' => 'ok', + 'ct_event_id' => $intCtEventId, + 'ct_total' => $intCtTotal, + 'linked_count' => count($tabEligibleKeys), + 'eligible_count' => count($arrClass['transferable']), + 'count' => count($tabOrphans), + 'orphans' => $tabOrphans, + 'elapsed_ms' => $floatMs, + 'message' => count($tabOrphans) . ' orphelin(s) sur ' . $intCtTotal . ' entries CT' + . ' (' . round($floatMs / 1000, 1) . ' s)', ); } @@ -3597,6 +3788,25 @@ function fxChronotrackApiSyncRunAutoPushForEvent($intEveId) { if (!empty($arrChunk['done'])) { break; } + // Identité finie → drain dossards en sous-lots + if (!empty($arrChunk['identity_done']) && intval($arrChunk['bib_pending'] ?? 0) > 0) { + $intBibLoops = 0; + while ($intBibLoops < $intMaxLoops) { + $intBibLoops++; + $arrBib = fxChronotrackApiSyncPushBibsPhase($intEveId, array( + 'limit' => MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE, + )); + $intOkSum += intval($arrBib['pushed_ok'] ?? 0); + $intErrSum += intval($arrBib['pushed_error'] ?? 0); + if (!empty($arrBib['done'])) { + break; + } + if (($arrBib['state'] ?? '') === 'error' && intval($arrBib['pushed_ok'] ?? 0) === 0) { + break; + } + } + break; + } if (($arrChunk['state'] ?? '') === 'error' && intval($arrChunk['pushed_ok'] ?? 0) === 0) { break; } diff --git a/php/inc_settings.php b/php/inc_settings.php index 5183bbfc..0be83a49 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.971'); // MSIN-4574 — logs/alertes pays + location_country +define('_VERSION_CODE', '4.72.974'); // MSIN-4574 — alignement léger + sync rapide + location_country define('_DATE_CODE', '2026-08-11'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); diff --git a/superadm/ajax_chronotrack_api.php b/superadm/ajax_chronotrack_api.php index 95d911ca..552ad785 100644 --- a/superadm/ajax_chronotrack_api.php +++ b/superadm/ajax_chronotrack_api.php @@ -212,7 +212,7 @@ switch ($strAction) { $tabRetour = array('state' => 'error', 'message' => 'eve_id manquant'); break; } - set_time_limit(300); + set_time_limit(180); $intOffset = max(0, intval($_REQUEST['offset'] ?? 0)); $intLimit = max(0, intval($_REQUEST['limit'] ?? 0)); if ($intLimit <= 0) { @@ -231,12 +231,35 @@ switch ($strAction) { } break; - case 'sync_orphans': + case 'sync_push_bibs': + // MSIN-4574 — passe dossards découpée (évite 503 hébergeur en fin de resync) if ($intEveId <= 0) { $tabRetour = array('state' => 'error', 'message' => 'eve_id manquant'); break; } set_time_limit(180); + $intLimit = max(0, intval($_REQUEST['limit'] ?? 0)); + if ($intLimit <= 0) { + $intLimit = MSIN_API_CHRONOTRACK_SYNC_BIB_CHUNK_SIZE; + } + try { + $tabRetour = fxChronotrackApiSyncPushBibsPhase($intEveId, array( + 'limit' => $intLimit, + )); + } catch (Throwable $e) { + $tabRetour = array( + 'state' => 'error', + 'message' => 'Exception dossards : ' . $e->getMessage(), + ); + } + break; + + case 'sync_orphans': + if ($intEveId <= 0) { + $tabRetour = array('state' => 'error', 'message' => 'eve_id manquant'); + break; + } + set_time_limit(240); $tabRetour = fxChronotrackApiSyncListCtOrphans($intEveId); break;