';
html += '
';
- html += 'Alignement ChronoTrack ';
+ html += 'Alignement ChronoTrack pas le transfert ';
html += 'Fermer ';
html += '
';
- html += '
Entries présentes sur ChronoTrack mais absentes des éligibles MS1 '
+ html += '
Lecture seule + retrait optionnel. '
+ + 'Ce cadre n’est pas le résultat de Tout renvoyer. '
+ + 'Entries présentes sur ChronoTrack mais absentes des éligibles MS1 '
+ '(annulés, transferts, fantômes après changement d’équipe / coureur).
';
if (!res || res.state !== 'ok') {
html += '
' + escHtml((res && res.message) ? res.message : 'Erreur') + '
';
@@ -2383,7 +2405,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
state: strState,
message: strFinal,
errors: tabErrAll.slice(0, 20)
- });
+ }, blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer');
if ($status.length) {
var strCls = strState === 'ok' ? 'ct-api-push-status--ok'
: (strState === 'partial' ? 'ct-api-push-status--todo' : '');
@@ -2503,7 +2525,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
if (res.total === 0 && res.state === 'error') {
stopElapsedTimer();
- renderPushResult(res);
+ renderPushResult(res, blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer');
$('#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;
@@ -2614,7 +2636,10 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
if (!prep || prep.state !== 'ok') {
stopElapsedTimer();
- renderPushResult(prep || { state: 'error', message: 'Préparation échouée' });
+ renderPushResult(
+ prep || { state: 'error', message: 'Préparation échouée' },
+ blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer'
+ );
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading')
.html(
@@ -2644,7 +2669,10 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
strMsg += ' — ' + String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
}
strMsg += '. Vérifiez que le code MSIN-4328 est déployé (action sync_push_prepare).';
- renderPushResult({ state: 'error', message: strMsg });
+ renderPushResult(
+ { state: 'error', message: strMsg },
+ blnForceFull ? 'Résultat — Tout renvoyer' : 'Résultat — Envoyer'
+ );
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo ct-api-push-status--loading')
.html(
diff --git a/php/chronotrack_api/fx_chronotrack_sync.php b/php/chronotrack_api/fx_chronotrack_sync.php
index 65d793f4..202c6e1a 100644
--- a/php/chronotrack_api/fx_chronotrack_sync.php
+++ b/php/chronotrack_api/fx_chronotrack_sync.php
@@ -579,6 +579,163 @@ function fxChronotrackApiSyncPayloadClearBib(array $arrPayload) {
return $arrOut;
}
+/**
+ * MSIN-4574 — comparer dossards CT/MS1 (2342 == 02342).
+ */
+function fxChronotrackApiSyncBibNorm($strBib) {
+ $str = trim((string)$strBib);
+ if ($str === '') {
+ return '';
+ }
+ if (ctype_digit($str)) {
+ return (string)intval($str);
+ }
+ return strtolower($str);
+}
+
+/**
+ * MSIN-4574 — conflit bib : trouver qui détient déjà ce dossard sur CT et le vider.
+ * Sans ça, on ne libère que « nous » → boucle swap max. Automatique, pas manuel.
+ *
+ * @return array{cleared:int,holders:array,message:string}
+ */
+function fxChronotrackApiSyncClearOtherBibHolderOnCt(
+ $intCtEventId,
+ $strBib,
+ $strExceptExternalId = '',
+ $strExceptEntryId = '',
+ $intEveId = 0,
+ $blnForceRefresh = false
+) {
+ $intCtEventId = intval($intCtEventId);
+ $intEveId = intval($intEveId);
+ $strBibWanted = fxChronotrackApiSyncBibNorm($strBib);
+ $strExceptExternalId = trim((string)$strExceptExternalId);
+ $strExceptEntryId = trim((string)$strExceptEntryId);
+ $arrOut = array('cleared' => 0, 'holders' => array(), 'message' => '');
+
+ if ($intCtEventId <= 0 || $strBibWanted === '') {
+ $arrOut['message'] = 'paramètres manquants';
+ return $arrOut;
+ }
+
+ static $tabEntitiesByEvent = array();
+ $strCacheKey = (string)$intCtEventId;
+ if ($blnForceRefresh || !isset($tabEntitiesByEvent[$strCacheKey])) {
+ $arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId);
+ if (($arrFetch['state'] ?? '') !== 'ok') {
+ $arrOut['message'] = (string)($arrFetch['message'] ?? 'lecture CT échouée');
+ return $arrOut;
+ }
+ $tabEntitiesByEvent[$strCacheKey] = is_array($arrFetch['entities'] ?? null)
+ ? $arrFetch['entities']
+ : array();
+ }
+
+ $tabEntities = &$tabEntitiesByEvent[$strCacheKey];
+ foreach ($tabEntities as $intIdx => $arrEntity) {
+ if (!is_array($arrEntity)) {
+ continue;
+ }
+ $strTheirBib = fxChronotrackApiSyncBibNorm(
+ fxChronotrackApiRaceField($arrEntity, array('bib', 'bib_number', 'entry_bib'))
+ );
+ if ($strTheirBib === '' || $strTheirBib !== $strBibWanted) {
+ continue;
+ }
+ $strTheirEntryId = fxChronotrackApiEntityId($arrEntity);
+ $strTheirExt = fxChronotrackApiEntityExternalId($arrEntity);
+ if ($strExceptEntryId !== '' && $strTheirEntryId !== '' && $strTheirEntryId === $strExceptEntryId) {
+ continue;
+ }
+ if ($strExceptExternalId !== '' && $strTheirExt !== '' && $strTheirExt === $strExceptExternalId) {
+ continue;
+ }
+ if ($strTheirEntryId === '') {
+ continue;
+ }
+
+ $arrClearPayload = array(
+ 'entry_id' => $strTheirEntryId,
+ 'external_id' => $strTheirExt !== '' ? $strTheirExt : '0',
+ 'event_id' => $intCtEventId,
+ 'bib' => '',
+ );
+ $intRaceId = intval(fxChronotrackApiRaceField($arrEntity, array('race_id', 'ct_race_id')));
+ if ($intRaceId > 0) {
+ $arrClearPayload['race_id'] = $intRaceId;
+ }
+ $strStatus = trim((string)fxChronotrackApiRaceField(
+ $arrEntity,
+ array('entry_status', 'status', 'reg_status')
+ ));
+ if ($strStatus !== '') {
+ $arrClearPayload['entry_status'] = $strStatus;
+ $arrClearPayload['status'] = $strStatus;
+ }
+
+ $arrWrite = fxChronotrackApiSyncPutEntry($strTheirEntryId, $arrClearPayload);
+ $arrOut['holders'][] = array(
+ 'entry_id' => $strTheirEntryId,
+ 'external_id' => $strTheirExt,
+ 'name' => fxChronotrackApiEntityEntryLabel($arrEntity),
+ 'ok' => (($arrWrite['state'] ?? '') === 'ok'),
+ 'message' => (string)($arrWrite['message'] ?? ''),
+ );
+ if (($arrWrite['state'] ?? '') === 'ok') {
+ $arrOut['cleared']++;
+ // Cache : dossard libéré pour les prochains conflits du même job
+ $tabEntities[$intIdx]['bib'] = '';
+ if ($intEveId > 0) {
+ fxChronotrackApiSyncLog(
+ $intEveId,
+ 0,
+ 'bib_holder_clear',
+ 'ok',
+ 'dossard=' . $strBibWanted
+ . ' retiré de entry_id=' . $strTheirEntryId
+ . ' external_id=' . $strTheirExt
+ . ' nom=' . fxChronotrackApiEntityEntryLabel($arrEntity)
+ );
+ }
+ } elseif ($intEveId > 0) {
+ fxChronotrackApiSyncLog(
+ $intEveId,
+ 0,
+ 'bib_holder_clear',
+ 'error',
+ 'dossard=' . $strBibWanted
+ . ' entry_id=' . $strTheirEntryId
+ . ' — ' . ($arrWrite['message'] ?? 'échec')
+ );
+ }
+ }
+
+ $arrOut['message'] = ($arrOut['cleared'] > 0)
+ ? ('détenteur(s) libéré(s)=' . $arrOut['cleared'])
+ : 'aucun autre détenteur trouvé';
+ return $arrOut;
+}
+
+/**
+ * MSIN-4574 — message d’échec bib lisible (UI / logs), pas du jargon RETRY_BIB.
+ */
+function fxChronotrackApiSyncHumanBibBlockedMessage(array $arrMeta, $strBib) {
+ $strName = trim((string)($arrMeta['name'] ?? ''));
+ $strExt = trim((string)($arrMeta['external_id'] ?? ''));
+ $strBib = trim((string)$strBib);
+ $str = 'Dossard ' . ($strBib !== '' ? $strBib : '?')
+ . ' encore pris après libération auto du détenteur ChronoTrack';
+ if ($strName !== '') {
+ $str .= ' — ' . $strName;
+ }
+ if ($strExt !== '') {
+ $str .= ' (ext ' . $strExt . ')';
+ }
+ $str .= '. Relancer Tout renvoyer — le système réessaie automatiquement.';
+ return $str;
+}
+
function fxChronotrackApiSyncParticipantLabel(array $arrRow) {
return trim(trim((string)($arrRow['par_prenom'] ?? '')) . ' ' . trim((string)($arrRow['par_nom'] ?? '')));
}
@@ -1688,8 +1845,41 @@ function fxChronotrackApiSyncPushOneEntryWithBibConflictHandling(
}
$strErr = (string)($arrFirst['message'] ?? 'Erreur entry');
- // Conflit dossard + on voulait en envoyer un → libérer puis 2e passe
+ // Conflit dossard + on voulait en envoyer un → libérer le détenteur CT, réessayer, sinon clear + 2e passe
if ($strBibWanted !== '' && fxChronotrackApiSyncIsBibConflictMessage($strErr)) {
+ // MSIN-4574 — auto : vider le dossard chez QUI le détient déjà sur CT (pas seulement chez nous)
+ fxChronotrackApiSyncClearOtherBibHolderOnCt(
+ $intCtEventId,
+ $strBibWanted,
+ (string)($arrMeta['external_id'] ?? ''),
+ $strEntryId,
+ $intEveId,
+ false
+ );
+ // Réessai immédiat avec le dossard voulu (souvent OK si le détenteur était un orphelin / autre)
+ if ($strEntryId !== '') {
+ $arrAfterHolder = fxChronotrackApiSyncPutEntry($strEntryId, $arrPayload);
+ } else {
+ $arrAfterHolder = fxChronotrackApiSyncPostEntries(
+ $intCtEventId,
+ array(fxChronotrackApiSyncPayloadForPost($arrPayload))
+ );
+ }
+ if (($arrAfterHolder['state'] ?? '') === 'ok') {
+ if ($intEveId > 0) {
+ if ($strMethod === 'POST' && $strEntryId === '') {
+ fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrAfterHolder['json'] ?? null, array($arrMeta));
+ }
+ fxChronotrackApiSyncLogPushEntryOk(
+ $intEveId,
+ $arrMeta,
+ $arrPayload,
+ $strMethod . '+holder_clear'
+ );
+ }
+ return array('ok' => 1, 'err' => 0, 'bib_retry' => array(), 'method' => $strMethod);
+ }
+
$arrCleared = fxChronotrackApiSyncPayloadClearBib($arrPayload);
if ($strEntryId !== '') {
$arrClearRes = fxChronotrackApiSyncPutEntry($strEntryId, $arrCleared);
@@ -1837,8 +2027,32 @@ function fxChronotrackApiSyncPostEntriesAdaptive($intCtEventId, array $tabEntrie
$strBibWanted = isset($arrPayload['bib']) ? trim((string)$arrPayload['bib']) : '';
if ($strBibWanted !== '' && fxChronotrackApiSyncIsBibConflictMessage($strOneErr)) {
- // MSIN-4328 — conflit sans entry_id : tenter clear bib='' puis sans champ bib
+ // MSIN-4574 — d’abord libérer le détenteur CT (auto)
$strEntryIdOne = trim((string)($arrPayload['entry_id'] ?? ''));
+ fxChronotrackApiSyncClearOtherBibHolderOnCt(
+ $intCtEventId,
+ $strBibWanted,
+ (string)($arrM['external_id'] ?? ''),
+ $strEntryIdOne,
+ $intEveId,
+ false
+ );
+ $arrAfterHolder = ($strEntryIdOne !== '')
+ ? fxChronotrackApiSyncPutEntry($strEntryIdOne, $arrPayload)
+ : fxChronotrackApiSyncPostEntries(
+ $intCtEventId,
+ array(fxChronotrackApiSyncPayloadForPost($arrPayload))
+ );
+ if (($arrAfterHolder['state'] ?? '') === 'ok') {
+ if ($intEveId > 0) {
+ if ($strEntryIdOne === '') {
+ fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrAfterHolder['json'] ?? null, array($arrM));
+ }
+ fxChronotrackApiSyncLogPushEntryOk($intEveId, $arrM, $arrPayload, 'POST+holder_clear');
+ }
+ return array('ok' => 1, 'err' => 0, 'bib_retry' => array());
+ }
+ // MSIN-4328 — conflit sans entry_id : tenter clear bib='' puis sans champ bib
if ($strEntryIdOne !== '') {
$arrClearRes = fxChronotrackApiSyncPutEntry(
$strEntryIdOne,
@@ -3608,16 +3822,45 @@ function fxChronotrackApiSyncPushBibsPhase($intEveId, $arrOptions = array()) {
$intBibRound++;
$arrJob['bib_round'] = $intBibRound;
}
- // Après trop de tours sans drain : abandonner le reste comme erreurs
+ // Après trop de tours sans drain : 1 dernière passe (refresh CT + clear détenteur), sinon erreur claire
if ($intBibRound >= 5 && count($tabRequeue) > 0 && count($tabRest) === 0) {
+ $tabStillBlocked = array();
foreach ($tabRequeue as $arrLeft) {
- $intErr++;
+ if (!is_array($arrLeft) || !is_array($arrLeft['payload'] ?? null)) {
+ continue;
+ }
+ $arrPayloadL = $arrLeft['payload'];
$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;
+ $strBibL = isset($arrPayloadL['bib']) ? trim((string)$arrPayloadL['bib']) : '';
+ $strEntryL = trim((string)($arrPayloadL['entry_id'] ?? ''));
+ if ($strBibL !== '') {
+ fxChronotrackApiSyncClearOtherBibHolderOnCt(
+ $intCtEventId,
+ $strBibL,
+ (string)($arrMetaL['external_id'] ?? ''),
+ $strEntryL,
+ $intEveId,
+ true
+ );
+ $arrLast = fxChronotrackApiSyncPushOneEntryWithBibConflictHandling(
+ $intCtEventId,
+ $arrPayloadL,
+ $arrMetaL,
+ $intEveId
+ );
+ if (intval($arrLast['ok'] ?? 0) > 0 && empty($arrLast['bib_retry'])) {
+ $intOk++;
+ continue;
+ }
+ }
+ $intErr++;
+ $tabStillBlocked[] = $arrLeft;
+ if (count($tabErrors) < 12) {
+ $tabErrors[] = fxChronotrackApiSyncHumanBibBlockedMessage($arrMetaL, $strBibL);
+ }
}
$tabNewPending = array();
+ unset($tabStillBlocked);
}
$arrJob['bib_retry'] = $tabNewPending;
diff --git a/php/inc_settings.php b/php/inc_settings.php
index dd0578d2..65d142bd 100644
--- a/php/inc_settings.php
+++ b/php/inc_settings.php
@@ -7,7 +7,7 @@
* Constantes *
*
**************/
-define('_VERSION_CODE', '4.72.980'); // MSIN-4574 — pays CT = ISO-2 (CA) + province QC (calibrage export)
+define('_VERSION_CODE', '4.72.981'); // MSIN-4574 — conflit bib: clear détenteur CT auto + messages UI clairs
define('_DATE_CODE', '2026-08-11');
//MSIN-4290
define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');