MSIN-4328 — Introduce a new button for full resync in the ChronoTrack admin panel, allowing all eligible participants to be resent regardless of last push date. Update JavaScript to handle the new button's functionality and enhance logging for successful entries. Adjust push preparation logic to accommodate full resync options. Increment version code.

This commit is contained in:
2026-07-22 15:19:51 -04:00
parent 23def197c8
commit 847f2f57a7
3 changed files with 94 additions and 28 deletions

View File

@ -327,6 +327,9 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar
echo '<i class="fa fa-refresh" aria-hidden="true"></i>&nbsp;Actualiser</button> ';
echo '<button type="button" class="btn btn-primary btn-lg" id="ct_api_btn_push" disabled>';
echo '<i class="fa fa-cloud-upload" aria-hidden="true"></i>&nbsp;Envoyer vers ChronoTrack</button> ';
echo '<button type="button" class="btn btn-outline-warning" id="ct_api_btn_push_full" disabled'
. ' title="Renvoie tous les éligibles, sans tenir compte de la date du dernier envoi">';
echo '<i class="fa fa-refresh" aria-hidden="true"></i>&nbsp;Tout renvoyer</button> ';
echo '<button type="button" class="btn btn-outline-dark btn-sm" id="ct_api_btn_logs">';
echo '<i class="fa fa-list" aria-hidden="true"></i>&nbsp;Voir les logs</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_auto_diag">'
@ -334,7 +337,9 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_cron_ping" title="Appelle auto_chronotrack_sync.php sur ce même domaine">'
. 'Lancer le script cron</button>';
echo '</div>';
echo '<p class="small text-muted mb-2">Envoi manuel : nouveaux / modifiés depuis le dernier push. '
echo '<p class="small text-muted mb-2"><strong>Envoyer</strong> = seulement les nouveaux / modifiés depuis le dernier push. '
. '<strong>Tout renvoyer</strong> = tous les éligibles, dates ignorées. '
. 'Chaque envoi est journalisé (Voir les logs). '
. 'Cet écran se rafraîchit doucement chaque minute (statut MS1 seulement).</p>';
echo '<div id="ct_api_push_result" class="small mb-3"></div>';
echo '<div id="ct_api_logs_wrap" class="mb-3 d-none"></div>';
@ -1010,6 +1015,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
var $sum = $('#ct_api_push_summary');
var $status = $('#ct_api_push_status');
var $btn = $('#ct_api_btn_push');
var $btnFull = $('#ct_api_btn_push_full');
objPushPreview = res;
if (!$sum.length && !$status.length) {
return;
@ -1024,6 +1030,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
$('#ct_api_push_blocked_wrap').addClass('d-none').empty();
$btn.prop('disabled', true);
$btnFull.prop('disabled', true);
return;
}
@ -1087,7 +1094,8 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
$sum.html(html);
renderBlockedPanel(res);
$btn.prop('disabled', intPending <= 0);
$btn.prop('disabled', intPending <= 0 || blnClosed || blnPushRunning);
$btnFull.prop('disabled', intEligible <= 0 || blnClosed || blnPushRunning);
// MSIN-4328 — auto-réparation au load : last_push déjà posé mais 100 % des
// éligibles « à pousser » (= faux différentiel, ex. par_maj bumpé par backfill entry_id).
@ -1341,12 +1349,13 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
}
var tabLogs = res.logs || [];
var html = '<div class="card border-secondary"><div class="card-header py-2 d-flex justify-content-between align-items-center">';
html += '<strong class="small">Logs sync (50 derniers)</strong>';
html += '<strong class="small">Logs sync (200 derniers)</strong>';
html += '<button type="button" class="btn btn-link btn-sm p-0" id="ct_api_btn_logs_close">Fermer</button>';
html += '</div><div class="card-body py-2">';
if (!tabLogs.length) {
html += '<p class="small text-muted mb-0">Aucun log pour cet événement.</p>';
} else {
html += '<p class="small text-muted mb-2">Chaque participant envoyé = ligne <code>push_entry</code> (ok ou error) avec external_id / dossard / nom.</p>';
html += '<div class="ct-api-logs-scroll border rounded">';
html += '<table class="table table-sm table-striped mb-0" style="font-size:0.78rem;">';
html += '<thead class="thead-light"><tr><th>Quand</th><th>Action</th><th>Statut</th><th>par_id</th><th>Message</th></tr></thead><tbody>';
@ -1369,7 +1378,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
function loadSyncLogs() {
var $wrap = $('#ct_api_logs_wrap');
$wrap.removeClass('d-none').html(waitHtml('Chargement des logs…'));
$.post(strAjaxUrl, { a: 'sync_logs', eve_id: intEveId, limit: 50 }, function (res) {
$.post(strAjaxUrl, { a: 'sync_logs', eve_id: intEveId, limit: 200 }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
@ -1492,17 +1501,23 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
});
});
$('#ct_api_btn_push').on('click', function () {
// MSIN-4328 — envoi manuel (différentiel) ou resync complète (force_full)
function runManualPush(blnForceFull) {
var intPending = 0;
if (objPushPreview) {
if (typeof objPushPreview.pending_push !== 'undefined') {
if (blnForceFull) {
intPending = objPushPreview.transferable || 0;
} else if (typeof objPushPreview.pending_push !== 'undefined') {
intPending = objPushPreview.pending_push;
} else {
intPending = objPushPreview.to_push || objPushPreview.transferable || 0;
}
}
// MSIN-4328 — push immédiat, sans confirm
var $btn = $(this);
if (blnForceFull) {
if (!window.confirm('Tout renvoyer vers ChronoTrack ?\n\nTous les participants éligibles seront renvoyés, sans tenir compte des dates du dernier envoi.')) {
return;
}
}
var $res = $('#ct_api_push_result');
var intChunk = 100;
var intOffset = 0;
@ -1514,13 +1529,14 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
var tabErrAll = [];
var blnFatal = false;
$btn.prop('disabled', true);
blnPushRunning = true;
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', true);
$('#ct_api_btn_push, #ct_api_btn_push_full, #ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', true);
function renderProgress(intDone, intTot, strExtra) {
var intPct = (intTot > 0) ? Math.min(100, Math.round((intDone * 100) / intTot)) : 0;
var strHtml = '<div class="mb-1"><strong>Envoi</strong> — '
var strHtml = '<div class="mb-1"><strong>'
+ (blnForceFull ? 'Resync complète' : 'Envoi')
+ '</strong> — '
+ intDone + ' / ' + intTot
+ (strExtra ? (' · ' + escHtml(strExtra)) : '')
+ '</div>';
@ -1540,10 +1556,12 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
function finishPush() {
blnPushRunning = false;
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
$btn.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 strState = blnFatal ? 'error' : ((intErrSum === 0) ? 'ok' : 'partial');
renderPushResult({
state: strState,
@ -1551,6 +1569,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
errors: tabErrAll.slice(0, 20)
});
loadPushPreview(true);
loadSyncLogs();
}
function pushChunk() {
@ -1577,8 +1596,6 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
finishPush();
return;
}
// MSIN-4328 — ne pas abandonner le job si un lot a des erreurs :
// continuer jusquà done (premier envoi = tous les lots).
if (res.state !== 'ok' && res.state !== 'partial' && res.state !== 'error') {
blnFatal = true;
if (res.message) {
@ -1591,7 +1608,7 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
renderPushResult(res);
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
blnPushRunning = false;
$btn.prop('disabled', false);
loadPushPreview(true);
return;
}
if (res.total) {
@ -1645,7 +1662,11 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
type: 'POST',
dataType: 'json',
timeout: 180000,
data: { a: 'sync_push_prepare', eve_id: intEveId }
data: {
a: 'sync_push_prepare',
eve_id: intEveId,
force_full: blnForceFull ? 1 : 0
}
}).done(function (prep) {
if (rejectIfSessionLost(prep)) {
return;
@ -1653,15 +1674,15 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
if (!prep || prep.state !== 'ok') {
renderPushResult(prep || { state: 'error', message: 'Préparation échouée' });
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
blnPushRunning = false;
$btn.prop('disabled', false);
blnPushRunning = false;
loadPushPreview(true);
return;
}
intTotal = parseInt(prep.total, 10) || intTotal;
if (prep.chunk_size) {
intChunk = parseInt(prep.chunk_size, 10) || intChunk;
}
renderProgress(0, intTotal, 'démarrage');
renderProgress(0, intTotal, blnForceFull ? 'resync complète' : 'démarrage');
pushChunk();
}).fail(function (jqXHR, strStatus) {
var strMsg = 'Erreur préparation push (' + strStatus
@ -1673,8 +1694,15 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
renderPushResult({ state: 'error', message: strMsg });
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
blnPushRunning = false;
$btn.prop('disabled', false);
loadPushPreview(true);
});
}
$('#ct_api_btn_push').on('click', function () {
runManualPush(false);
});
$('#ct_api_btn_push_full').on('click', function () {
runManualPush(true);
});
}

View File

@ -995,6 +995,22 @@ function fxChronotrackApiSyncPostEntriesAdaptive($intCtEventId, array $tabEntrie
// MSIN-4328 — récupérer entry_id CT pour lien admin
if ($intEveId > 0 && count($tabMeta) > 0) {
fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrPost['json'] ?? null, $tabMeta);
// MSIN-4328 — log précis de chaque envoi réussi (contrôle / audit)
foreach ($tabMeta as $arrM) {
if (!is_array($arrM)) {
continue;
}
$strMsg = 'external_id=' . ($arrM['external_id'] ?? '')
. ' bib=' . ($arrM['bib'] ?? '')
. ' ' . trim((string)($arrM['name'] ?? ''));
fxChronotrackApiSyncLog(
$intEveId,
intval($arrM['par_id'] ?? 0),
'push_entry',
'ok',
trim($strMsg)
);
}
}
return array('ok' => $intCount, 'err' => 0);
}
@ -1689,9 +1705,12 @@ function fxChronotrackApiSyncPushJobClear($intEveId) {
/**
* Prépare un job de push (liste éligibles + entry_id CT). MSIN-4328.
*
* @param array $arrOptions force_full=true → tous les éligibles (ignore last_push_at)
*/
function fxChronotrackApiSyncPushPrepare($intEveId) {
function fxChronotrackApiSyncPushPrepare($intEveId, $arrOptions = array()) {
$intEveId = intval($intEveId);
$blnForceFull = !empty($arrOptions['force_full']);
$arrConfig = fxChronotrackApiConfigGet($intEveId);
if ($arrConfig === null) {
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
@ -1708,18 +1727,29 @@ function fxChronotrackApiSyncPushPrepare($intEveId) {
// MSIN-4328 — ne plus journaliser push_skip ici (spam à chaque prepare/auto).
// Les exclus restent dans lUI « Détails & exclus » (preview), jamais dans le lot poussé.
// MSIN-4328 — ne pousser que les éligibles modifiés depuis last_push_at
// MSIN-4328 — différentiel par défaut ; force_full = tout renvoyer
$strLastPushAt = trim((string)($arrConfig['last_push_at'] ?? ''));
$tabPending = fxChronotrackApiSyncFilterChangedSincePush($arrClass['transferable'], $strLastPushAt);
if ($blnForceFull) {
$tabPending = $arrClass['transferable'];
} else {
$tabPending = fxChronotrackApiSyncFilterChangedSincePush($arrClass['transferable'], $strLastPushAt);
}
// Plus de scan CT à la préparation : POST upsert (crée ou maj) sans entry_id.
$tabBatch = array();
$tabMeta = array();
foreach ($tabPending as $arrItem) {
$tabBatch[] = fxChronotrackApiSyncPayloadForPost($arrItem['payload']);
$arrPayload = fxChronotrackApiSyncPayloadForPost($arrItem['payload']);
$tabBatch[] = $arrPayload;
$tabMeta[] = array(
'par_id' => intval($arrItem['row']['par_id'] ?? 0),
'external_id' => $arrItem['payload']['external_id'],
'external_id' => $arrPayload['external_id'] ?? '',
'bib' => isset($arrPayload['bib']) ? (string)$arrPayload['bib'] : '',
'name' => trim(
trim((string)($arrPayload['first_name'] ?? ''))
. ' '
. trim((string)($arrPayload['last_name'] ?? ''))
),
);
}
@ -1738,15 +1768,17 @@ function fxChronotrackApiSyncPushPrepare($intEveId) {
'transferable' => count($arrClass['transferable']),
'pending_push' => 0,
'total' => 0,
'force_full' => $blnForceFull ? 1 : 0,
);
}
$strMode = $blnForceFull ? 'FULL resync' : 'différentiel';
fxChronotrackApiSyncLog(
$intEveId,
0,
'push_prepare',
'ok',
'job prêt — pending=' . count($tabBatch)
$strMode . ' — job prêt — pending=' . count($tabBatch)
. ' éligibles=' . count($arrClass['transferable'])
. ' exclus=' . intval($arrClass['blocked_count'])
);
@ -1756,6 +1788,7 @@ function fxChronotrackApiSyncPushPrepare($intEveId) {
'batch' => $tabBatch,
'meta' => $tabMeta,
'blocked_count' => $arrClass['blocked_count'],
'force_full' => $blnForceFull ? 1 : 0,
'ok_sum' => 0,
'err_sum' => 0,
'created_at' => time(),
@ -1769,13 +1802,15 @@ function fxChronotrackApiSyncPushPrepare($intEveId) {
return array(
'state' => 'ok',
'message' => count($tabBatch) . ' prêts à envoyer',
'message' => count($tabBatch) . ' prêts à envoyer'
. ($blnForceFull ? ' (resync complète)' : ''),
'total' => count($tabBatch),
'pending_push' => count($tabBatch),
'transferable' => count($arrClass['transferable']),
'blocked_count' => $arrClass['blocked_count'],
'chunk_size' => MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE,
'ct_event_id' => $intCtEventId,
'force_full' => $blnForceFull ? 1 : 0,
);
}

View File

@ -186,7 +186,10 @@ switch ($strAction) {
break;
}
set_time_limit(180);
$tabRetour = fxChronotrackApiSyncPushPrepare($intEveId);
$blnForceFull = (intval($_REQUEST['force_full'] ?? 0) === 1);
$tabRetour = fxChronotrackApiSyncPushPrepare($intEveId, array(
'force_full' => $blnForceFull,
));
break;
case 'sync_push':