From a18197b31c917f6b1cc6627ca20d2bb17654267a Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 14 Jul 2026 12:46:27 -0400 Subject: [PATCH] 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. --- auto_chronotrack_sync.php | 34 +---- php/chronotrack_api/fx_chronotrack_admin.php | 84 +++++++++--- php/chronotrack_api/fx_chronotrack_sync.php | 131 ++++++++++++++++++- superadm/ajax_chronotrack_api.php | 9 ++ 4 files changed, 208 insertions(+), 50 deletions(-) diff --git a/auto_chronotrack_sync.php b/auto_chronotrack_sync.php index 549753c..5a11d7e 100644 --- a/auto_chronotrack_sync.php +++ b/auto_chronotrack_sync.php @@ -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 d’exé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 d’attente : 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"; } diff --git a/php/chronotrack_api/fx_chronotrack_admin.php b/php/chronotrack_api/fx_chronotrack_admin.php index 47c7df2..59da8fd 100644 --- a/php/chronotrack_api/fx_chronotrack_admin.php +++ b/php/chronotrack_api/fx_chronotrack_admin.php @@ -298,14 +298,23 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar echo ''; echo '

'; if ($blnAutoOn && $strUntilRaw !== '') { - echo 'Config enregistrée — cron auto_chronotrack_sync.php (toutes les minutes). Arrêt : ' + echo 'Config enregistrée — cron auto_chronotrack_sync.php (comme liste d’attente). Arrêt : ' . fxChronotrackApiAdminEsc($strUntilRaw) . '.'; } else { - echo 'Cochez, choisissez fréquence + date/heure de fin, puis Enregistrer. ' - . 'Le cron serveur doit appeler auto_chronotrack_sync.php chaque minute.'; + echo 'Cochez, choisissez fréquence + date/heure de fin, puis Enregistrer.'; + } + echo '

'; + $strLastAuto = trim((string)($arrConfig['last_auto_run_at'] ?? '')); + echo '

'; + echo 'Dernier passage auto : '; + if ($strLastAuto !== '' && $strLastAuto !== '0000-00-00 00:00:00') { + echo fxChronotrackApiAdminEsc($strLastAuto); + } else { + echo 'jamais (le cron n’a pas encore poussé cet event)'; } echo '

'; echo '
'; + echo '
'; echo ''; // Zone employé : statut clair + actions @@ -320,13 +329,13 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar echo ' Envoyer vers ChronoTrack '; echo ' '; - echo ''; + echo ' '; + echo ''; echo ''; - echo '

Envoi manuel : nouveaux / modifiés depuis le dernier push. ' + echo '

Envoi manuel : nouveaux / modifiés depuis le dernier push. ' . 'Page : refresh statut MS1 chaque minute.

'; - echo '

Cron : utiliser curl -sk sur preprod (certificat). ' - . 'Preuve serveur : log/chronotrack_cron_last.txt.

'; echo '
'; echo '
'; @@ -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('Appel cron…'); + $res.html('Appel auto_chronotrack_sync.php…'); $.ajax({ url: '/auto_chronotrack_sync.php', method: 'GET', dataType: 'text', timeout: 120000 }).done(function (txt) { - $res.html('
Réponse cron :
'
-                    + $('
').text(txt || '(vide)').html() + '
' - + '
Si ran=0 : intervalle pas dû, pas de fin, ou auto OFF. Vérifier aussi log/chronotrack_cron_last.txt
'); + $res.html('
Réponse script :
'
+                    + $('
').text(txt || '(vide)').html() + '
'); + loadAutoDiag(); + loadSyncLogs(); }).fail(function (xhr) { - $res.html('
Cron injoignable (HTTP ' - + (xhr.status || '?') + '). Fichier absent sur ce domaine ? Certificat / 404 ?
'); + $res.html('
Script injoignable sur ce domaine (HTTP ' + + (xhr.status || '?') + '). Fichier pas déployé ici ?
'); }).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('' + + escHtml((res && res.message) ? res.message : 'Diag impossible') + ''); + return; + } + var d = res.diag; + var html = '
'; + html += '
Verdict : ' + escHtml(d.would || '') + '
'; + html += '
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') + '
'; + html += '
logs en base pour cet event : ' + escHtml(String(d.log_count)) + ''; + if (d.last_log) { + html += ' · dernier : [' + escHtml(d.last_log.created_at || '') + '] ' + + escHtml(d.last_log.action || '') + ' — ' + escHtml(d.last_log.message || ''); + } + html += '
'; + if (d.cols_note) { + html += '
' + escHtml(d.cols_note) + '
'; + } + html += '
host=' + escHtml(d.host || '') + '
'; + html += '
'; + $box.html(html); + if (d.last_auto_run_at) { + $('#ct_api_auto_last').html('Dernier passage auto : ' + + escHtml(d.last_auto_run_at)); + } + }, 'json').fail(function () { + $box.html('Erreur réseau (diag).'); + }); + } + $('#ct_api_btn_auto_diag').on('click', loadAutoDiag); + $('#ct_api_btn_ct_count').on('click', function () { loadCtLiveCount(); }); diff --git a/php/chronotrack_api/fx_chronotrack_sync.php b/php/chronotrack_api/fx_chronotrack_sync.php index c3d046d..a3ba7d9 100644 --- a/php/chronotrack_api/fx_chronotrack_sync.php +++ b/php/chronotrack_api/fx_chronotrack_sync.php @@ -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 à l’enregistrement) $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 d’intervalle (é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 d’essai-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 d’intervalle (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, + ), ); } diff --git a/superadm/ajax_chronotrack_api.php b/superadm/ajax_chronotrack_api.php index 5fe17f8..49e3329 100644 --- a/superadm/ajax_chronotrack_api.php +++ b/superadm/ajax_chronotrack_api.php @@ -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');