diff --git a/auto_chronotrack_sync.php b/auto_chronotrack_sync.php
new file mode 100644
index 0000000..5ad278a
--- /dev/null
+++ b/auto_chronotrack_sync.php
@@ -0,0 +1,44 @@
+ /dev/null 2>&1
+ *
+ * La fréquence réelle d’envoi par événement est celle choisie dans l’admin
+ * (1 / 5 / 15 / 30 / 60 min) + date/heure de fin.
+ */
+
+ini_set('display_errors', 0);
+ini_set('log_errors', 1);
+ini_set('error_log', __DIR__ . '/log/php_log.txt');
+
+@set_time_limit(300);
+
+$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)) {
+ // Un passage précédent tourne encore — on saute ce tick.
+ if (is_resource($fpLock)) {
+ fclose($fpLock);
+ }
+ header('Content-Type: text/plain; charset=utf-8');
+ echo 'busy';
+ exit;
+}
+
+session_start();
+
+require_once __DIR__ . '/php/inc_fonctions.php';
+require_once __DIR__ . '/php/chronotrack_api/inc_bootstrap.php';
+
+$arrResult = fxChronotrackApiSyncRunAutoCron();
+$strLine = 'auto_chronotrack_sync [' . date('Y-m-d H:i:s') . '] '
+ . ($arrResult['message'] ?? 'done');
+error_log($strLine);
+
+header('Content-Type: text/plain; charset=utf-8');
+echo $strLine . "\n";
+
+flock($fpLock, LOCK_UN);
+fclose($fpLock);
diff --git a/php/chronotrack_api/fx_chronotrack_admin.php b/php/chronotrack_api/fx_chronotrack_admin.php
index f9380e1..7ca13df 100644
--- a/php/chronotrack_api/fx_chronotrack_admin.php
+++ b/php/chronotrack_api/fx_chronotrack_admin.php
@@ -298,11 +298,11 @@ function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $ar
echo '';
echo '
';
if ($blnAutoOn && $strUntilRaw !== '') {
- echo 'Config enregistrée — exécution serveur (cron) à brancher. Arrêt prévu : '
+ echo 'Config enregistrée — cron auto_chronotrack_sync.php (toutes les minutes). Arrêt : '
. fxChronotrackApiAdminEsc($strUntilRaw) . '.';
} else {
- echo 'Quand c’est coché, le serveur (cron) poussera le différentiel à cet intervalle jusqu’à la date de fin. '
- . 'Pas encore actif côté serveur.';
+ echo 'Cochez, choisissez fréquence + date/heure de fin, puis Enregistrer. '
+ . 'Le cron serveur doit appeler auto_chronotrack_sync.php chaque minute.';
}
echo '
';
echo '';
@@ -1174,10 +1174,10 @@ function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed
$msg.addClass('text-success').text(res.message || 'Enregistré');
if (res.auto_sync) {
if (res.auto_sync.enabled && res.auto_sync.until) {
- $hint.text('Config enregistrée — exécution serveur (cron) à brancher. Arrêt prévu : '
+ $hint.text('Config OK — cron toutes les minutes lit cette config (auto_chronotrack_sync.php). Arrêt : '
+ res.auto_sync.until + '.');
} else {
- $hint.text('Sync auto désactivée. Le cron (phase B) ne traitera pas cet événement.');
+ $hint.text('Sync auto désactivée.');
}
}
}, 'json').fail(function () {
diff --git a/php/chronotrack_api/fx_chronotrack_config.php b/php/chronotrack_api/fx_chronotrack_config.php
index 19ed628..6a71cc3 100644
--- a/php/chronotrack_api/fx_chronotrack_config.php
+++ b/php/chronotrack_api/fx_chronotrack_config.php
@@ -197,7 +197,7 @@ function fxChronotrackApiConfigSaveAutoSync($intEveId, $blnEnabled, $intInterval
'state' => 'ok',
'message' => $blnEnabled
? ('Sync auto enregistrée (toutes les ' . $intIntervalMin . ' min jusqu’au '
- . trim((string)($arrFresh['auto_sync_until'] ?? '')) . '). Le cron démarrera en phase B.')
+ . trim((string)($arrFresh['auto_sync_until'] ?? '')) . '). Cron : auto_chronotrack_sync.php')
: 'Sync auto désactivée.',
'auto_sync' => array(
'enabled' => intval($arrFresh['auto_sync_enabled'] ?? 0) === 1,
@@ -208,6 +208,60 @@ function fxChronotrackApiConfigSaveAutoSync($intEveId, $blnEnabled, $intInterval
);
}
+/**
+ * MSIN-4328 — events avec sync auto ON (pas fermés).
+ */
+function fxChronotrackApiConfigListAutoSyncEnabled() {
+ global $objDatabase;
+
+ $sql = "SELECT c.*"
+ . " FROM api_chronotrack_config c"
+ . " WHERE IFNULL(c.auto_sync_enabled, 0) = 1"
+ . " AND c.sync_status <> '" . $objDatabase->fxEscape(MSIN_API_CHRONOTRACK_SYNC_FERME) . "'"
+ . " ORDER BY c.eve_id ASC";
+ $arrRows = $objDatabase->fxGetResults($sql);
+ $tabOut = array();
+ if (!is_array($arrRows)) {
+ return $tabOut;
+ }
+ for ($i = 1; $i <= count($arrRows); $i++) {
+ $tabOut[] = $arrRows[$i];
+ }
+ return $tabOut;
+}
+
+/**
+ * MSIN-4328 — horodatage dernier passage cron auto.
+ */
+function fxChronotrackApiConfigTouchAutoRun($intEveId) {
+ global $objDatabase;
+
+ $intEveId = intval($intEveId);
+ if ($intEveId <= 0) {
+ return false;
+ }
+ $sql = "UPDATE api_chronotrack_config SET last_auto_run_at = '" . fxGetDateTime() . "'"
+ . ", updated_at = '" . fxGetDateTime() . "'"
+ . " WHERE eve_id = " . $intEveId;
+ return (bool)$objDatabase->fxQuery($sql);
+}
+
+/**
+ * MSIN-4328 — coupe la sync auto (échéance atteinte).
+ */
+function fxChronotrackApiConfigDisableAutoSync($intEveId) {
+ global $objDatabase;
+
+ $intEveId = intval($intEveId);
+ if ($intEveId <= 0) {
+ return false;
+ }
+ $sql = "UPDATE api_chronotrack_config SET auto_sync_enabled = 0"
+ . ", updated_at = '" . fxGetDateTime() . "'"
+ . " WHERE eve_id = " . $intEveId;
+ return (bool)$objDatabase->fxQuery($sql);
+}
+
function fxChronotrackApiRaceMapGetForEvent($intEveId) {
global $objDatabase;
diff --git a/php/chronotrack_api/fx_chronotrack_sync.php b/php/chronotrack_api/fx_chronotrack_sync.php
index 6cda232..5a25787 100644
--- a/php/chronotrack_api/fx_chronotrack_sync.php
+++ b/php/chronotrack_api/fx_chronotrack_sync.php
@@ -1661,3 +1661,143 @@ function fxChronotrackApiSyncListCtOrphans($intEveId) {
'orphans' => $tabOrphans,
);
}
+
+/**
+ * MSIN-4328 — push différentiel complet pour un event (cron auto).
+ */
+function fxChronotrackApiSyncRunAutoPushForEvent($intEveId) {
+ $intEveId = intval($intEveId);
+ $arrPrep = fxChronotrackApiSyncPushPrepare($intEveId);
+ if (($arrPrep['state'] ?? '') !== 'ok') {
+ // Rien à pousser = succès « idle »
+ $strMsg = (string)($arrPrep['message'] ?? 'prepare failed');
+ if (stripos($strMsg, 'Aucun changement') !== false || intval($arrPrep['pending_push'] ?? -1) === 0) {
+ fxChronotrackApiSyncLog($intEveId, 0, 'auto_push', 'ok', 'idle — ' . $strMsg);
+ return array('state' => 'ok', 'message' => $strMsg, 'pushed_ok' => 0, 'idle' => true);
+ }
+ fxChronotrackApiSyncLog($intEveId, 0, 'auto_push', 'error', $strMsg);
+ return $arrPrep;
+ }
+
+ $intOffset = 0;
+ $intLimit = MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE;
+ $intOkSum = 0;
+ $intErrSum = 0;
+ $intLoops = 0;
+ $intMaxLoops = 500;
+
+ while ($intLoops < $intMaxLoops) {
+ $intLoops++;
+ $arrChunk = fxChronotrackApiSyncPushEvent($intEveId, array(
+ 'offset' => $intOffset,
+ 'limit' => $intLimit,
+ ));
+ $intOkSum += intval($arrChunk['pushed_ok'] ?? 0);
+ $intErrSum += intval($arrChunk['pushed_error'] ?? 0);
+ if (!empty($arrChunk['done'])) {
+ break;
+ }
+ if (($arrChunk['state'] ?? '') === 'error' && intval($arrChunk['pushed_ok'] ?? 0) === 0) {
+ break;
+ }
+ $intOffset = intval($arrChunk['next_offset'] ?? ($intOffset + $intLimit));
+ }
+
+ $strState = ($intErrSum === 0) ? 'ok' : (($intOkSum > 0) ? 'partial' : 'error');
+ $strMsg = 'auto push ok=' . $intOkSum . ' err=' . $intErrSum
+ . ' total_job=' . intval($arrPrep['total'] ?? 0);
+ fxChronotrackApiSyncLog($intEveId, 0, 'auto_push', ($strState === 'ok') ? 'ok' : 'error', $strMsg);
+
+ return array(
+ 'state' => $strState,
+ 'message' => $strMsg,
+ 'pushed_ok' => $intOkSum,
+ 'pushed_error' => $intErrSum,
+ 'idle' => false,
+ );
+}
+
+/**
+ * MSIN-4328 — passage cron : events auto ON, fréquence / fin respectées.
+ */
+function fxChronotrackApiSyncRunAutoCron() {
+ if (!fxChronotrackApiSettingsConfigured()) {
+ return array(
+ 'state' => 'error',
+ 'message' => fxChronotrackApiSettingsErrorMessage(),
+ 'ran' => 0,
+ 'skipped' => 0,
+ );
+ }
+
+ $tabConfigs = fxChronotrackApiConfigListAutoSyncEnabled();
+ $intRan = 0;
+ $intSkipped = 0;
+ $intExpired = 0;
+ $tabDetails = array();
+ $intNow = time();
+
+ foreach ($tabConfigs as $arrCfg) {
+ $intEveId = intval($arrCfg['eve_id'] ?? 0);
+ if ($intEveId <= 0) {
+ continue;
+ }
+
+ $strUntil = trim((string)($arrCfg['auto_sync_until'] ?? ''));
+ if ($strUntil !== '' && $strUntil !== '0000-00-00 00:00:00') {
+ $intUntil = strtotime($strUntil);
+ if ($intUntil !== false && $intNow > $intUntil) {
+ fxChronotrackApiConfigDisableAutoSync($intEveId);
+ fxChronotrackApiSyncLog(
+ $intEveId,
+ 0,
+ 'auto_sync',
+ 'ok',
+ 'Échéance atteinte (' . $strUntil . ') — sync auto désactivée'
+ );
+ $intExpired++;
+ $tabDetails[] = array('eve_id' => $intEveId, 'action' => 'expired');
+ continue;
+ }
+ } else {
+ // Pas de fin = on ne tourne pas (UI exige une fin à l’enregistrement)
+ $intSkipped++;
+ $tabDetails[] = array('eve_id' => $intEveId, 'action' => 'skip_no_until');
+ continue;
+ }
+
+ $intInterval = intval($arrCfg['auto_sync_interval_min'] ?? 15);
+ if (!in_array($intInterval, fxChronotrackApiConfigAutoSyncIntervals(), true)) {
+ $intInterval = 15;
+ }
+ $strLast = trim((string)($arrCfg['last_auto_run_at'] ?? ''));
+ if ($strLast !== '' && $strLast !== '0000-00-00 00:00:00') {
+ $intLast = strtotime($strLast);
+ if ($intLast !== false && ($intNow - $intLast) < ($intInterval * 60)) {
+ $intSkipped++;
+ $tabDetails[] = array('eve_id' => $intEveId, 'action' => 'skip_interval');
+ continue;
+ }
+ }
+
+ $arrRes = fxChronotrackApiSyncRunAutoPushForEvent($intEveId);
+ fxChronotrackApiConfigTouchAutoRun($intEveId);
+ $intRan++;
+ $tabDetails[] = array(
+ 'eve_id' => $intEveId,
+ 'action' => 'run',
+ 'result' => $arrRes['state'] ?? 'error',
+ 'message' => $arrRes['message'] ?? '',
+ 'ok' => intval($arrRes['pushed_ok'] ?? 0),
+ );
+ }
+
+ return array(
+ 'state' => 'ok',
+ 'message' => 'cron auto — ran=' . $intRan . ' skipped=' . $intSkipped . ' expired=' . $intExpired,
+ 'ran' => $intRan,
+ 'skipped' => $intSkipped,
+ 'expired' => $intExpired,
+ 'details' => $tabDetails,
+ );
+}