" . intval($strCtEntryId) . ")"; return (bool)$objDatabase->fxQuery($sql); } /** * MSIN-4328 — mappe la réponse POST/GET entry → par_id (via external_id). */ function fxChronotrackApiSyncApplyEntryIdsFromApiJson($mixJson, array $tabMeta) { if (!is_array($mixJson) || count($tabMeta) === 0) { return 0; } $tabByExt = array(); foreach ($tabMeta as $arrM) { $strExt = trim((string)($arrM['external_id'] ?? '')); $intParId = intval($arrM['par_id'] ?? 0); if ($strExt !== '' && $intParId > 0) { $tabByExt[$strExt] = $intParId; } } if (count($tabByExt) === 0) { return 0; } $arrEntities = fxChronotrackApiNormalizeEntityList($mixJson, 'entry'); $intSaved = 0; foreach ($arrEntities as $arrEnt) { if (!is_array($arrEnt)) { continue; } $strStatus = strtoupper(trim((string)( $arrEnt['status'] ?? $arrEnt['entry_status'] ?? '' ))); if ($strStatus === 'FAILURE') { continue; } $strEntryId = fxChronotrackApiEntityId($arrEnt); $strExt = fxChronotrackApiEntityExternalId($arrEnt); if ($strExt === '' && isset($arrEnt['entry_external_id'])) { $strExt = trim((string)$arrEnt['entry_external_id']); } if ($strEntryId === '' || $strExt === '' || !isset($tabByExt[$strExt])) { continue; } if (fxChronotrackApiSyncSaveCtEntryId($tabByExt[$strExt], $strEntryId)) { $intSaved++; } } return $intSaved; } /** * MSIN-4328 — URL admin ChronoTrack (fiche athlète). */ function fxChronotrackApiAdminEntryUrl($intCtEventId, $strCtEntryId) { $intCtEventId = intval($intCtEventId); $strCtEntryId = preg_replace('/[^0-9]/', '', (string)$strCtEntryId); if ($intCtEventId <= 0 || $strCtEntryId === '') { return ''; } return 'https://admin.chronotrack.com/admin/entry/index/eventID/' . $intCtEventId . '?entryID=' . $strCtEntryId; } function fxChronotrackApiSyncIso2ToIso3($strIso2) { static $tabMap = null; if ($tabMap === null) { $tabMap = array( 'CA' => 'CAN', 'US' => 'USA', 'MX' => 'MEX', 'FR' => 'FRA', 'GB' => 'GBR', 'UK' => 'GBR', 'DE' => 'DEU', 'ES' => 'ESP', 'IT' => 'ITA', 'BE' => 'BEL', 'CH' => 'CHE', 'AU' => 'AUS', 'JP' => 'JPN', 'CN' => 'CHN', 'BR' => 'BRA', 'NL' => 'NLD', 'SE' => 'SWE', 'NO' => 'NOR', 'DK' => 'DNK', 'FI' => 'FIN', 'IE' => 'IRL', 'PT' => 'PRT', 'PL' => 'POL', 'AT' => 'AUT', 'NZ' => 'NZL', 'IN' => 'IND', 'KR' => 'KOR', 'AD' => 'AND', 'AE' => 'ARE', 'AF' => 'AFG', ); } $strIso2 = strtoupper(trim((string)$strIso2)); if ($strIso2 === '') { return ''; } if (strlen($strIso2) === 3) { return $strIso2; } return isset($tabMap[$strIso2]) ? $tabMap[$strIso2] : ''; } function fxChronotrackApiSyncMapEntryStatus($strMs1Status, $blnCancelled = false) { if ($blnCancelled) { return 'WITHDRAWN'; } $strMs1Status = strtoupper(trim((string)$strMs1Status)); switch ($strMs1Status) { case 'DQ': return 'DQ'; case 'DNS': return 'DNS'; case 'NP': return 'NP'; case 'ABANDON': return 'DNF'; case 'UNRANKED': return 'UNRANKED'; case 'DEFERRED': return 'DEFERRED'; case 'CONF': default: return 'CONF'; } } function fxChronotrackApiSyncNormalizeSex($strSexe) { $strSexe = strtolower(trim((string)$strSexe)); if ($strSexe === 'f') { return 'F'; } if ($strSexe === 'h' || $strSexe === 'm') { return 'M'; } if ($strSexe === '' || $strSexe === 'n') { return 'NOT SPECIFIED'; } // TODO ChronoTrack : « a » (autre / non-binaire selon config épreuve) — confirmer option CT. return ''; } function fxChronotrackApiSyncHasValidSex($strSexe) { return fxChronotrackApiSyncNormalizeSex($strSexe) !== ''; } function fxChronotrackApiSyncExtractBib(array $arrRow) { $strBib = trim((string)($arrRow['no_bib'] ?? '')); if ($strBib === '' || $strBib === '0') { return ''; } return $strBib; } function fxChronotrackApiSyncParticipantLabel(array $arrRow) { return trim(trim((string)($arrRow['par_prenom'] ?? '')) . ' ' . trim((string)($arrRow['par_nom'] ?? ''))); } /** * MSIN-4328 — libellé épreuve MS1 (type — nom), pour messages d’exclusion. */ function fxChronotrackApiSyncEpreuveLabel(array $arrRow) { $strNom = trim((string)($arrRow['epr_nom'] ?? '')); $strType = trim((string)($arrRow['epr_type'] ?? '')); if ($strType !== '' && $strNom !== '') { return $strType . ' — ' . $strNom; } if ($strNom !== '') { return $strNom; } return $strType; } function fxChronotrackApiSyncIsTeamRow(array $arrRow) { return intval($arrRow['pec_equipe'] ?? 0) === 1 || intval($arrRow['par_equipe'] ?? 0) === 1; } /** * MSIN-4328 — éligibles dont par_maj > dernier push (ou tous si jamais poussé). * Inclut aussi no_bib_remis_date : le check-in « dossard récupéré » met souvent * par_maj = par_maj (n’incrémente pas) mais rafraîchit no_bib_remis_date. */ function fxChronotrackApiSyncFilterChangedSincePush(array $tabTransferable, $strLastPushAt) { $strLastPushAt = trim((string)$strLastPushAt); if ($strLastPushAt === '' || $strLastPushAt === '0000-00-00 00:00:00') { return $tabTransferable; } $intLast = strtotime($strLastPushAt); if ($intLast === false) { return $tabTransferable; } $tabOut = array(); foreach ($tabTransferable as $arrItem) { $strMaj = trim((string)($arrItem['row']['par_maj'] ?? '')); if ($strMaj === '' || $strMaj === '0000-00-00 00:00:00') { $tabOut[] = $arrItem; continue; } $intMaj = strtotime($strMaj); if ($intMaj === false || $intMaj > $intLast) { $tabOut[] = $arrItem; continue; } // MSIN-4328 — dossard récupéré / check-in CT $strRemisDate = trim((string)($arrItem['row']['no_bib_remis_date'] ?? '')); if ($strRemisDate !== '' && $strRemisDate !== '0000-00-00 00:00:00') { $intRemis = strtotime($strRemisDate); if ($intRemis !== false && $intRemis > $intLast) { $tabOut[] = $arrItem; } } } return $tabOut; } function fxChronotrackApiSyncFormatRoleLabel($intRolId) { switch (intval($intRolId)) { case 1: return 'Capitaine'; case 2: return 'Membre'; default: return $intRolId > 0 ? ('Rôle ' . intval($intRolId)) : ''; } } function fxChronotrackApiSyncFormatTeamBlockMessage(array $arrRow) { $arrParts = array('Inscription équipe — transfert individuel seulement (traitement équipe à venir)'); $strTeamName = trim((string)($arrRow['pec_nom_equipe'] ?? $arrRow['par_nom_equipe'] ?? '')); if ($strTeamName !== '') { $arrParts[] = '« ' . $strTeamName . ' »'; } $strNoEquipe = trim((string)($arrRow['no_equipe'] ?? '')); if ($strNoEquipe !== '' && $strNoEquipe !== '0') { $arrParts[] = 'no équipe #' . $strNoEquipe; } return implode(' · ', $arrParts); } function fxChronotrackApiSyncParticipantSummary(array $arrRow) { return array( 'par_id' => intval($arrRow['par_id'] ?? 0), 'external_id' => (string)intval($arrRow['par_id_original'] ?? 0), 'name' => fxChronotrackApiSyncParticipantLabel($arrRow), 'no_bib' => fxChronotrackApiSyncExtractBib($arrRow), 'par_sexe' => trim((string)($arrRow['par_sexe'] ?? '')), 'pec_id' => intval($arrRow['pec_id'] ?? 0), 'is_team' => fxChronotrackApiSyncIsTeamRow($arrRow), 'team_name' => trim((string)($arrRow['pec_nom_equipe'] ?? $arrRow['par_nom_equipe'] ?? '')), 'no_equipe' => trim((string)($arrRow['no_equipe'] ?? '')), 'role_label' => fxChronotrackApiSyncFormatRoleLabel($arrRow['rol_id'] ?? 0), ); } function fxChronotrackApiSyncGroupBlockedByType(array $tabBlocked) { $tabTypeLabels = array( 'team' => 'Inscriptions équipe', 'duplicate_bib' => 'Dossard en double (individuel)', 'no_bib' => 'Dossard manquant', 'no_sex' => 'Sexe non reconnu', 'no_first_name' => 'Prénom manquant', 'no_last_name' => 'Nom manquant', 'no_race' => 'Épreuve non mappée', 'no_external' => 'External ID manquant', ); $tabTypeOrder = array( 'team', 'duplicate_bib', 'no_bib', 'no_sex', 'no_first_name', 'no_last_name', 'no_race', 'no_external', ); $tabGroups = array(); foreach ($tabBlocked as $arrItem) { $strCode = (string)($arrItem['code'] ?? 'other'); if (!isset($tabGroups[$strCode])) { $tabGroups[$strCode] = array( 'code' => $strCode, 'label' => isset($tabTypeLabels[$strCode]) ? $tabTypeLabels[$strCode] : $strCode, 'items' => array(), ); } $tabGroups[$strCode]['items'][] = $arrItem; } $tabOrdered = array(); foreach ($tabTypeOrder as $strCode) { if (isset($tabGroups[$strCode])) { $tabOrdered[] = $tabGroups[$strCode]; unset($tabGroups[$strCode]); } } foreach ($tabGroups as $arrGroup) { $tabOrdered[] = $arrGroup; } return $tabOrdered; } /** * Classe les participants MS1 : transférables vs exclus (dossard, sexe, doublons MS1, mapping). */ function fxChronotrackApiSyncClassifyParticipants(array $tabParticipants, array $arrRaceMap, $intCtEventId = 0) { $tabCandidates = array(); $tabBlocked = array(); $tabBibCounts = array(); foreach ($tabParticipants as $arrRow) { $intExternalId = intval($arrRow['par_id_original'] ?? 0); if ($intExternalId <= 0) { $tabBlocked[] = array( 'code' => 'no_external', 'message' => 'par_id_original manquant', 'par_id' => intval($arrRow['par_id'] ?? 0), 'external_id' => '', 'name' => fxChronotrackApiSyncParticipantLabel($arrRow), 'no_bib' => fxChronotrackApiSyncExtractBib($arrRow), 'par_sexe' => trim((string)($arrRow['par_sexe'] ?? '')), ); continue; } $intEprId = intval($arrRow['epr_id'] ?? 0); $intCtRaceId = intval($arrRaceMap[$intEprId]['ct_race_id'] ?? 0); if ($intCtRaceId <= 0) { $arrSummary = fxChronotrackApiSyncParticipantSummary($arrRow); $strEprLabel = fxChronotrackApiSyncEpreuveLabel($arrRow); // MSIN-4328 — nom d’épreuve en premier (epr_id en secours) if ($strEprLabel !== '') { $strMsg = '« ' . $strEprLabel . ' » (epr_id ' . $intEprId . ') non mappée vers ChronoTrack'; } else { $strMsg = 'Épreuve MS1 (epr_id ' . $intEprId . ') non mappée vers ChronoTrack'; } $tabBlocked[] = array( 'code' => 'no_race', 'message' => $strMsg, 'epr_id' => $intEprId, 'epr_nom' => $strEprLabel, ) + $arrSummary; continue; } $tabCandidates[] = $arrRow; $strBib = fxChronotrackApiSyncExtractBib($arrRow); if ($strBib !== '') { if (!isset($tabBibCounts[$strBib])) { $tabBibCounts[$strBib] = 0; } $tabBibCounts[$strBib]++; } } $tabDuplicateBibs = array(); foreach ($tabBibCounts as $strBib => $intCount) { if ($intCount > 1) { $tabDuplicateBibs[$strBib] = true; } } $tabTransferable = array(); $intBlockedTeam = 0; $intBlockedNoBib = 0; $intBlockedNoSex = 0; $intBlockedNoFirstName = 0; $intBlockedNoLastName = 0; $intBlockedDuplicateBib = 0; foreach ($tabCandidates as $arrRow) { $arrSummary = fxChronotrackApiSyncParticipantSummary($arrRow); if (fxChronotrackApiSyncIsTeamRow($arrRow)) { $intBlockedTeam++; $tabBlocked[] = array( 'code' => 'team', 'message' => fxChronotrackApiSyncFormatTeamBlockMessage($arrRow), ) + $arrSummary; continue; } $strBib = $arrSummary['no_bib']; if ($strBib !== '' && isset($tabDuplicateBibs[$strBib])) { $intBlockedDuplicateBib++; $tabBlocked[] = array( 'code' => 'duplicate_bib', 'message' => 'Dossard #' . $strBib . ' en double — corriger dans MS1', 'bib' => $strBib, ) + $arrSummary; continue; } if ($strBib === '') { $intBlockedNoBib++; $tabBlocked[] = array( 'code' => 'no_bib', 'message' => 'Dossard manquant — corriger dans MS1', ) + $arrSummary; continue; } if (!fxChronotrackApiSyncHasValidSex($arrRow['par_sexe'] ?? '')) { $intBlockedNoSex++; $strSexeMs1 = trim((string)($arrRow['par_sexe'] ?? '')); $tabBlocked[] = array( 'code' => 'no_sex', 'message' => 'Sexe « ' . $strSexeMs1 . ' » — mapping CT à définir', ) + $arrSummary; continue; } // MSIN-4328 — CT refuse sans first_name / last_name $strPrenom = trim((string)($arrRow['par_prenom'] ?? '')); $strNom = trim((string)($arrRow['par_nom'] ?? '')); if ($strPrenom === '') { $intBlockedNoFirstName++; $tabBlocked[] = array( 'code' => 'no_first_name', 'message' => 'Prénom manquant — corriger dans MS1', ) + $arrSummary; continue; } if ($strNom === '') { $intBlockedNoLastName++; $tabBlocked[] = array( 'code' => 'no_last_name', 'message' => 'Nom manquant — corriger dans MS1', ) + $arrSummary; continue; } $arrPayload = fxChronotrackApiSyncBuildEntryPayload($arrRow, $arrRaceMap, $intCtEventId); if ($arrPayload === null) { continue; } $tabTransferable[] = array( 'row' => $arrRow, 'payload' => $arrPayload, 'summary' => $arrSummary, ); } $intBlockedNoRace = 0; $intBlockedNoExternal = 0; foreach ($tabBlocked as $arrItem) { if (($arrItem['code'] ?? '') === 'no_race') { $intBlockedNoRace++; } elseif (($arrItem['code'] ?? '') === 'no_external') { $intBlockedNoExternal++; } } return array( 'transferable' => $tabTransferable, 'blocked' => $tabBlocked, 'blocked_by_type' => fxChronotrackApiSyncGroupBlockedByType($tabBlocked), 'blocked_count' => count($tabBlocked), 'skipped_no_race' => $intBlockedNoRace, 'skipped_no_external' => $intBlockedNoExternal, 'blocked_no_bib' => $intBlockedNoBib, 'blocked_no_sex' => $intBlockedNoSex, 'blocked_no_first_name' => $intBlockedNoFirstName, 'blocked_no_last_name' => $intBlockedNoLastName, 'blocked_duplicate_bib' => $intBlockedDuplicateBib, 'blocked_team' => $intBlockedTeam, 'duplicate_bib_numbers' => array_keys($tabDuplicateBibs), // Alias rétrocompat (compteurs) 'anomalies' => $tabBlocked, 'anomaly_no_bib' => $intBlockedNoBib, 'anomaly_no_sex' => $intBlockedNoSex, 'anomaly_duplicate_bib' => $intBlockedDuplicateBib, ); } function fxChronotrackApiSyncNormalizeBirthdate($strDob) { $strDob = trim((string)$strDob); if ($strDob === '' || $strDob === '0000-00-00') { return ''; } $intTs = strtotime($strDob); if ($intTs === false) { return ''; } return date('Y-m-d', $intTs); } 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() . "'"; $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; } /** * MSIN-4328 — derniers logs sync pour un événement (écran admin). */ function fxChronotrackApiSyncLogList($intEveId, $intLimit = 50) { global $objDatabase; $intEveId = intval($intEveId); $intLimit = max(1, min(200, intval($intLimit))); if ($intEveId <= 0) { return array('state' => 'error', 'message' => 'eve_id manquant', 'logs' => array()); } $sql = "SELECT log_id, eve_id, par_id, action, status, message, created_at" . " FROM api_chronotrack_sync_log" . " WHERE eve_id = " . $intEveId . " ORDER BY log_id DESC" . " LIMIT " . $intLimit; $arrRows = $objDatabase->fxGetResults($sql); $tabOut = array(); if (is_array($arrRows)) { for ($i = 1; $i <= count($arrRows); $i++) { $tabOut[] = $arrRows[$i]; } } return array( 'state' => 'ok', 'logs' => $tabOut, 'count' => count($tabOut), ); } function fxChronotrackApiSyncPaysHasIso3Column() { static $blnHas = null; if ($blnHas !== null) { return $blnHas; } global $objDatabase; $arrRow = $objDatabase->fxGetRow("SHOW COLUMNS FROM inscriptions_pays LIKE 'pay_iso3'"); $blnHas = ($arrRow !== null); return $blnHas; } function fxChronotrackApiSyncLoadMs1Participants($intEveId) { global $objDatabase; $intEveId = intval($intEveId); if ($intEveId <= 0) { return array(); } $strCountryCols = " (SELECT pay_iso FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso2"; if (fxChronotrackApiSyncPaysHasIso3Column()) { $strCountryCols = " (SELECT pay_iso3 FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso3," . " (SELECT pay_iso FROM inscriptions_pays WHERE pay_id = p.pay_id) AS country_iso2"; } $sql = "SELECT p.par_id, p.par_id_original, p.epr_id, p.pec_id, p.rol_id, p.par_equipe, p.par_nom_equipe," . " p.par_prenom, p.par_nom, p.par_sexe," . " p.par_naissance, p.no_bib, p.no_bib_remis, p.no_bib_remis_date, p.par_statut_course," . " p.par_ville, p.par_adresse, p.par_codepostal," . " p.is_cancelled, p.par_maj, p.ct_entry_id," . " ec.pec_equipe, ec.no_equipe, ec.pec_nom_equipe," . " ie.epr_nom_fr AS epr_nom, ie.epr_type_fr AS epr_type," . " (SELECT pro_iso FROM inscriptions_provinces WHERE pro_id = p.pro_id) AS state_iso," . $strCountryCols . " FROM resultats_participants p" . " JOIN resultats_epreuves_commandees ec ON p.pec_id = ec.pec_id_original" . " LEFT JOIN inscriptions_epreuves ie ON ie.epr_id = p.epr_id" . " WHERE ec.pec_actif = 1 AND ec.is_cancelled = 0 AND p.is_cancelled = 0" . " AND p.eve_id = " . $intEveId . " ORDER BY p.par_id_original, p.par_id"; $tabRows = $objDatabase->fxGetResults($sql); if (!is_array($tabRows)) { return array(); } $tabOut = array(); for ($i = 1; $i <= count($tabRows); $i++) { $tabOut[] = $tabRows[$i]; } return $tabOut; } function fxChronotrackApiSyncResolveRegChoiceIdForRace($intCtEventId, $intCtRaceId) { static $tabCache = array(); $strKey = intval($intCtEventId) . ':' . intval($intCtRaceId); if (array_key_exists($strKey, $tabCache)) { return $tabCache[$strKey]; } $tabCache[$strKey] = 0; $arrRaces = fxChronotrackApiFetchRacesForEvent($intCtEventId); if (($arrRaces['state'] ?? '') !== 'ok' || empty($arrRaces['races'])) { return 0; } foreach ($arrRaces['races'] as $arrRace) { if (intval($arrRace['ct_race_id'] ?? 0) !== intval($intCtRaceId)) { continue; } $arrRaw = isset($arrRace['raw']) && is_array($arrRace['raw']) ? $arrRace['raw'] : array(); $arrChoices = fxChronotrackApiCollectRegChoiceEntities($arrRaw); if (count($arrChoices) === 0 && isset($arrRaw['reg_choice_id'])) { $tabCache[$strKey] = intval($arrRaw['reg_choice_id']); return $tabCache[$strKey]; } foreach ($arrChoices as $arrChoice) { if (!is_array($arrChoice) || empty($arrChoice['reg_choice_id'])) { continue; } $tabCache[$strKey] = intval($arrChoice['reg_choice_id']); return $tabCache[$strKey]; } } return 0; } function fxChronotrackApiSyncBuildEntryPayload(array $arrRow, array $arrRaceMap, $intCtEventId = 0) { $intEprId = intval($arrRow['epr_id'] ?? 0); $intCtRaceId = intval($arrRaceMap[$intEprId]['ct_race_id'] ?? 0); if ($intCtRaceId <= 0) { return null; } $intExternalId = intval($arrRow['par_id_original'] ?? 0); if ($intExternalId <= 0) { return null; } $strCountry = strtoupper(trim((string)($arrRow['country_iso3'] ?? ''))); if ($strCountry === '') { $strCountry = fxChronotrackApiSyncIso2ToIso3($arrRow['country_iso2'] ?? ''); } $strState = strtoupper(trim((string)($arrRow['state_iso'] ?? ''))); $strSex = fxChronotrackApiSyncNormalizeSex($arrRow['par_sexe'] ?? ''); if ($strSex === '') { return null; } $strBib = fxChronotrackApiSyncExtractBib($arrRow); if ($strBib === '') { return null; } $arrEntry = array( 'external_id' => (string)$intExternalId, 'race_id' => $intCtRaceId, 'first_name' => trim((string)($arrRow['par_prenom'] ?? '')), 'last_name' => trim((string)($arrRow['par_nom'] ?? '')), 'sex' => $strSex, ); if ($intCtEventId > 0) { $arrEntry['event_id'] = $intCtEventId; } $intRegChoiceId = fxChronotrackApiSyncResolveRegChoiceIdForRace($intCtEventId, $intCtRaceId); if ($intRegChoiceId > 0) { $arrEntry['reg_choice_id'] = $intRegChoiceId; } $strDob = fxChronotrackApiSyncNormalizeBirthdate($arrRow['par_naissance'] ?? ''); if ($strDob !== '') { $arrEntry['birthdate'] = $strDob; } $strStreet = trim((string)($arrRow['par_adresse'] ?? '')); if ($strStreet !== '') { $arrEntry['street'] = $strStreet; } $strCity = trim((string)($arrRow['par_ville'] ?? '')); if ($strCity !== '') { $arrEntry['city'] = $strCity; } $strPostal = trim((string)($arrRow['par_codepostal'] ?? '')); if ($strPostal !== '') { $arrEntry['postal_code'] = $strPostal; } if ($strState !== '') { $arrEntry['state_code'] = $strState; } if ($strCountry !== '') { $arrEntry['country_code'] = $strCountry; } $arrEntry['entry_status'] = fxChronotrackApiSyncMapEntryStatus( $arrRow['par_statut_course'] ?? 'CONF', intval($arrRow['is_cancelled'] ?? 0) === 1 ); $arrEntry['status'] = $arrEntry['entry_status']; // MSIN-4328 — CT refuse « assign a bib » si l’entry n’est pas confirmed. // Un simple DQ (ou autre statut) ne doit pas renvoyer le dossard. // Bib envoyé seulement à la 1re synchro (pas encore de ct_entry_id) + statut CONF. $blnLinkedCt = (intval($arrRow['ct_entry_id'] ?? 0) > 0); if (!$blnLinkedCt && $arrEntry['entry_status'] === 'CONF') { $arrEntry['bib'] = $strBib; } // MSIN-4328 — Dossard récupéré (MS1) → Check In Status CT (entry_check_in = "1" / null) $arrEntry['entry_check_in'] = (intval($arrRow['no_bib_remis'] ?? 0) === 1) ? '1' : null; return $arrEntry; } function fxChronotrackApiSyncPayloadForPost(array $arrPayload) { $arrOut = $arrPayload; unset($arrOut['entry_id']); return $arrOut; } function fxChronotrackApiSyncPayloadForPut(array $arrPayload) { $arrOut = $arrPayload; unset($arrOut['entry_id']); return $arrOut; } function fxChronotrackApiSyncPutEntry($strEntryId, array $arrPayload) { $strEntryId = trim((string)$strEntryId); if ($strEntryId === '') { return array('state' => 'error', 'message' => 'entry_id manquant pour PUT'); } $arrPut = fxChronotrackApiOAuthApiPut('entry/' . $strEntryId, fxChronotrackApiSyncPayloadForPut($arrPayload)); if ($arrPut['state'] === 'ok') { $arrPut['method'] = 'PUT'; } return $arrPut; } function fxChronotrackApiSyncPushOneEntry($intCtEventId, array $arrPayload) { $intCtEventId = intval($intCtEventId); $strEntryId = trim((string)($arrPayload['entry_id'] ?? '')); if ($strEntryId !== '') { $arrPut = fxChronotrackApiSyncPutEntry($strEntryId, $arrPayload); $arrPut['method'] = 'PUT'; return $arrPut; } $arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, array(fxChronotrackApiSyncPayloadForPost($arrPayload))); if ($arrPost['state'] === 'ok') { $arrPost['method'] = 'POST'; } return $arrPost; } function fxChronotrackApiSyncEnrichPayloadWithCtEntry(array $arrPayload, $arrCtEntity) { if (!is_array($arrCtEntity)) { return $arrPayload; } $strEntryId = fxChronotrackApiEntityId($arrCtEntity); if ($strEntryId !== '') { $arrPayload['entry_id'] = $strEntryId; } return $arrPayload; } function fxChronotrackApiSyncPostEntries($intCtEventId, array $tabEntries) { $intCtEventId = intval($intCtEventId); if ($intCtEventId <= 0 || count($tabEntries) === 0) { return array('state' => 'error', 'message' => 'Aucune entry à envoyer'); } static $strBodyFormat = 'array'; if ($strBodyFormat === 'event_entry') { $arrPost = fxChronotrackApiOAuthApiPost( 'event/' . $intCtEventId . '/entry', array('event_entry' => $tabEntries) ); if ($arrPost['state'] === 'ok') { $arrPost['body_format'] = 'event_entry'; return $arrPost; } return $arrPost; } // Doc CT : POST body = tableau JSON d'entités entry (pas de wrapper event_entry). $arrPost = fxChronotrackApiOAuthApiPost('event/' . $intCtEventId . '/entry', $tabEntries); if ($arrPost['state'] === 'ok') { $arrPost['body_format'] = 'array'; $strBodyFormat = 'array'; return $arrPost; } // Essai wrapper une seule fois (mémoire du format si succès). $arrPostWrap = fxChronotrackApiOAuthApiPost( 'event/' . $intCtEventId . '/entry', array('event_entry' => $tabEntries) ); if ($arrPostWrap['state'] === 'ok') { $strBodyFormat = 'event_entry'; $arrPostWrap['body_format'] = 'event_entry'; return $arrPostWrap; } $arrPost['body_format'] = 'array'; $arrPost['fallback_error'] = $arrPostWrap['message'] ?? ''; return $arrPost; } /** * POST entries avec découpage dichotomique si le lot échoue (évite 250 appels unitaires). * MSIN-4328 */ function fxChronotrackApiSyncPostEntriesAdaptive($intCtEventId, array $tabEntries, array &$tabErrors, $intEveId = 0, array $tabMeta = array()) { $intCount = count($tabEntries); if ($intCount === 0) { return array('ok' => 0, 'err' => 0); } $arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, $tabEntries); if ($arrPost['state'] === 'ok') { // MSIN-4328 — récupérer entry_id CT pour lien admin if ($intEveId > 0 && count($tabMeta) > 0) { fxChronotrackApiSyncApplyEntryIdsFromApiJson($arrPost['json'] ?? null, $tabMeta); } return array('ok' => $intCount, 'err' => 0); } if ($intCount === 1) { $arrM = $tabMeta[0] ?? array('par_id' => 0, 'external_id' => ''); $strOneErr = $arrPost['message'] ?? 'Erreur POST entry'; if (count($tabErrors) < 12) { $tabErrors[] = 'external_id=' . ($arrM['external_id'] ?? '') . ' — ' . $strOneErr; } if ($intEveId > 0) { fxChronotrackApiSyncLog($intEveId, intval($arrM['par_id'] ?? 0), 'push_entry', 'error', 'external_id=' . ($arrM['external_id'] ?? '') . ' — ' . $strOneErr); } return array('ok' => 0, 'err' => 1); } $intMid = (int)floor($intCount / 2); $arrLeft = fxChronotrackApiSyncPostEntriesAdaptive( $intCtEventId, array_slice($tabEntries, 0, $intMid), $tabErrors, $intEveId, array_slice($tabMeta, 0, $intMid) ); $arrRight = fxChronotrackApiSyncPostEntriesAdaptive( $intCtEventId, array_slice($tabEntries, $intMid), $tabErrors, $intEveId, array_slice($tabMeta, $intMid) ); return array( 'ok' => $arrLeft['ok'] + $arrRight['ok'], 'err' => $arrLeft['err'] + $arrRight['err'], ); } function fxChronotrackApiSyncProbePush($intEveId) { $arrConfig = fxChronotrackApiConfigGet($intEveId); if ($arrConfig === null) { return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack'); } $intCtEventId = intval($arrConfig['ct_event_id']); $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); if (count($arrClass['transferable']) === 0) { return array('state' => 'error', 'message' => 'Aucun participant transférable pour la sonde (voir anomalies).'); } $arrFirst = $arrClass['transferable'][0]; $arrPayload = $arrFirst['payload']; $arrRow = $arrFirst['row']; $arrCtFetch = fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId); if ($arrCtFetch['state'] === 'ok') { $strExt = $arrPayload['external_id']; $arrPayload = fxChronotrackApiSyncEnrichPayloadWithCtEntry( $arrPayload, $arrCtFetch['entries'][$strExt] ?? null ); } $arrPost = fxChronotrackApiSyncPushOneEntry($intCtEventId, $arrPayload); return array( 'state' => $arrPost['state'], 'message' => $arrPost['message'] ?? '', 'ct_event_id' => $intCtEventId, 'par_id' => intval($arrRow['par_id'] ?? 0), 'payload' => $arrPayload, 'method' => $arrPost['method'] ?? '', 'body_format' => $arrPost['body_format'] ?? '', 'http_code' => intval($arrPost['http']['http_code'] ?? 0), 'response_body' => substr(trim((string)($arrPost['http']['body'] ?? '')), 0, 2000), 'fallback_error'=> $arrPost['fallback_error'] ?? '', ); } /** * MSIN-4328 — sonde « custom field » : tente comme un import Custom * (POST entry avec custom_Ms1Probe*) + lit l’entry, + teste endpoints question. */ function fxChronotrackApiSyncProbeCustomField($intEveId) { $arrConfig = fxChronotrackApiConfigGet($intEveId); if ($arrConfig === null) { return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack'); } if (!fxChronotrackApiSettingsConfigured()) { return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage()); } $intCtEventId = intval($arrConfig['ct_event_id']); $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); if (count($arrClass['transferable']) === 0) { return array('state' => 'error', 'message' => 'Aucun participant transférable pour la sonde custom.'); } // Préférer une entry déjà liée CT (maj légère, pas de création complète) $arrPick = null; foreach ($arrClass['transferable'] as $arrItem) { if (intval($arrItem['row']['ct_entry_id'] ?? 0) > 0) { $arrPick = $arrItem; break; } } if ($arrPick === null) { $arrPick = $arrClass['transferable'][0]; } $arrRow = $arrPick['row']; $strExternalId = (string)($arrPick['payload']['external_id'] ?? ''); $intCtEntryId = intval($arrRow['ct_entry_id'] ?? 0); $strFieldKey = 'custom_Ms1Probe'; $strValue = 'ms1-probe-' . date('YmdHis'); $tabQuestionEndpoints = array(); foreach (array('question', 'custom_question', 'survey_question', 'reg_question') as $strEp) { $arrG = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/' . $strEp); $tabQuestionEndpoints[] = array( 'path' => 'event/' . $intCtEventId . '/' . $strEp, 'http_code' => intval($arrG['http']['http_code'] ?? 0), 'state' => $arrG['state'] ?? 'error', 'message' => isset($arrG['message']) ? substr((string)$arrG['message'], 0, 200) : '', ); } // Corps minimal : identifier + champ custom (mimique import « Custom ») $arrProbePayload = array( 'external_id' => $strExternalId, $strFieldKey => $strValue, ); if ($intCtEntryId > 0) { // Certaines maj CT acceptent l’id numérique $arrProbePayload['entry_id'] = (string)$intCtEntryId; } $arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, array($arrProbePayload)); $mixReadbackValue = null; $blnFieldPresent = false; $arrReadback = null; $strReadbackHow = ''; if ($intCtEntryId > 0) { $arrGet = fxChronotrackApiOAuthApiGet('entry/' . $intCtEntryId); $strReadbackHow = 'GET entry/' . $intCtEntryId; if (($arrGet['state'] ?? '') === 'ok' && is_array($arrGet['json'] ?? null)) { $arrReadback = $arrGet['json']; // CT peut wrapper { entry: {...} } ou renvoyer l’objet direct if (isset($arrReadback['entry']) && is_array($arrReadback['entry'])) { $arrReadback = $arrReadback['entry']; } elseif (isset($arrReadback['event_entry'][0]) && is_array($arrReadback['event_entry'][0])) { $arrReadback = $arrReadback['event_entry'][0]; } if (array_key_exists($strFieldKey, $arrReadback)) { $blnFieldPresent = true; $mixReadbackValue = $arrReadback[$strFieldKey]; } } } if (!$blnFieldPresent) { // Repli : 1re page entries et recherche par external_id $arrList = fxChronotrackApiOAuthApiGet( 'event/' . $intCtEventId . '/entry', array('page' => 1, 'page_size' => 50) ); if ($strReadbackHow === '') { $strReadbackHow = 'GET event/.../entry page1'; } else { $strReadbackHow .= ' + list page1'; } $tabEnt = array(); if (($arrList['state'] ?? '') === 'ok' && is_array($arrList['json'] ?? null)) { $tabEnt = fxChronotrackApiNormalizeEntityList($arrList['json'], 'entry'); if (count($tabEnt) === 0 && isset($arrList['json']['event_entry']) && is_array($arrList['json']['event_entry'])) { $tabEnt = $arrList['json']['event_entry']; } } foreach ($tabEnt as $arrEnt) { if (!is_array($arrEnt)) { continue; } $strExt = trim((string)( $arrEnt['entry_external_id'] ?? $arrEnt['external_id'] ?? '' )); if ($strExt !== $strExternalId) { continue; } $arrReadback = $arrEnt; if (array_key_exists($strFieldKey, $arrEnt)) { $blnFieldPresent = true; $mixReadbackValue = $arrEnt[$strFieldKey]; } break; } } $strVerdict = 'POST ok mais champ absent au relecture (CT n’a pas créé le custom comme à l’import).'; $strState = 'error'; if (($arrPost['state'] ?? '') !== 'ok') { $strVerdict = 'POST refusé — ' . (string)($arrPost['message'] ?? 'erreur'); $strState = 'error'; } elseif ($blnFieldPresent && (string)$mixReadbackValue === $strValue) { $strVerdict = 'OK — CT a accepté et conservé ' . $strFieldKey . ' (création / slot custom plausible).'; $strState = 'ok'; } elseif ($blnFieldPresent) { $strVerdict = 'POST ok, champ présent mais valeur différente: ' . json_encode($mixReadbackValue, JSON_UNESCAPED_UNICODE); $strState = 'partial'; } fxChronotrackApiSyncLog( $intEveId, intval($arrRow['par_id'] ?? 0), 'probe_custom', ($strState === 'ok') ? 'ok' : 'error', $strVerdict ); return array( 'state' => $strState, 'message' => $strVerdict, 'ct_event_id' => $intCtEventId, 'par_id' => intval($arrRow['par_id'] ?? 0), 'external_id' => $strExternalId, 'ct_entry_id' => $intCtEntryId, 'field_key' => $strFieldKey, 'field_value_sent' => $strValue, 'field_present' => $blnFieldPresent, 'field_value_read' => $mixReadbackValue, 'payload' => $arrProbePayload, 'post_http_code' => intval($arrPost['http']['http_code'] ?? 0), 'post_response' => substr(trim((string)($arrPost['http']['body'] ?? '')), 0, 1500), 'readback_how' => $strReadbackHow, 'question_endpoints' => $tabQuestionEndpoints, ); } /** * Sonde perf — teste 10 / 50 / 100 / 200 entries (upsert, mêmes gens). MSIN-4328 * Évite un push complet pour calibrer la taille de lot. */ function fxChronotrackApiSyncProbeBatchPerf($intEveId) { $arrConfig = fxChronotrackApiConfigGet($intEveId); if ($arrConfig === null) { return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack'); } if (!fxChronotrackApiSettingsConfigured()) { return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage()); } $intCtEventId = intval($arrConfig['ct_event_id']); $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); $intAvail = count($arrClass['transferable']); if ($intAvail < 10) { return array( 'state' => 'error', 'message' => 'Il faut au moins 10 éligibles pour la sonde lots (actuellement ' . $intAvail . ').', ); } $tabPayloads = array(); $intMax = min(200, $intAvail); for ($i = 0; $i < $intMax; $i++) { $tabPayloads[] = fxChronotrackApiSyncPayloadForPost($arrClass['transferable'][$i]['payload']); } $tabSizes = array(10, 50, 100, 200); $tabResults = array(); $intBest = 10; foreach ($tabSizes as $intSize) { if ($intSize > count($tabPayloads)) { break; } $tabSlice = array_slice($tabPayloads, 0, $intSize); $floatStart = microtime(true); $arrPost = fxChronotrackApiSyncPostEntries($intCtEventId, $tabSlice); $floatMs = round((microtime(true) - $floatStart) * 1000); $blnOk = (($arrPost['state'] ?? '') === 'ok'); $tabResults[] = array( 'size' => $intSize, 'ok' => $blnOk, 'ms' => $floatMs, 'http_code' => intval($arrPost['http']['http_code'] ?? 0), 'message' => $blnOk ? 'OK' : ($arrPost['message'] ?? 'échec'), 'ms_per' => $blnOk && $intSize > 0 ? round($floatMs / $intSize, 1) : null, ); if ($blnOk) { $intBest = $intSize; } else { break; } } $intEst2000 = ($intBest > 0) ? (int)ceil(2000 / $intBest) * max(1, intval($tabResults[count($tabResults) - 1]['ms'] ?? 1000)) : 0; return array( 'state' => 'ok', 'message' => 'Plus grand lot OK : ' . $intBest . ' — lot push actuel configuré : ' . MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE, 'recommended' => $intBest, 'configured' => MSIN_API_CHRONOTRACK_SYNC_BATCH_SIZE, 'results' => $tabResults, 'est_ms_2000' => $intEst2000, 'est_min_2000' => $intEst2000 > 0 ? round($intEst2000 / 60000, 1) : null, 'ct_event_id' => $intCtEventId, ); } /** * Liste toutes les entries CT (pagination), dédupliquées par entry_id. * Ne filtre pas sur external_id — décompte réel côté CT. */ function fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId) { $intCtEventId = intval($intCtEventId); $tabAll = array(); $tabSeenEntryIds = array(); $intPage = 1; $intPageSize = 100; $intMaxPages = 500; while ($intPage <= $intMaxPages) { $arrApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry', array( 'page' => $intPage, 'page_size' => $intPageSize, )); if ($arrApi['state'] !== 'ok') { return array( 'state' => 'error', 'message' => $arrApi['message'] ?? 'Erreur lecture entries CT', 'entities' => $tabAll, ); } $arrEntities = fxChronotrackApiNormalizeEntityList($arrApi['json'], 'entry'); if (count($arrEntities) === 0) { break; } foreach ($arrEntities as $arrEntity) { if (!is_array($arrEntity)) { continue; } $strEntryId = fxChronotrackApiEntityId($arrEntity); if ($strEntryId === '' || isset($tabSeenEntryIds[$strEntryId])) { continue; } $tabSeenEntryIds[$strEntryId] = true; $tabAll[] = $arrEntity; } if (count($arrEntities) < $intPageSize) { break; } $intPage++; } return array( 'state' => 'ok', 'message' => '', 'entities' => $tabAll, 'count' => count($tabAll), ); } function fxChronotrackApiSyncFetchCtEntriesIndexed($intCtEventId) { $arrFetchAll = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId); if ($arrFetchAll['state'] !== 'ok') { return array( 'state' => 'error', 'message' => $arrFetchAll['message'] ?? 'Erreur lecture entries CT', 'entries' => array(), ); } $tabIndexed = array(); foreach ($arrFetchAll['entities'] as $arrEntity) { if (!is_array($arrEntity)) { continue; } $strExt = fxChronotrackApiEntityExternalId($arrEntity); if ($strExt === '') { continue; } $tabIndexed[$strExt] = $arrEntity; } return array( 'state' => 'ok', 'message' => '', 'entries' => $tabIndexed, 'entities' => $arrFetchAll['entities'], 'total_entry_count' => intval($arrFetchAll['count'] ?? count($arrFetchAll['entities'])), 'without_external_id'=> max(0, intval($arrFetchAll['count'] ?? 0) - count($tabIndexed)), ); } function fxChronotrackApiSyncFetchCtEntryRows($intCtEventId) { $arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId); if ($arrFetch['state'] !== 'ok') { return $arrFetch; } $tabRows = array(); foreach ($arrFetch['entities'] as $arrEntity) { if (!is_array($arrEntity)) { continue; } $strEntryId = fxChronotrackApiEntityId($arrEntity); if ($strEntryId === '') { continue; } $tabRows[] = array( 'entry_id' => $strEntryId, 'external_id' => fxChronotrackApiEntityExternalId($arrEntity), 'entity' => $arrEntity, ); } return array( 'state' => 'ok', 'message' => '', 'rows' => $tabRows, 'count' => count($tabRows), ); } function fxChronotrackApiSyncBuildPreview($intEveId) { $arrConfig = fxChronotrackApiConfigGet($intEveId); if ($arrConfig === null) { return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack'); } if (!fxChronotrackApiSettingsConfigured()) { return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage()); } $intCtEventId = intval($arrConfig['ct_event_id']); $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); $intMs1Total = count($tabParticipants); $intTransferable = count($arrClass['transferable']); // MSIN-4328 — preview MS1 seul (rapide). Plus de scan CT fiche-par-fiche : // le push upsert crée/maj sans avoir besoin du inventaire Live ici. $strLastPushAt = trim((string)($arrConfig['last_push_at'] ?? '')); $tabPending = fxChronotrackApiSyncFilterChangedSincePush($arrClass['transferable'], $strLastPushAt); $intPendingPush = count($tabPending); return array( 'state' => 'ok', 'ct_event_id' => $intCtEventId, 'ms1_total' => $intMs1Total, 'transferable' => $intTransferable, 'blocked_count' => $arrClass['blocked_count'], 'blocked_by_type' => $arrClass['blocked_by_type'], 'blocked' => $arrClass['blocked'], 'eligible' => $intTransferable, 'skipped_no_race' => $arrClass['skipped_no_race'], 'skipped_no_external' => $arrClass['skipped_no_external'], 'blocked_no_bib' => $arrClass['blocked_no_bib'], 'blocked_no_sex' => $arrClass['blocked_no_sex'], 'blocked_duplicate_bib' => $arrClass['blocked_duplicate_bib'], 'blocked_team' => $arrClass['blocked_team'], 'anomaly_no_bib' => $arrClass['blocked_no_bib'], 'anomaly_no_sex' => $arrClass['blocked_no_sex'], 'anomaly_duplicate_bib' => $arrClass['blocked_duplicate_bib'], 'duplicate_bib_numbers' => $arrClass['duplicate_bib_numbers'], 'anomalies' => $arrClass['blocked'], 'ct_total' => null, 'ct_total_loaded' => false, 'already_in_ct' => null, 'will_update' => null, 'to_create' => null, 'will_create' => null, 'ct_orphans' => null, 'ct_without_external_id' => null, 'to_push' => $intPendingPush, 'pending_push' => $intPendingPush, 'last_push_at' => ($strLastPushAt !== '' && $strLastPushAt !== '0000-00-00 00:00:00') ? $strLastPushAt : null, 'last_push_ok_count' => intval($arrConfig['last_push_ok_count'] ?? 0), 'is_first_push' => ($strLastPushAt === '' || $strLastPushAt === '0000-00-00 00:00:00'), ); } /** * MSIN-4328 — total entries CT Live (sans construire le diff MS1↔CT). * Essaie d’abord un total dans la réponse API ; sinon compte par pagination légère. */ function fxChronotrackApiSyncExtractCtListTotal($mixJson) { if (!is_array($mixJson)) { return null; } $tabKeys = array( 'total', 'total_count', 'totalCount', 'count', 'entry_count', 'num_results', 'results_count', 'nb_results', 'total_entries', ); foreach ($tabKeys as $strKey) { if (isset($mixJson[$strKey]) && is_numeric($mixJson[$strKey])) { $intVal = intval($mixJson[$strKey]); if ($intVal >= 0) { return $intVal; } } } foreach (array('pagination', 'page', 'meta', 'paging') as $strNest) { if (!isset($mixJson[$strNest]) || !is_array($mixJson[$strNest])) { continue; } $intNested = fxChronotrackApiSyncExtractCtListTotal($mixJson[$strNest]); if ($intNested !== null) { return $intNested; } } return null; } /** * MSIN-4328 — total entries CT Live (+ backfill ct_entry_id si eve_id fourni). */ function fxChronotrackApiSyncFetchCtEntryCount($intCtEventId, $intEveId = 0) { global $objDatabase; $intCtEventId = intval($intCtEventId); $intEveId = intval($intEveId); if ($intCtEventId <= 0) { return array('state' => 'error', 'message' => 'ct_event_id manquant', 'ct_total' => null); } $tabByExt = array(); if ($intEveId > 0) { $sqlMap = "SELECT par_id, par_id_original FROM resultats_participants" . " WHERE eve_id = " . $intEveId . " AND is_cancelled = 0" . " AND IFNULL(par_id_original, 0) > 0"; $arrMapRows = $objDatabase->fxGetResults($sqlMap); if (is_array($arrMapRows)) { for ($i = 1; $i <= count($arrMapRows); $i++) { $strExt = (string)intval($arrMapRows[$i]['par_id_original']); $tabByExt[$strExt] = intval($arrMapRows[$i]['par_id']); } } } // Si pas de backfill et métadonnée total dispo → 1 appel. if (count($tabByExt) === 0) { $arrApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry', array( 'page' => 1, 'page_size' => 1, )); if ($arrApi['state'] !== 'ok') { return array( 'state' => 'error', 'message' => $arrApi['message'] ?? 'Erreur lecture entries CT', 'ct_total' => null, ); } $intFromMeta = fxChronotrackApiSyncExtractCtListTotal($arrApi['json'] ?? null); if ($intFromMeta !== null) { return array( 'state' => 'ok', 'message' => 'Total ChronoTrack Live (métadonnée API)', 'ct_total' => $intFromMeta, 'via' => 'meta', 'backfilled'=> 0, ); } } $intTotal = 0; $intBackfilled = 0; $intPage = 1; $intPageSize = 250; $intMaxPages = 200; while ($intPage <= $intMaxPages) { $arrPage = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId . '/entry', array( 'page' => $intPage, 'page_size' => $intPageSize, )); if ($arrPage['state'] !== 'ok') { return array( 'state' => 'error', 'message' => $arrPage['message'] ?? 'Erreur lecture entries CT', 'ct_total' => ($intTotal > 0) ? $intTotal : null, 'backfilled' => $intBackfilled, ); } $arrEntities = fxChronotrackApiNormalizeEntityList($arrPage['json'], 'entry'); $intN = count($arrEntities); $intTotal += $intN; if (count($tabByExt) > 0) { $tabMetaPage = array(); foreach ($arrEntities as $arrEnt) { if (!is_array($arrEnt)) { continue; } $strExt = fxChronotrackApiEntityExternalId($arrEnt); if ($strExt === '' || !isset($tabByExt[$strExt])) { continue; } $tabMetaPage[] = array( 'par_id' => $tabByExt[$strExt], 'external_id' => $strExt, ); } if (count($tabMetaPage) > 0) { $intBackfilled += fxChronotrackApiSyncApplyEntryIdsFromApiJson( array('event_entry' => $arrEntities), $tabMetaPage ); } } if ($intN < $intPageSize) { break; } $intPage++; } return array( 'state' => 'ok', 'message' => 'Total ChronoTrack Live (comptage paginé)' . ($intBackfilled > 0 ? (' — ' . $intBackfilled . ' entry_id liés') : ''), 'ct_total' => $intTotal, 'via' => 'pages', 'backfilled' => $intBackfilled, ); } /** * Stockage job push (fichier temp) — évite de saturer $_SESSION sur ~2000 entries. MSIN-4328 */ function fxChronotrackApiSyncPushJobPath($intEveId) { $strDir = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'ms1_ct_push'; if (!is_dir($strDir)) { @mkdir($strDir, 0700, true); } $strSid = substr(preg_replace('/[^a-zA-Z0-9_-]/', '', (string)session_id()), 0, 40); if ($strSid === '') { $strSid = 'nosession'; } return $strDir . DIRECTORY_SEPARATOR . 'eve' . intval($intEveId) . '_' . $strSid . '.json'; } function fxChronotrackApiSyncPushJobLoad($intEveId) { $strPath = fxChronotrackApiSyncPushJobPath($intEveId); if (!is_file($strPath)) { return null; } $strRaw = @file_get_contents($strPath); if ($strRaw === false || $strRaw === '') { return null; } $arrJob = json_decode($strRaw, true); return is_array($arrJob) ? $arrJob : null; } function fxChronotrackApiSyncPushJobSave($intEveId, array $arrJob) { $strPath = fxChronotrackApiSyncPushJobPath($intEveId); $strJson = json_encode($arrJob, JSON_UNESCAPED_UNICODE); if (!is_string($strJson)) { return false; } return (@file_put_contents($strPath, $strJson, LOCK_EX) !== false); } function fxChronotrackApiSyncPushJobClear($intEveId) { $strPath = fxChronotrackApiSyncPushJobPath($intEveId); if (is_file($strPath)) { @unlink($strPath); } } /** * Prépare un job de push (liste éligibles + entry_id CT). MSIN-4328. */ function fxChronotrackApiSyncPushPrepare($intEveId) { $intEveId = intval($intEveId); $arrConfig = fxChronotrackApiConfigGet($intEveId); if ($arrConfig === null) { return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack'); } if (!fxChronotrackApiSettingsConfigured()) { return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage()); } $intCtEventId = intval($arrConfig['ct_event_id']); $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); // MSIN-4328 — ne plus journaliser push_skip ici (spam à chaque prepare/auto). // Les exclus restent dans l’UI « Détails & exclus » (preview), jamais dans le lot poussé. // MSIN-4328 — ne pousser que les éligibles modifiés depuis last_push_at $strLastPushAt = trim((string)($arrConfig['last_push_at'] ?? '')); $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']); $tabMeta[] = array( 'par_id' => intval($arrItem['row']['par_id'] ?? 0), 'external_id' => $arrItem['payload']['external_id'], ); } if (count($tabBatch) === 0) { fxChronotrackApiSyncPushJobClear($intEveId); $strMsg = count($arrClass['transferable']) === 0 ? ('Aucun participant transférable.' . ($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, 'blocked_count' => $arrClass['blocked_count'], 'transferable' => count($arrClass['transferable']), 'pending_push' => 0, 'total' => 0, ); } 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, 'meta' => $tabMeta, 'blocked_count' => $arrClass['blocked_count'], 'ok_sum' => 0, 'err_sum' => 0, 'created_at' => time(), ); if (!fxChronotrackApiSyncPushJobSave($intEveId, $arrJob)) { return array( 'state' => 'error', 'message' => 'Impossible d’écrire le fichier temporaire du push (permissions /tmp).', ); } return array( 'state' => 'ok', 'message' => count($tabBatch) . ' prêts à envoyer', '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, ); } /** * Push un slice du job préparé. MSIN-4328. */ function fxChronotrackApiSyncPushEvent($intEveId, $arrOptions = array()) { $intEveId = intval($intEveId); $intOffset = max(0, intval($arrOptions['offset'] ?? 0)); $intLimit = max(0, intval($arrOptions['limit'] ?? 0)); if ($intLimit <= 0) { $intLimit = MSIN_API_CHRONOTRACK_SYNC_CHUNK_SIZE; } $arrJob = fxChronotrackApiSyncPushJobLoad($intEveId); if (!is_array($arrJob) || empty($arrJob['batch'])) { $arrPrep = fxChronotrackApiSyncPushPrepare($intEveId); if ($arrPrep['state'] !== 'ok') { return $arrPrep; } $arrJob = fxChronotrackApiSyncPushJobLoad($intEveId); } if (!is_array($arrJob) || empty($arrJob['batch'])) { return array('state' => 'error', 'message' => 'Job de push introuvable — relancer'); } $intCtEventId = intval($arrJob['ct_event_id']); $tabBatchAll = $arrJob['batch']; $tabMetaAll = $arrJob['meta']; $intTotal = count($tabBatchAll); $intSkipped = intval($arrJob['blocked_count'] ?? 0); $tabBatch = array_slice($tabBatchAll, $intOffset, $intLimit); $tabMeta = array_slice($tabMetaAll, $intOffset, $intLimit); $intChunkCount = count($tabBatch); if ($intChunkCount === 0) { fxChronotrackApiSyncPushJobClear($intEveId); return array( 'state' => 'ok', 'message' => 'Rien à envoyer à cet offset', 'pushed_ok' => 0, 'pushed_put' => 0, 'pushed_post' => 0, 'pushed_error' => 0, 'total' => $intTotal, 'offset' => $intOffset, 'next_offset' => $intOffset, 'chunk_done' => 0, 'done' => true, 'blocked_count' => $intSkipped, ); } $intOk = 0; $intErr = 0; $intPutOk = 0; $intPostOk = 0; $tabErrors = array(); $tabCreates = array(); $tabCreatesMeta = array(); $tabUpdates = array(); $tabUpdatesMeta = array(); foreach ($tabBatch as $intIdx => $arrPayload) { $arrMeta = $tabMeta[$intIdx]; if (trim((string)($arrPayload['entry_id'] ?? '')) !== '') { $tabUpdates[] = $arrPayload; $tabUpdatesMeta[] = $arrMeta; } else { $tabCreates[] = fxChronotrackApiSyncPayloadForPost($arrPayload); $tabCreatesMeta[] = $arrMeta; } } // Créations + maj via POST lot (upsert) — découpage dichotomique si échec. if (count($tabCreates) > 0) { $arrRes = fxChronotrackApiSyncPostEntriesAdaptive( $intCtEventId, $tabCreates, $tabErrors, $intEveId, $tabCreatesMeta ); $intOk += $arrRes['ok']; $intPostOk += $arrRes['ok']; $intErr += $arrRes['err']; } if (count($tabUpdates) > 0) { $tabUpdatesForPost = array(); foreach ($tabUpdates as $arrPayload) { $tabUpdatesForPost[] = fxChronotrackApiSyncPayloadForPost($arrPayload); } $arrRes = fxChronotrackApiSyncPostEntriesAdaptive( $intCtEventId, $tabUpdatesForPost, $tabErrors, $intEveId, $tabUpdatesMeta ); $intOk += $arrRes['ok']; $intPostOk += $arrRes['ok']; $intErr += $arrRes['err']; } // Si des MAJ restent en erreur (POST n’a pas pris le statut), tenter PUT unitaire // uniquement pour ce petit sous-ensemble déjà en erreur — trop coûteux de re-PUT tout. // Les erreurs sont déjà dans $tabErrors / logs. fxChronotrackApiSyncLog( $intEveId, 0, 'push_chunk', ($intErr === 0) ? 'ok' : 'error', 'offset=' . $intOffset . ' n=' . $intChunkCount . ' ok=' . $intOk . ' err=' . $intErr . ' POST=' . $intPostOk . ' PUT=' . $intPutOk ); $intOkSum = intval($arrJob['ok_sum'] ?? 0) + $intOk; $intErrSum = intval($arrJob['err_sum'] ?? 0) + $intErr; $arrJob['ok_sum'] = $intOkSum; $arrJob['err_sum'] = $intErrSum; $intNextOffset = $intOffset + $intChunkCount; $blnDone = ($intNextOffset >= $intTotal); $blnLastPushSaved = false; if ($blnDone) { // MSIN-4328 — ne figer last_push_at que si tout le job a réussi if ($intErrSum === 0) { $blnLastPushSaved = fxChronotrackApiConfigSetLastPush($intEveId, $intOkSum); } fxChronotrackApiSyncPushJobClear($intEveId); } else { fxChronotrackApiSyncPushJobSave($intEveId, $arrJob); } $strState = ($intErr === 0) ? 'ok' : (($intOk > 0) ? 'partial' : 'error'); $strMessage = ($intErr === 0) ? ($intOk . ' envoyé(s) (offset ' . $intOffset . ').') : ($intOk . ' OK, ' . $intErr . ' erreur(s) sur ce lot.'); return array( 'state' => $strState, 'message' => $strMessage, 'pushed_ok' => $intOk, 'pushed_put' => $intPutOk, 'pushed_post' => $intPostOk, 'pushed_error' => $intErr, 'job_ok_sum' => $intOkSum, 'job_err_sum' => $intErrSum, 'last_push_saved' => $blnLastPushSaved, 'skipped' => $intSkipped, 'blocked_count' => $intSkipped, 'errors' => array_slice($tabErrors, 0, 20), 'total' => $intTotal, 'offset' => $intOffset, 'next_offset' => $intNextOffset, 'chunk_done' => $intChunkCount, 'chunk_size' => $intLimit, 'done' => $blnDone, ); } function fxChronotrackApiSyncOrphanReasonLabel($strReason) { switch ($strReason) { case 'sans_external_id': return 'Sans external ID'; case 'doublon': return 'Doublon CT'; case 'hors_eligible': default: return 'Hors éligibles MS1'; } } function fxChronotrackApiSyncComputeCtOrphanRows(array $tabEntities, array $tabEligibleKeys) { $tabSeenLinkedExt = array(); $tabOrphans = array(); foreach ($tabEntities as $arrEntity) { if (!is_array($arrEntity)) { continue; } $strEntryId = fxChronotrackApiEntityId($arrEntity); if ($strEntryId === '') { continue; } $strExt = fxChronotrackApiEntityExternalId($arrEntity); $strReason = ''; if ($strExt === '') { $strReason = 'sans_external_id'; } elseif (!isset($tabEligibleKeys[$strExt])) { $strReason = 'hors_eligible'; } elseif (isset($tabSeenLinkedExt[$strExt])) { $strReason = 'doublon'; } else { $tabSeenLinkedExt[$strExt] = true; continue; } $tabOrphans[] = array( 'entry_id' => $strEntryId, 'external_id' => $strExt, 'name' => fxChronotrackApiEntityEntryLabel($arrEntity), 'bib' => fxChronotrackApiRaceField($arrEntity, array('bib', 'bib_number', 'entry_bib')), 'entry_status' => fxChronotrackApiRaceField($arrEntity, array('entry_status', 'status')), 'race_id' => fxChronotrackApiRaceField($arrEntity, array('race_id', 'event_race_id')), 'reason' => $strReason, 'reason_label' => fxChronotrackApiSyncOrphanReasonLabel($strReason), ); } usort($tabOrphans, function ($a, $b) { $strA = strtolower(($a['name'] ?? '') . ($a['external_id'] ?? '') . ($a['entry_id'] ?? '')); $strB = strtolower(($b['name'] ?? '') . ($b['external_id'] ?? '') . ($b['entry_id'] ?? '')); return strcmp($strA, $strB); }); return $tabOrphans; } /** * Entries CT présentes sur l'événement mais absentes du jeu éligible MS1 (lié, doublon, sans ext). */ function fxChronotrackApiSyncListCtOrphans($intEveId) { $arrPreview = fxChronotrackApiSyncBuildPreview($intEveId); if ($arrPreview['state'] !== 'ok') { return $arrPreview; } $intCtEventId = intval($arrPreview['ct_event_id']); $arrRaceMap = fxChronotrackApiRaceMapGetForEvent($intEveId); $tabParticipants = fxChronotrackApiSyncLoadMs1Participants($intEveId); $arrClass = fxChronotrackApiSyncClassifyParticipants($tabParticipants, $arrRaceMap, $intCtEventId); $tabEligibleKeys = array(); foreach ($arrClass['transferable'] as $arrItem) { $tabEligibleKeys[$arrItem['payload']['external_id']] = true; } $arrFetch = fxChronotrackApiSyncFetchAllCtEntryEntities($intCtEventId); if ($arrFetch['state'] !== 'ok') { return array( 'state' => 'error', 'message' => $arrFetch['message'] ?? 'Erreur lecture entries CT', ); } $tabOrphans = fxChronotrackApiSyncComputeCtOrphanRows($arrFetch['entities'], $tabEligibleKeys); return array( 'state' => 'ok', 'ct_event_id' => $intCtEventId, 'ct_total' => intval($arrPreview['ct_total'] ?? 0), 'linked_count' => intval($arrPreview['already_in_ct'] ?? 0), 'count' => count($tabOrphans), '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. * 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()) { return array( 'state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage(), 'ran' => 0, 'skipped' => 0, 'enabled_configs' => 0, ); } $tabConfigs = fxChronotrackApiConfigListAutoSyncEnabled(); $intRan = 0; $intSkipped = 0; $intExpired = 0; $tabDetails = array(); $intNow = time(); $intEnabled = count($tabConfigs); 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++; 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; } $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++; // 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; } } $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), ); } $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' => $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, ), ); }