Refactor auto ChronoTrack sync script to enhance logging and error handling. Remove local heartbeat logging and update cron execution feedback. Improve admin interface with clearer instructions and a new button for manual sync diagnostics. Implement auto-sync diagnostics to provide detailed status and logs for events, facilitating better monitoring and troubleshooting of synchronization processes.

This commit is contained in:
2026-07-14 12:46:27 -04:00
parent c3340605f2
commit a18197b31c
4 changed files with 208 additions and 50 deletions

View File

@ -2,10 +2,10 @@
/**
* MSIN-4328 — Cron sync auto ChronoTrack (différentiel).
*
* Cron recommandé (toutes les minutes) — IMPORTANT : -k si certificat preprod invalide :
* /usr/bin/curl -sk https://VOTRE-DOMAINE/auto_chronotrack_sync.php > /dev/null 2>&1
* Même pattern que auto_liste_attente.php :
* /usr/bin/curl -s https://VOTRE-DOMAINE/auto_chronotrack_sync.php > /dev/null 2>&1
*
* Preuve dexécution : fichier log/chronotrack_cron_last.txt (mtime = dernier appel).
* Minute=* Hour=* … → le script décide selon fréquence/fin de chaque event.
*/
ini_set('display_errors', 0);
@ -14,33 +14,12 @@ ini_set('error_log', __DIR__ . '/log/php_log.txt');
@set_time_limit(300);
$strLogDir = __DIR__ . '/log';
if (!is_dir($strLogDir)) {
@mkdir($strLogDir, 0755, true);
}
$strHeartbeat = $strLogDir . '/chronotrack_cron_last.txt';
/**
* Écrit une preuve locale que le script a été appelé (cron ou navigateur).
*/
function fxChronotrackCronHeartbeat($strHeartbeat, $strLine) {
$strPayload = $strLine . "\n"
. 'host=' . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') . "\n"
. 'remote=' . (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '') . "\n"
. 'ua=' . (isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '') . "\n";
@file_put_contents($strHeartbeat, $strPayload, LOCK_EX);
}
// Preuve immédiate (même si lock / erreur après)
fxChronotrackCronHeartbeat($strHeartbeat, 'HIT ' . date('Y-m-d H:i:s'));
$strLockPath = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'ms1_chronotrack_auto_sync.lock';
$fpLock = @fopen($strLockPath, 'c');
if ($fpLock === false || !flock($fpLock, LOCK_EX | LOCK_NB)) {
if (is_resource($fpLock)) {
fclose($fpLock);
}
fxChronotrackCronHeartbeat($strHeartbeat, 'BUSY ' . date('Y-m-d H:i:s'));
header('Content-Type: text/plain; charset=utf-8');
echo 'busy';
exit;
@ -55,15 +34,14 @@ try {
$arrResult = fxChronotrackApiSyncRunAutoCron();
$strLine = 'auto_chronotrack_sync [' . date('Y-m-d H:i:s') . '] '
. ($arrResult['message'] ?? 'done');
// Même style que liste dattente : une ligne dans log/php_log.txt
error_log($strLine);
fxChronotrackCronHeartbeat($strHeartbeat, 'OK ' . $strLine);
header('Content-Type: text/plain; charset=utf-8');
echo $strLine . "\n";
} catch (Throwable $ex) {
$strErr = 'ERROR ' . date('Y-m-d H:i:s') . ' ' . $ex->getMessage();
error_log('auto_chronotrack_sync ' . $strErr);
fxChronotrackCronHeartbeat($strHeartbeat, $strErr);
$strErr = 'auto_chronotrack_sync ERROR [' . date('Y-m-d H:i:s') . '] ' . $ex->getMessage();
error_log($strErr);
header('Content-Type: text/plain; charset=utf-8');
echo $strErr . "\n";
}

View File

@ -298,14 +298,23 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar
echo '</div>';
echo '<p class="small text-muted mb-0 mt-2" id="ct_api_auto_hint">';
if ($blnAutoOn && $strUntilRaw !== '') {
echo 'Config enregistrée — cron <code>auto_chronotrack_sync.php</code> (toutes les minutes). Arrêt : '
echo 'Config enregistrée — cron <code>auto_chronotrack_sync.php</code> (comme liste dattente). Arrêt : '
. fxChronotrackApiAdminEsc($strUntilRaw) . '.';
} else {
echo 'Cochez, choisissez fréquence + date/heure de fin, puis Enregistrer. '
. 'Le cron serveur doit appeler <code>auto_chronotrack_sync.php</code> chaque minute.';
echo 'Cochez, choisissez fréquence + date/heure de fin, puis Enregistrer.';
}
echo '</p>';
$strLastAuto = trim((string)($arrConfig['last_auto_run_at'] ?? ''));
echo '<p class="small mb-0 mt-1" id="ct_api_auto_last">';
echo '<strong>Dernier passage auto :</strong> ';
if ($strLastAuto !== '' && $strLastAuto !== '0000-00-00 00:00:00') {
echo fxChronotrackApiAdminEsc($strLastAuto);
} else {
echo '<span class="text-warning">jamais (le cron na pas encore poussé cet event)</span>';
}
echo '</p>';
echo '<div id="ct_api_auto_msg" class="small mt-1"></div>';
echo '<div id="ct_api_auto_diag" class="small mt-2 text-muted"></div>';
echo '</div>';
// Zone employé : statut clair + actions
@ -320,13 +329,13 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar
echo '<i class="fa fa-cloud-upload" aria-hidden="true"></i>&nbsp;Envoyer vers ChronoTrack</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_cron_ping" title="Appelle le script cron maintenant (test)">';
echo 'Tester le cron</button>';
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_auto_diag">'
. 'État sync auto</button> ';
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-1">Envoi manuel : nouveaux / modifiés depuis le dernier push. '
echo '<p class="small text-muted mb-2">Envoi manuel : nouveaux / modifiés depuis le dernier push. '
. 'Page : refresh statut MS1 chaque minute.</p>';
echo '<p class="small text-muted mb-2" id="ct_api_cron_hint">Cron : utiliser <code>curl -sk</code> sur preprod (certificat). '
. 'Preuve serveur : <code>log/chronotrack_cron_last.txt</code>.</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>';
@ -1285,29 +1294,74 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
$('#ct_api_logs_wrap').addClass('d-none').empty();
});
// MSIN-4328 — appelle auto_chronotrack_sync.php (même URL que le cron)
// MSIN-4328 — appelle auto_chronotrack_sync.php (même URL / même base que le CrowdJob)
$('#ct_api_btn_cron_ping').on('click', function () {
var $btn = $(this);
var $res = $('#ct_api_push_result');
$btn.prop('disabled', true);
$res.html('<span class="text-muted">Appel cron…</span>');
$res.html('<span class="text-muted">Appel auto_chronotrack_sync.php…</span>');
$.ajax({
url: '/auto_chronotrack_sync.php',
method: 'GET',
dataType: 'text',
timeout: 120000
}).done(function (txt) {
$res.html('<div class="alert alert-info small mb-0"><strong>Réponse cron :</strong><br><pre class="mb-0" style="white-space:pre-wrap;">'
+ $('<div/>').text(txt || '(vide)').html() + '</pre>'
+ '<div class="mt-1 text-muted">Si ran=0 : intervalle pas dû, pas de fin, ou auto OFF. Vérifier aussi log/chronotrack_cron_last.txt</div></div>');
$res.html('<div class="alert alert-info small mb-0"><strong>Réponse script :</strong><br><pre class="mb-0" style="white-space:pre-wrap;">'
+ $('<div/>').text(txt || '(vide)').html() + '</pre></div>');
loadAutoDiag();
loadSyncLogs();
}).fail(function (xhr) {
$res.html('<div class="alert alert-danger small mb-0">Cron injoignable (HTTP '
+ (xhr.status || '?') + '). Fichier absent sur ce domaine ? Certificat / 404 ?</div>');
$res.html('<div class="alert alert-danger small mb-0">Script injoignable sur ce domaine (HTTP '
+ (xhr.status || '?') + '). Fichier pas déployé ici ?</div>');
}).always(function () {
$btn.prop('disabled', false);
});
});
function loadAutoDiag() {
var $box = $('#ct_api_auto_diag');
if (!$box.length) {
return;
}
$box.text('Lecture état sync auto…');
$.post(strAjaxUrl, { a: 'sync_auto_diag', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok' || !res.diag) {
$box.html('<span class="text-danger">'
+ escHtml((res && res.message) ? res.message : 'Diag impossible') + '</span>');
return;
}
var d = res.diag;
var html = '<div class="border rounded p-2 bg-light">';
html += '<div><strong>Verdict :</strong> ' + escHtml(d.would || '') + '</div>';
html += '<div>auto=' + (d.auto_enabled ? 'ON' : 'OFF')
+ ' · freq=' + escHtml(String(d.interval_min)) + ' min'
+ ' · fin=' + escHtml(d.until || '—')
+ ' · dernier passage=' + escHtml(d.last_auto_run_at || 'jamais') + '</div>';
html += '<div>logs en base pour cet event : <strong>' + escHtml(String(d.log_count)) + '</strong>';
if (d.last_log) {
html += ' · dernier : [' + escHtml(d.last_log.created_at || '') + '] '
+ escHtml(d.last_log.action || '') + ' — ' + escHtml(d.last_log.message || '');
}
html += '</div>';
if (d.cols_note) {
html += '<div class="text-danger">' + escHtml(d.cols_note) + '</div>';
}
html += '<div class="text-muted">host=' + escHtml(d.host || '') + '</div>';
html += '</div>';
$box.html(html);
if (d.last_auto_run_at) {
$('#ct_api_auto_last').html('<strong>Dernier passage auto :</strong> '
+ escHtml(d.last_auto_run_at));
}
}, 'json').fail(function () {
$box.html('<span class="text-danger">Erreur réseau (diag).</span>');
});
}
$('#ct_api_btn_auto_diag').on('click', loadAutoDiag);
$('#ct_api_btn_ct_count').on('click', function () {
loadCtLiveCount();
});

View File

@ -481,13 +481,24 @@ function fxChronotrackApiSyncNormalizeBirthdate($strDob) {
function fxChronotrackApiSyncLog($intEveId, $intParId, $strAction, $strStatus, $strMessage) {
global $objDatabase;
if (!isset($objDatabase) || !is_object($objDatabase)) {
error_log('MSIN-4328 sync_log: $objDatabase absent — action=' . $strAction);
return false;
}
$sql = "INSERT INTO api_chronotrack_sync_log SET eve_id = " . intval($intEveId)
. ", par_id = " . ($intParId > 0 ? intval($intParId) : 'NULL')
. ", action = '" . $objDatabase->fxEscape(substr(trim($strAction), 0, 32)) . "'"
. ", status = '" . $objDatabase->fxEscape($strStatus === 'ok' ? 'ok' : 'error') . "'"
. ", message = '" . $objDatabase->fxEscape(substr(trim($strMessage), 0, 65000)) . "'"
. ", created_at = '" . fxGetDateTime() . "'";
$objDatabase->fxQuery($sql);
$blnOk = (bool)$objDatabase->fxQuery($sql);
if (!$blnOk) {
// Sans ça, push/cron « OK » mais Voir les logs = vide (INSERT silencieux).
error_log('MSIN-4328 sync_log INSERT failed eve=' . intval($intEveId)
. ' action=' . $strAction . ' sql_err=' . (isset($objDatabase->con) ? mysqli_error($objDatabase->con) : '?'));
}
return $blnOk;
}
/**
@ -1393,6 +1404,7 @@ function fxChronotrackApiSyncPushPrepare($intEveId) {
. ($arrClass['blocked_count'] > 0 ? ' ' . $arrClass['blocked_count'] . ' exclus.' : ''))
: ('Aucun changement depuis le dernier push ('
. count($arrClass['transferable']) . ' éligibles à jour).');
fxChronotrackApiSyncLog($intEveId, 0, 'push_prepare', 'ok', 'idle — ' . $strMsg);
return array(
'state' => 'error',
'message' => $strMsg,
@ -1403,6 +1415,16 @@ function fxChronotrackApiSyncPushPrepare($intEveId) {
);
}
fxChronotrackApiSyncLog(
$intEveId,
0,
'push_prepare',
'ok',
'job prêt — pending=' . count($tabBatch)
. ' éligibles=' . count($arrClass['transferable'])
. ' exclus=' . intval($arrClass['blocked_count'])
);
$arrJob = array(
'ct_event_id' => $intCtEventId,
'batch' => $tabBatch,
@ -1750,6 +1772,8 @@ function fxChronotrackApiSyncRunAutoPushForEvent($intEveId) {
/**
* MSIN-4328 — passage cron : events auto ON, fréquence / fin respectées.
* Chaque décision (run / skip / expire) écrit un log pour lévénement — sinon
* « Voir les logs » reste vide alors que le CrowdJob tourne vraiment.
*/
function fxChronotrackApiSyncRunAutoCron() {
if (!fxChronotrackApiSettingsConfigured()) {
@ -1758,6 +1782,7 @@ function fxChronotrackApiSyncRunAutoCron() {
'message' => fxChronotrackApiSettingsErrorMessage(),
'ran' => 0,
'skipped' => 0,
'enabled_configs' => 0,
);
}
@ -1767,6 +1792,7 @@ function fxChronotrackApiSyncRunAutoCron() {
$intExpired = 0;
$tabDetails = array();
$intNow = time();
$intEnabled = count($tabConfigs);
foreach ($tabConfigs as $arrCfg) {
$intEveId = intval($arrCfg['eve_id'] ?? 0);
@ -1793,6 +1819,13 @@ function fxChronotrackApiSyncRunAutoCron() {
} else {
// Pas de fin = on ne tourne pas (UI exige une fin à lenregistrement)
$intSkipped++;
fxChronotrackApiSyncLog(
$intEveId,
0,
'auto_cron',
'error',
'Skip: date/heure de fin manquante (réenregistrer la sync auto)'
);
$tabDetails[] = array('eve_id' => $intEveId, 'action' => 'skip_no_until');
continue;
}
@ -1806,6 +1839,8 @@ function fxChronotrackApiSyncRunAutoCron() {
$intLast = strtotime($strLast);
if ($intLast !== false && ($intNow - $intLast) < ($intInterval * 60)) {
$intSkipped++;
// Trace légère: 1 seule fois par fenêtre dintervalle (évite 1440 lignes/jour).
// Déjà logué ailleurs au run — ici on ne spam pas.
$tabDetails[] = array('eve_id' => $intEveId, 'action' => 'skip_interval');
continue;
}
@ -1823,12 +1858,94 @@ function fxChronotrackApiSyncRunAutoCron() {
);
}
$strMsg = 'cron auto — configs_on=' . $intEnabled
. ' ran=' . $intRan . ' skipped=' . $intSkipped . ' expired=' . $intExpired;
if ($intEnabled === 0) {
$strMsg .= ' (aucun event avec sync auto ON sur cette base)';
}
return array(
'state' => 'ok',
'message' => 'cron auto — ran=' . $intRan . ' skipped=' . $intSkipped . ' expired=' . $intExpired,
'ran' => $intRan,
'skipped' => $intSkipped,
'expired' => $intExpired,
'details' => $tabDetails,
'state' => 'ok',
'message' => $strMsg,
'ran' => $intRan,
'skipped' => $intSkipped,
'expired' => $intExpired,
'enabled_configs' => $intEnabled,
'details' => $tabDetails,
);
}
/**
* MSIN-4328 — état sync auto + logs pour un événement (diag UI, pas dessai-erreur SSL).
*/
function fxChronotrackApiSyncAutoDiag($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
$arrCfg = fxChronotrackApiConfigGet($intEveId);
if ($arrCfg === null) {
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
}
$blnColsOk = true;
$strColNote = '';
$sqlCol = "SELECT COUNT(*) AS n FROM information_schema.COLUMNS"
. " WHERE TABLE_SCHEMA = DATABASE()"
. " AND TABLE_NAME = 'api_chronotrack_config'"
. " AND COLUMN_NAME = 'auto_sync_enabled'";
$arrCol = $objDatabase->fxGetRow($sqlCol);
if (intval($arrCol['n'] ?? 0) < 1) {
$blnColsOk = false;
$strColNote = 'Colonnes auto_sync absentes — exécuter sql/MSIN-4328-chronotrack-auto-sync-config.sql';
}
$sqlLog = "SELECT COUNT(*) AS n FROM api_chronotrack_sync_log WHERE eve_id = " . $intEveId;
$arrLogCnt = $objDatabase->fxGetRow($sqlLog);
$intLogCnt = intval($arrLogCnt['n'] ?? 0);
$arrLastLog = $objDatabase->fxGetRow(
"SELECT log_id, action, status, message, created_at FROM api_chronotrack_sync_log"
. " WHERE eve_id = " . $intEveId
. " ORDER BY log_id DESC LIMIT 1"
);
$blnOn = (intval($arrCfg['auto_sync_enabled'] ?? 0) === 1);
$intInterval = intval($arrCfg['auto_sync_interval_min'] ?? 15);
$strUntil = trim((string)($arrCfg['auto_sync_until'] ?? ''));
$strLastRun = trim((string)($arrCfg['last_auto_run_at'] ?? ''));
$intNow = time();
$strWould = 'inconnu';
if (!$blnColsOk) {
$strWould = 'bloqué: SQL auto_sync manquant';
} elseif (!$blnOn) {
$strWould = 'ne tourne pas: sync auto OFF';
} elseif ($strUntil === '' || $strUntil === '0000-00-00 00:00:00') {
$strWould = 'ne tourne pas: date de fin manquante';
} elseif (strtotime($strUntil) !== false && $intNow > strtotime($strUntil)) {
$strWould = 'ne tourne pas: date de fin dépassée (' . $strUntil . ')';
} elseif ($strLastRun !== '' && $strLastRun !== '0000-00-00 00:00:00'
&& strtotime($strLastRun) !== false
&& ($intNow - strtotime($strLastRun)) < ($intInterval * 60)) {
$strWould = 'en attente dintervalle (dernier passage ' . $strLastRun
. ', fréquence ' . $intInterval . ' min)';
} else {
$strWould = 'devrait pousser au prochain appel de auto_chronotrack_sync.php';
}
return array(
'state' => 'ok',
'message' => $strWould,
'diag' => array(
'cols_ok' => $blnColsOk,
'cols_note' => $strColNote,
'auto_enabled' => $blnOn,
'interval_min' => $intInterval,
'until' => $strUntil !== '' ? $strUntil : null,
'last_auto_run_at' => $strLastRun !== '' ? $strLastRun : null,
'log_count' => $intLogCnt,
'last_log' => is_array($arrLastLog) ? $arrLastLog : null,
'host' => isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '',
'would' => $strWould,
),
);
}

View File

@ -152,6 +152,15 @@ switch ($strAction) {
$tabRetour = fxChronotrackApiSyncLogList($intEveId, $intLimit);
break;
case 'sync_auto_diag':
// MSIN-4328 — pourquoi le cron pousse / ne pousse pas (sans essais curl)
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');
break;
}
$tabRetour = fxChronotrackApiSyncAutoDiag($intEveId);
break;
case 'sync_probe_push':
if ($intEveId <= 0) {
$tabRetour = array('state' => 'error', 'message' => 'eve_id manquant');