Files
ms1inscription-v5/php/chronotrack_api/fx_chronotrack_admin.php

1598 lines
74 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* MSIN API ChronoTrack — rendu admin superadm (Phase 1 : lien + mapping).
*/
function fxChronotrackApiAdminEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
function fxChronotrackApiAdminRenderWaitHtml($strMessage, $vDomaine) {
$strSrc = fxChronotrackApiAdminEsc($vDomaine . '/images/ms1-runner-loader.gif?v=' . _VERSION_CODE);
$strMsg = fxChronotrackApiAdminEsc($strMessage);
return '<div class="ms1-loader ms1-loader--inline" role="status" aria-live="polite" aria-busy="true">'
. '<img class="ms1-loader__runner" src="' . $strSrc . '" alt="" role="presentation" decoding="async">'
. '<span class="ms1-loader__label">' . $strMsg . '</span>'
. '</div>';
}
function fxChronotrackApiAdminRenderLoaderAssets($vDomaine) {
echo '<link rel="stylesheet" href="' . fxChronotrackApiAdminEsc($vDomaine) . '/css/ms1-loader.css?v=' . _VERSION_CODE . '">';
echo '<script src="' . fxChronotrackApiAdminEsc($vDomaine) . '/js/ms1-loader.js?v=' . _VERSION_CODE . '"></script>';
}
function fxChronotrackApiAdminRenderEventLinkPicker($blnClosed) {
echo '<div class="card mb-4" id="ct_api_event_link_card"><div class="card-header">1. Lier un événement ChronoTrack</div><div class="card-body">';
if ($blnClosed) {
echo '<p class="text-muted mb-0">Lecture seule.</p>';
} else {
echo '<div class="form-row align-items-end mb-3">';
echo '<div class="col-md-5 mb-2"><label for="ct_api_search">Recherche</label>';
echo '<input type="text" class="form-control form-control-sm" id="ct_api_search" placeholder="Nom ou ID ChronoTrack"></div>';
echo '<div class="col-md-4 mb-2"><div class="form-check mt-4">';
echo '<input type="checkbox" class="form-check-input" id="ct_api_show_past" value="1">';
echo '<label class="form-check-label" for="ct_api_show_past">Afficher les événements passés</label></div></div>';
echo '<div class="col-md-3 mb-2"><button type="button" class="btn btn-primary btn-sm btn-block" id="ct_api_btn_load">Charger la liste</button></div>';
echo '</div>';
echo '<div id="ct_api_events_wrap" class="border rounded p-2" style="max-height:320px;overflow:auto;">';
echo '<p class="text-muted small mb-0">Cliquez sur « Charger la liste » pour voir les événements à venir (tri par date).</p>';
echo '</div>';
echo '<button type="button" class="btn btn-success btn-sm mt-3" id="ct_api_btn_save_link" disabled>Enregistrer le lien</button>';
}
echo '</div></div>';
}
function fxChronotrackApiAdminRenderEventPage($intEveId, $strLangue = 'fr', $blnInlineOnEventForm = false, $blnInsideV2Panel = false) {
global $vDomaine;
$arrEvent = fxChronotrackApiMs1EventRow($intEveId, $strLangue);
if ($arrEvent === null) {
echo '<div class="alert alert-danger">Événement MS1 introuvable.</div>';
return;
}
$arrConfig = fxChronotrackApiConfigGet($intEveId);
$blnConfigured = fxChronotrackApiSettingsConfigured();
$strStatus = $arrConfig['sync_status'] ?? '';
$blnClosed = ($strStatus === MSIN_API_CHRONOTRACK_SYNC_FERME);
$strWrapperId = '';
echo '<div class="ct-api-admin mt-2 mb-3"' . $strWrapperId . '>';
if (!$blnInsideV2Panel) {
if ($blnInlineOnEventForm) {
echo '<h2 class="h5">MSIN API ChronoTrack</h2>';
} else {
echo '<h1 class="h3">MSIN API ChronoTrack</h1>';
}
}
echo '<p class="text-muted">Configuration du lien pour l\'événement MS1 <strong>' . fxChronotrackApiAdminEsc($arrEvent['eve_nom']) . '</strong> (eve_id '
. intval($arrEvent['eve_id']) . ').</p>';
if ($blnConfigured) {
$strApiBase = fxChronotrackApiGetBaseUrl();
$blnIsTestApi = (strpos($strApiBase, 'api-test') !== false);
echo '<p class="small text-muted mb-2">Endpoint API : <code>' . fxChronotrackApiAdminEsc($strApiBase) . '</code>';
if ($blnIsTestApi) {
echo ' <span class="badge badge-warning">QA</span>';
} else {
echo ' <span class="badge badge-secondary">prod</span>';
}
echo '</p>';
}
if (!$blnConfigured) {
echo '<div class="alert alert-warning">' . fxChronotrackApiAdminEsc(fxChronotrackApiSettingsErrorMessage()) . '</div>';
}
if ($arrConfig !== null) {
echo '<div class="alert alert-info">';
echo 'Statut : <strong>' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($strStatus)) . '</strong>';
echo ' — ChronoTrack : <strong>' . fxChronotrackApiAdminEsc($arrConfig['ct_event_name']) . '</strong>';
echo ' (ID ' . intval($arrConfig['ct_event_id']) . ')';
echo '</div>';
}
if ($blnClosed) {
echo '<div class="alert alert-secondary">Statut <strong>fermé</strong> : plus de sync auto ni modification du lien/mapping. ';
echo 'Le <strong>push manuel</strong> reste disponible ci-dessous.</div>';
}
$blnEventLinked = ($arrConfig !== null && intval($arrConfig['ct_event_id'] ?? 0) > 0);
if (!$blnEventLinked) {
fxChronotrackApiAdminRenderEventLinkPicker($blnClosed);
}
// Sync dabord (usage courant) ; config repliable ensuite. MSIN-4328
if ($arrConfig !== null && $blnEventLinked && $blnConfigured) {
fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed, $arrConfig);
}
if ($arrConfig !== null && $blnEventLinked) {
$blnHasSavedMapping = false;
$arrMapRows = fxChronotrackApiRaceMapGetForEvent($intEveId);
foreach ($arrMapRows as $arrMapRow) {
if (intval($arrMapRow['ct_race_id'] ?? 0) > 0) {
$blnHasSavedMapping = true;
break;
}
}
$strConfigExpanded = $blnHasSavedMapping ? '' : ' show';
echo '<div class="card mb-4" id="ct_api_config_card">';
echo '<div class="card-header py-2">';
echo '<a class="d-block text-dark' . ($blnHasSavedMapping ? ' collapsed' : '') . '" data-toggle="collapse" '
. 'href="#ct_api_config_collapse" role="button" aria-expanded="'
. ($blnHasSavedMapping ? 'false' : 'true') . '">';
echo 'Configuration <span class="text-muted small">(cycle de vie, mapping épreuves)</span>';
echo '</a></div>';
echo '<div class="collapse' . $strConfigExpanded . '" id="ct_api_config_collapse"><div class="card-body pt-3">';
fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked);
fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked);
echo '</div></div></div>';
} elseif ($arrConfig !== null) {
fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked);
fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked);
}
if ($blnEventLinked && $blnConfigured) {
fxChronotrackApiAdminRenderDiagnosticPanel(intval($arrConfig['ct_event_id']));
}
if (!$blnInlineOnEventForm) {
echo '<p class="mt-3"><a class="btn btn-outline-secondary btn-sm" href="index.php?t='
. urlencode(base64_encode(4)) . '&amp;a=mod&amp;id=' . intval($intEveId) . '">&larr; Retour fiche événement</a></p>';
}
echo '</div>';
fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed, $vDomaine, $blnInlineOnEventForm);
}
function fxChronotrackApiAdminRenderStatusActions($intEveId, $strStatus, $blnEventLinked = true) {
echo '<div class="card mb-3 border"><div class="card-header py-2">Cycle de vie sync</div><div class="card-body py-3">';
echo '<p class="small text-muted mb-2">Activer avant la course ; <strong>Terminer</strong> après pour bloquer la sync auto '
. 'et les changements de config. (La sync auto différentielle viendra ensuite.)</p>';
echo '<div class="btn-group" role="group">';
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_LIE) {
echo '<button type="button" class="btn btn-warning btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_ACTIF . '">Activer la sync</button>';
}
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_ACTIF) {
echo '<span class="badge badge-success align-self-center mr-2">Sync active</span>';
echo '<button type="button" class="btn btn-danger btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_FERME . '">Terminer / Déconnecter</button>';
}
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_FERME) {
echo '<span class="badge badge-secondary align-self-center mr-2">Fermé</span>';
echo '<button type="button" class="btn btn-outline-primary btn-sm ct-api-set-status" data-status="' . MSIN_API_CHRONOTRACK_SYNC_LIE . '">Rouvrir (Lié)</button>';
}
echo '</div></div></div>';
}
function fxChronotrackApiAdminRenderRaceMapping($intEveId, $arrConfig, $strLangue, $blnClosed, $blnEventLinked = true) {
$arrEpreuves = fxChronotrackApiMs1EpreuvesForEvent($intEveId, $strLangue);
$arrMap = fxChronotrackApiRaceMapGetForEvent($intEveId);
$blnHasSavedMapping = false;
if ($arrEpreuves !== null) {
for ($i = 1; $i <= count($arrEpreuves); $i++) {
$intEprId = intval($arrEpreuves[$i]['epr_id']);
if (intval($arrMap[$intEprId]['ct_race_id'] ?? 0) > 0) {
$blnHasSavedMapping = true;
break;
}
}
}
echo '<div class="card mb-0 border"><div class="card-header py-2">Mapping épreuves MS1 → races ChronoTrack</div><div class="card-body py-3">';
if ($arrEpreuves === null || count($arrEpreuves) === 0) {
echo '<p class="text-muted mb-0">Aucune épreuve MS1 pour cet événement.</p></div></div>';
return;
}
if ($blnHasSavedMapping) {
echo '<div id="ct_api_race_summary_wrap">';
echo '<table class="table table-sm table-bordered mb-2"><thead><tr><th>Épreuve MS1</th><th>Race ChronoTrack</th></tr></thead><tbody>';
for ($i = 1; $i <= count($arrEpreuves); $i++) {
$intEprId = intval($arrEpreuves[$i]['epr_id']);
$strLabel = trim($arrEpreuves[$i]['epr_type'] . ' — ' . $arrEpreuves[$i]['epr_nom']);
$intCtRaceId = intval($arrMap[$intEprId]['ct_race_id'] ?? 0);
$strCtRaceName = trim((string)($arrMap[$intEprId]['ct_race_name'] ?? ''));
echo '<tr><td>' . fxChronotrackApiAdminEsc($strLabel) . ' <span class="text-muted">(epr_id '
. $intEprId . ')</span></td><td>';
if ($intCtRaceId > 0) {
echo fxChronotrackApiAdminEsc($strCtRaceName !== '' ? $strCtRaceName : ('Race ID ' . $intCtRaceId));
echo ' <span class="text-muted">(ID ' . $intCtRaceId . ')</span>';
} else {
echo '<span class="text-muted">— Non mappé —</span>';
}
echo '</td></tr>';
}
echo '</tbody></table>';
if (!$blnClosed) {
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_edit_map">'
. '<i class="fa fa-pencil" aria-hidden="true"></i>&nbsp;Modifier le mapping</button>';
}
echo '</div>';
}
$strEditStyle = $blnHasSavedMapping ? ' style="display:none;"' : '';
echo '<div id="ct_api_race_edit_wrap"' . $strEditStyle . '>';
echo '<p class="small text-muted">ChronoTrack event ID : ' . intval($arrConfig['ct_event_id'])
. ' — courses CT Live (<code>race_name</code> via <code>GET event/{id}/race</code>).</p>';
echo '<div id="ct_api_races_status" class="small mb-2"></div>';
if (!$blnClosed) {
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_reload_races">'
. '<i class="fa fa-refresh" aria-hidden="true"></i>&nbsp;Charger les courses ChronoTrack</button>';
}
echo '<table class="table table-sm table-bordered"><thead><tr><th>Épreuve MS1</th><th>Race ChronoTrack</th></tr></thead><tbody>';
for ($i = 1; $i <= count($arrEpreuves); $i++) {
$intEprId = intval($arrEpreuves[$i]['epr_id']);
$strLabel = trim($arrEpreuves[$i]['epr_type'] . ' — ' . $arrEpreuves[$i]['epr_nom']);
echo '<tr><td>' . fxChronotrackApiAdminEsc($strLabel) . ' <span class="text-muted">(epr_id '
. $intEprId . ')</span></td><td>';
echo '<select class="form-control form-control-sm ct-api-race-select" data-epr-id="' . $intEprId . '"'
. ($blnClosed ? ' disabled' : '') . '>';
echo '<option value="">— Non mappé —</option>';
echo '</select></td></tr>';
}
echo '</tbody></table>';
if (!$blnClosed) {
echo '<button type="button" class="btn btn-primary btn-sm" id="ct_api_btn_save_map">Enregistrer le mapping</button>';
if ($blnHasSavedMapping) {
echo ' <button type="button" class="btn btn-link btn-sm" id="ct_api_btn_cancel_edit_map">Annuler</button>';
}
}
echo '</div></div></div>';
}
function fxChronotrackApiAdminRenderPushPanel($intEveId, $blnClosed = false, $arrConfig = null) {
echo '<div class="card mb-4 border-primary" id="ct_api_push_card">';
echo '<div class="card-header">Synchronisation ChronoTrack</div>';
echo '<div class="card-body">';
if ($blnClosed) {
echo '<div class="alert alert-warning py-2 small mb-2">Événement fermé — envoi manuel encore possible.</div>';
}
// MSIN-4328 — Phase A : config sync auto (exécutée plus tard par cron)
$blnAutoOn = (intval($arrConfig['auto_sync_enabled'] ?? 0) === 1);
$intAutoInterval = intval($arrConfig['auto_sync_interval_min'] ?? 15);
if (!in_array($intAutoInterval, array(1, 5, 15, 30, 60), true)) {
$intAutoInterval = 15;
}
$strUntilRaw = trim((string)($arrConfig['auto_sync_until'] ?? ''));
$strUntilLocal = '';
if ($strUntilRaw !== '' && $strUntilRaw !== '0000-00-00 00:00:00') {
$intUntilTs = strtotime($strUntilRaw);
if ($intUntilTs !== false) {
$strUntilLocal = date('Y-m-d\TH:i', $intUntilTs);
}
}
echo '<div class="ct-api-auto-wrap mb-3' . ($blnClosed ? ' ct-api-auto-wrap--disabled' : '') . '" id="ct_api_auto_wrap">';
echo '<div class="form-check mb-2">';
echo '<input class="form-check-input" type="checkbox" id="ct_api_auto_enabled" value="1"'
. ($blnAutoOn ? ' checked' : '') . ($blnClosed ? ' disabled' : '') . '>';
echo '<label class="form-check-label font-weight-bold" for="ct_api_auto_enabled">Mise à jour automatique</label>';
echo '</div>';
echo '<div class="form-row align-items-end" id="ct_api_auto_options">';
echo '<div class="form-group col-sm-4 mb-2 mb-sm-0">';
echo '<label class="small text-muted mb-1" for="ct_api_auto_interval">Fréquence</label>';
echo '<select class="form-control form-control-sm" id="ct_api_auto_interval"'
. ($blnClosed ? ' disabled' : '') . '>';
foreach (array(1 => 'Toutes les minutes', 5 => 'Toutes les 5 minutes', 15 => 'Toutes les 15 minutes',
30 => 'Toutes les 30 minutes', 60 => 'Toutes les heures') as $intMin => $strLabel) {
echo '<option value="' . $intMin . '"' . ($intAutoInterval === $intMin ? ' selected' : '') . '>'
. fxChronotrackApiAdminEsc($strLabel) . '</option>';
}
echo '</select></div>';
echo '<div class="form-group col-sm-5 mb-2 mb-sm-0">';
echo '<label class="small text-muted mb-1" for="ct_api_auto_until">Date et heure de fin</label>';
echo '<input type="datetime-local" class="form-control form-control-sm" id="ct_api_auto_until" value="'
. fxChronotrackApiAdminEsc($strUntilLocal) . '"' . ($blnClosed ? ' disabled' : '') . '>';
echo '</div>';
if (!$blnClosed) {
echo '<div class="form-group col-sm-3 mb-0">';
echo '<button type="button" class="btn btn-outline-primary btn-sm btn-block" id="ct_api_btn_auto_save">'
. 'Enregistrer</button>';
echo '</div>';
}
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> (comme liste dattente). Arrêt : '
. fxChronotrackApiAdminEsc($strUntilRaw) . '.';
} else {
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
echo '<div id="ct_api_push_status" class="ct-api-push-status mb-3">';
echo '<div class="text-muted small">Cliquez <strong>Actualiser</strong> pour vérifier sil y a des changements à envoyer.</div>';
echo '</div>';
echo '<div class="ct-api-push-actions mb-2">';
echo '<button type="button" class="btn btn-outline-secondary" id="ct_api_btn_push_refresh">';
echo '<i class="fa fa-refresh" aria-hidden="true"></i>&nbsp;Actualiser</button> ';
echo '<button type="button" class="btn btn-primary btn-lg" id="ct_api_btn_push" disabled>';
echo '<i class="fa fa-cloud-upload" aria-hidden="true"></i>&nbsp;Envoyer vers ChronoTrack</button> ';
echo '<button type="button" class="btn btn-outline-dark btn-sm" id="ct_api_btn_logs">';
echo '<i class="fa fa-list" aria-hidden="true"></i>&nbsp;Voir les logs</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_auto_diag">'
. 'É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-2">Envoi manuel : nouveaux / modifiés depuis le dernier push. '
. 'Page : refresh statut MS1 chaque minute.</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>';
// Détails / debug : sous les boutons, repliés par défaut
echo '<div class="card border-light mb-0" id="ct_api_push_details_card">';
echo '<div class="card-header py-2 bg-white">';
echo '<a class="d-block text-muted small collapsed" data-toggle="collapse" href="#ct_api_push_details_collapse" '
. 'role="button" aria-expanded="false">';
echo 'Détails &amp; exclus <span class="text-muted">(compteurs, liste à corriger)</span>';
echo '</a></div>';
echo '<div class="collapse" id="ct_api_push_details_collapse"><div class="card-body py-2">';
echo '<div id="ct_api_push_summary" class="mb-2 text-muted small"></div>';
echo '<div id="ct_api_push_blocked_wrap" class="mb-2 d-none"></div>';
echo '<button type="button" class="btn btn-outline-info btn-sm" id="ct_api_btn_ct_count">';
echo '<i class="fa fa-calculator" aria-hidden="true"></i>&nbsp;Nb ChronoTrack Live</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm" id="ct_api_btn_mark_current" title="Si le différentiel est faux (ex. après comptage Live)">';
echo 'Remettre à jour (sans envoyer)</button>';
echo '<p class="small text-muted mt-2 mb-0">Info technique : external ID = <code>par_id_original</code>. '
. 'Le comptage Live sert aussi à lier les entry_id pour le bouton fiche.</p>';
echo '</div></div></div>';
echo '</div></div>';
}
function fxChronotrackApiAdminRenderDiagnosticPanel($intCtEventId) {
echo '<div class="card mb-4 border-secondary"><div class="card-header py-2">';
echo '<a class="d-block text-dark collapsed" data-toggle="collapse" href="#ct_api_diagnostic_collapse" role="button" aria-expanded="false">';
echo 'Diagnostic API ChronoTrack <span class="text-muted small">(support — event ID ' . intval($intCtEventId) . ')</span>';
echo '</a></div>';
echo '<div class="collapse" id="ct_api_diagnostic_collapse"><div class="card-body">';
echo '<p class="small text-muted mb-2">Outils de support (credentials MS1 / OAuth en cache). '
. 'Pas nécessaire pour une course en production.</p>';
echo '<button type="button" class="btn btn-outline-dark btn-sm mb-2" id="ct_api_btn_diagnostic">';
echo '<i class="fa fa-stethoscope" aria-hidden="true"></i>&nbsp;Lancer le diagnostic API</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2" id="ct_api_btn_probe_push">';
echo '<i class="fa fa-flask" aria-hidden="true"></i>&nbsp;Sonde (1 participant POST)</button> ';
echo '<button type="button" class="btn btn-outline-secondary btn-sm mb-2 d-none" id="ct_api_btn_diagnostic_copy">';
echo 'Copier le rapport</button>';
echo '<div id="ct_api_diagnostic_out" class="small text-muted">Cliquez sur « Lancer le diagnostic API » ou « Sonde ».</div>';
echo '</div></div></div>';
}
function fxChronotrackApiAdminRenderPageScript($intEveId, $arrConfig, $blnClosed, $vDomaine, $blnInlineOnEventForm = false) {
$intCtEventId = ($arrConfig !== null) ? intval($arrConfig['ct_event_id']) : 0;
$arrMapCurrent = array();
$blnHasSavedMapping = false;
if ($arrConfig !== null) {
$arrMapRows = fxChronotrackApiRaceMapGetForEvent($intEveId);
foreach ($arrMapRows as $intEprId => $arrRow) {
$intCtRaceId = intval($arrRow['ct_race_id']);
$arrMapCurrent[intval($intEprId)] = $intCtRaceId;
if ($intCtRaceId > 0) {
$blnHasSavedMapping = true;
}
}
}
?>
<style>
#ct_api_push_blocked_wrap .ct-api-blocked-table { font-size: 0.8rem; }
#ct_api_push_blocked_wrap .ct-api-blocked-table th { white-space: nowrap; }
#ct_api_push_blocked_wrap .ct-api-blocked-scroll { max-height: 55vh; overflow: auto; }
#ct_api_push_summary .ct-api-dash-table { font-size: 0.85rem; margin-bottom: 0.5rem; }
#ct_api_push_summary .ct-api-dash-table th { background: #f8f9fa; font-weight: 600; white-space: nowrap; }
#ct_api_push_summary .ct-api-dash-table td.num { text-align: right; font-variant-numeric: tabular-nums; font-weight: 600; }
#ct_api_push_summary .ct-api-dash-push { font-size: 0.85rem; margin-bottom: 0; }
#ct_api_config_collapse .card { box-shadow: none; }
.ct-api-push-status {
padding: 12px 14px;
border-radius: 8px;
background: #f8f9fa;
border: 1px solid #e9ecef;
}
.ct-api-push-status--ok {
background: #e8f7ee;
border-color: #b7e4c7;
}
.ct-api-push-status--todo {
background: #fff8e6;
border-color: #ffe1a6;
}
.ct-api-push-status__title {
font-size: 1.15rem;
font-weight: 700;
margin: 0 0 4px;
line-height: 1.3;
}
.ct-api-push-status__hint {
margin: 0;
font-size: 0.9rem;
}
.ct-api-push-actions .btn-lg { font-size: 1.05rem; padding: 0.55rem 1.1rem; }
#ct_api_push_details_card .card-header a { text-decoration: none; }
.ct-api-auto-wrap {
padding: 12px 14px;
border-radius: 8px;
border: 1px dashed #adb5bd;
background: #fafbfc;
}
.ct-api-auto-wrap--disabled { opacity: 0.7; }
#ct_api_logs_wrap .ct-api-logs-scroll { max-height: 280px; overflow: auto; }
</style>
<?php fxChronotrackApiAdminRenderLoaderAssets($vDomaine); ?>
<script>
(function ($) {
var intEveId = <?php echo intval($intEveId); ?>;
var intLinkedCtEventId = <?php echo $intCtEventId; ?>;
var tabMapCurrent = <?php echo json_encode($arrMapCurrent); ?>;
var strAjaxUrl = <?php echo json_encode($vDomaine . '/superadm/ajax_chronotrack_api.php'); ?>;
var strSelectedCtEventId = intLinkedCtEventId > 0 ? String(intLinkedCtEventId) : '';
var blnHasSavedMapping = <?php echo $blnHasSavedMapping ? 'true' : 'false'; ?>;
var blnClosed = <?php echo $blnClosed ? 'true' : 'false'; ?>;
var blnInlineOnEventForm = <?php echo $blnInlineOnEventForm ? 'true' : 'false'; ?>;
var xhrPushPreview = null;
var blnPushPreviewLoaded = false;
var intCtLiveCount = null;
var blnPushRunning = false;
var intStatusRefreshTimer = null;
function escHtml(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
if (window.Ms1Loader) {
Ms1Loader.init({
src: <?php echo json_encode($vDomaine . '/images/ms1-runner-loader.gif?v=' . _VERSION_CODE); ?>
});
}
function waitHtml(strMessage) {
if (window.Ms1Loader) {
return Ms1Loader.inlineHtml(strMessage);
}
return '<span class="text-muted">' + escHtml(strMessage || 'Chargement…') + '</span>';
}
function showMsg(strType, strText) {
window.alert(strText);
}
function rejectIfSessionLost(res) {
if (!res || typeof res !== 'object') {
return false;
}
if (res.code === 'session' || res.message === 'session') {
if (typeof window.fxSuperadmSessionExpiredRedirect === 'function') {
window.fxSuperadmSessionExpiredRedirect();
} else {
window.location.href = <?php echo json_encode($vDomaine . '/superadm/login.php?expired=1'); ?>;
}
return true;
}
var strMsg = String(res.message || '');
if (strMsg.indexOf('Session expir') === 0) {
if (typeof window.fxSuperadmSessionExpiredRedirect === 'function') {
window.fxSuperadmSessionExpiredRedirect();
} else {
window.location.href = <?php echo json_encode($vDomaine . '/superadm/login.php?expired=1'); ?>;
}
return true;
}
return false;
}
function renderEvents(arrEvents) {
var $wrap = $('#ct_api_events_wrap');
$wrap.empty();
if (!arrEvents || !arrEvents.length) {
$wrap.append('<p class="text-muted small mb-0">Aucun événement trouvé (essayez « événements passés » ou autre recherche).</p>');
return;
}
var $list = $('<div class="list-group list-group-flush"></div>');
$.each(arrEvents, function (_, ev) {
var strId = String(ev.ct_event_id || '');
var $item = $('<label class="list-group-item list-group-item-action py-2 mb-0"></label>');
var $radio = $('<input type="radio" name="ct_api_event_pick" class="mr-2">').val(strId);
if (strSelectedCtEventId === strId) {
$radio.prop('checked', true);
}
$item.append($radio);
$item.append(document.createTextNode((ev.start_label ? ev.start_label + ' — ' : '') + (ev.name || '') + ' (ID ' + strId + ')'));
if (ev.is_past) {
$item.append(' <span class="badge badge-secondary ml-1">passé</span>');
}
$list.append($item);
});
$wrap.append($list);
$('#ct_api_btn_save_link').prop('disabled', false);
}
$('#ct_api_btn_load').on('click', function () {
var $wrap = $('#ct_api_events_wrap');
$wrap.html(waitHtml('Chargement de la liste ChronoTrack…'));
$.post(strAjaxUrl, {
a: 'list_events',
eve_id: intEveId,
search: $('#ct_api_search').val(),
show_past: $('#ct_api_show_past').is(':checked') ? 1 : 0
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur chargement');
return;
}
renderEvents(res.events || []);
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
});
});
$(document).on('change', 'input[name="ct_api_event_pick"]', function () {
strSelectedCtEventId = $(this).val();
});
$('#ct_api_btn_save_link').on('click', function () {
if (!strSelectedCtEventId) {
showMsg('error', 'Sélectionnez un événement ChronoTrack');
return;
}
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, {
a: 'save_link',
eve_id: intEveId,
ct_event_id: strSelectedCtEventId
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur enregistrement');
$btn.prop('disabled', false);
return;
}
window.location.reload();
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
$btn.prop('disabled', false);
});
});
$('.ct-api-set-status').on('click', function () {
var strStatus = $(this).data('status');
var strConfirm;
if (strStatus === 'ferme') {
strConfirm = 'Terminer la course ? La sync auto et les changements de config seront bloqués (push manuel toujours possible).';
} else if (strStatus === 'lie') {
strConfirm = 'Rouvrir cet événement ChronoTrack (statut Lié) ?';
} else {
strConfirm = 'Activer la synchronisation automatique pour cet événement ?';
}
if (!window.confirm(strConfirm)) {
return;
}
$.post(strAjaxUrl, { a: 'set_status', eve_id: intEveId, sync_status: strStatus }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur statut');
return;
}
window.location.reload();
}, 'json');
});
function loadRacesIntoSelects() {
if (intLinkedCtEventId <= 0) {
return;
}
var $status = $('#ct_api_races_status');
$status.removeClass('text-danger text-warning text-success').html(waitHtml('Chargement des courses ChronoTrack…'));
$.post(strAjaxUrl, { a: 'list_races', eve_id: intEveId, ct_event_id: intLinkedCtEventId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
$status.addClass('text-danger').text((res && res.message) ? res.message : 'Erreur chargement des courses');
return;
}
var arrRaces = res.races || [];
var strKind = res.kind || 'reg_choice';
if (!arrRaces.length) {
$status.addClass('text-warning').text((res.message) ? res.message : 'Aucune course ChronoTrack pour cet event.');
} else {
$status.addClass('text-success').text(arrRaces.length + ' course(s) ChronoTrack chargée(s).');
}
$('.ct-api-race-select').each(function () {
var $sel = $(this);
var intEprId = parseInt($sel.data('epr-id'), 10);
var intCurrent = tabMapCurrent[intEprId] || 0;
$sel.find('option:not(:first)').remove();
$.each(arrRaces, function (_, race) {
var strText = race.label || race.name || 'Course';
strText += ' (ID ' + race.ct_race_id + ')';
var $opt = $('<option></option>').val(race.ct_race_id).text(strText);
$sel.append($opt);
});
if (intCurrent > 0) {
$sel.val(String(intCurrent));
}
});
}, 'json').fail(function () {
$status.addClass('text-danger').text('Erreur réseau lors du chargement des races.');
});
}
function showRaceEditMode(blnLoadRaces) {
$('#ct_api_race_summary_wrap').hide();
$('#ct_api_race_edit_wrap').show();
if (blnLoadRaces) {
loadRacesIntoSelects();
}
}
function showRaceSummaryMode() {
$('#ct_api_race_edit_wrap').hide();
$('#ct_api_races_status').empty();
$('#ct_api_race_summary_wrap').show();
}
$('#ct_api_btn_edit_map').on('click', function () {
showRaceEditMode(true);
});
$('#ct_api_btn_cancel_edit_map').on('click', function () {
showRaceSummaryMode();
});
$('#ct_api_btn_reload_races').on('click', function () {
loadRacesIntoSelects();
});
$('#chronotrackApiCollapse').on('shown.bs.collapse', function () {
if (!blnHasSavedMapping && intLinkedCtEventId > 0 && !blnClosed) {
loadRacesIntoSelects();
}
});
<?php if ($arrConfig !== null && !$blnHasSavedMapping) { ?>
if (intLinkedCtEventId > 0 && !blnClosed && !$('#chronotrackApiCollapse').length) {
loadRacesIntoSelects();
}
<?php } ?>
$('#ct_api_btn_save_map').on('click', function () {
var tabMap = {};
$('.ct-api-race-select').each(function () {
var intEprId = parseInt($(this).data('epr-id'), 10);
var strRaceId = $(this).val();
tabMap[intEprId] = { ct_race_id: strRaceId ? parseInt(strRaceId, 10) : 0, ct_race_name: $(this).find('option:selected').text() };
});
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, { a: 'save_race_map', eve_id: intEveId, map: tabMap }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
showMsg('error', (res && res.message) ? res.message : 'Erreur mapping');
$btn.prop('disabled', false);
return;
}
window.location.reload();
}, 'json').fail(function () {
showMsg('error', 'Erreur réseau');
$btn.prop('disabled', false);
});
});
var objDiagnosticReport = null;
function renderDiagnosticReport(res) {
objDiagnosticReport = res;
var $out = $('#ct_api_diagnostic_out');
if (!res || res.state !== 'ok') {
$out.html('<div class="text-danger">' + escHtml((res && res.message) ? res.message : 'Erreur diagnostic') + '</div>');
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
return;
}
var html = '<p class="mb-2"><strong>OAuth :</strong> ' + escHtml(res.oauth || '?')
+ ' — <strong>Event CT :</strong> ' + escHtml(String(res.ct_event_id || '')) + '</p>';
html += '<div class="table-responsive"><table class="table table-sm table-bordered mb-2">';
html += '<thead><tr><th>Endpoint</th><th>HTTP</th><th>Résultat</th><th>Entités détectées</th></tr></thead><tbody>';
$.each(res.probes || [], function (_, probe) {
html += '<tr>';
html += '<td><code>' + escHtml(probe.path || '') + '</code></td>';
html += '<td>' + escHtml(String(probe.http_code || '')) + '</td>';
html += '<td>' + escHtml(probe.state === 'ok' ? 'OK' : (probe.message || 'erreur')) + '</td>';
html += '<td>';
if (probe.summary) {
if (probe.summary.event_race && probe.summary.event_race.count > 0) {
html += 'event_race: ' + probe.summary.event_race.count + '<br>';
}
if (probe.summary.entry && probe.summary.entry.count > 0) {
html += 'entry: ' + probe.summary.entry.count + ' (page)<br>';
}
if (probe.summary.reg_choice && probe.summary.reg_choice.count > 0) {
html += 'reg_choice: ' + probe.summary.reg_choice.count + '<br>';
}
if (probe.summary.race && probe.summary.race.count > 0) {
html += 'race: ' + probe.summary.race.count + '<br>';
}
if (probe.summary.field_keys && probe.summary.field_keys.length) {
html += '<span class="text-muted">champs: ' + escHtml(probe.summary.field_keys.slice(0, 12).join(', '));
if (probe.summary.field_keys.length > 12) {
html += '…';
}
html += '</span><br>';
}
var arrSampleSource = probe.summary.entry || probe.summary.event_race || probe.summary.race || probe.summary.reg_choice;
if (arrSampleSource && arrSampleSource.samples && arrSampleSource.samples.length) {
html += '<span class="text-muted">ex. ';
$.each(arrSampleSource.samples, function (i, s) {
if (i > 0) {
html += ' · ';
}
var strLabel = s.name || s.id || '?';
if (s.external_id) {
strLabel += ' [ext:' + s.external_id + ']';
}
if (s.bib) {
strLabel += ' #' + s.bib;
}
if (s.entry_status) {
strLabel += ' (' + s.entry_status + ')';
}
html += escHtml(strLabel);
});
html += '</span>';
}
}
html += '</td></tr>';
});
html += '</tbody></table></div>';
$.each(res.probes || [], function (idx, probe) {
var strProbeLabel = probe.path || ('probe ' + idx);
if (probe.query && typeof probe.query === 'object') {
var arrQ = [];
$.each(probe.query, function (k, v) {
arrQ.push(k + '=' + v);
});
if (arrQ.length) {
strProbeLabel += '?' + arrQ.join('&');
}
}
html += '<details class="mb-2"><summary class="small"><strong>JSON brut — '
+ escHtml(strProbeLabel) + '</strong></summary>';
html += '<pre class="small border rounded p-2 mb-0" style="max-height:280px;overflow:auto;background:#f8f9fa;">'
+ escHtml(probe.raw_json || '') + '</pre></details>';
});
$out.html(html);
$('#ct_api_btn_diagnostic_copy').removeClass('d-none');
}
$('#ct_api_btn_diagnostic').on('click', function () {
var $out = $('#ct_api_diagnostic_out');
$out.html(waitHtml('Diagnostic en cours… (OAuth + 4 endpoints CT, dont entry)'));
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
$.post(strAjaxUrl, {
a: 'diagnostic',
eve_id: intEveId,
ct_event_id: intLinkedCtEventId
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
renderDiagnosticReport(res);
}, 'json').fail(function () {
$out.html('<div class="text-danger">Erreur réseau lors du diagnostic.</div>');
});
});
$('#ct_api_btn_diagnostic_copy').on('click', function () {
if (!objDiagnosticReport) {
return;
}
var strText = JSON.stringify(objDiagnosticReport, null, 2);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(strText).then(function () {
window.alert('Rapport copié dans le presse-papiers.');
});
return;
}
window.prompt('Copier le rapport (Ctrl+C) :', strText);
});
$('#ct_api_btn_probe_push').on('click', function () {
var $btn = $(this);
var $out = $('#ct_api_diagnostic_out');
$btn.prop('disabled', true);
$out.html(waitHtml('Sonde POST (1 participant)…'));
$('#ct_api_btn_diagnostic_copy').addClass('d-none');
$.post(strAjaxUrl, { a: 'sync_probe_push', eve_id: intEveId }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
var strClass = (res && res.state === 'ok') ? 'text-success' : 'text-danger';
var strHtml = '<div class="' + strClass + '"><strong>Sonde</strong> — '
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
if (res && res.payload) {
strHtml += '<pre class="small bg-light p-2 mt-2 mb-1" style="max-height:200px;overflow:auto;">'
+ escHtml(JSON.stringify(res.payload, null, 2)) + '</pre>';
}
if (res && res.response_body) {
strHtml += '<pre class="small bg-light p-2 mb-0" style="max-height:200px;overflow:auto;">'
+ escHtml(res.response_body) + '</pre>';
}
$out.html(strHtml);
}, 'json').fail(function () {
$out.html('<div class="text-danger">Erreur réseau (sonde).</div>');
}).always(function () {
$btn.prop('disabled', false);
});
});
var objPushPreview = null;
function renderBlockedPanel(res) {
var $wrap = $('#ct_api_push_blocked_wrap');
if (!$wrap.length) {
return;
}
var tabGroups = (res && res.blocked_by_type) ? res.blocked_by_type : [];
var intTotal = (res && res.blocked_count) ? res.blocked_count : 0;
if (intTotal <= 0 || !tabGroups.length) {
$wrap.addClass('d-none').empty();
return;
}
var html = '<div class="card border-danger">';
html += '<div class="card-header py-2 bg-white">';
html += '<a class="d-block text-danger collapsed" data-toggle="collapse" href="#ct_api_blocked_detail" role="button" aria-expanded="false">';
html += '<strong>Exclus MS1</strong> <span class="badge badge-danger">' + intTotal + '</span>';
html += '</a></div>';
html += '<div class="collapse" id="ct_api_blocked_detail"><div class="card-body py-2">';
$.each(tabGroups, function (_, grp) {
var intCnt = (grp.items && grp.items.length) ? grp.items.length : 0;
if (intCnt === 0) {
return;
}
var blnTeam = (grp.code === 'team');
var strHeadClass = blnTeam ? 'text-warning' : 'text-danger';
html += '<div class="mb-3">';
html += '<div class="d-flex align-items-center mb-1">';
html += '<strong class="' + strHeadClass + ' small">' + escHtml(grp.label || grp.code) + '</strong>';
html += ' <span class="badge badge-secondary ml-1">' + intCnt + '</span>';
if (blnTeam) {
html += ' <span class="text-muted small ml-1">MVP : capitaine seulement (plus tard)</span>';
}
html += '</div>';
html += '<div class="ct-api-blocked-scroll border rounded">';
html += '<table class="table table-sm table-striped ct-api-blocked-table mb-0">';
html += '<thead class="thead-light"><tr>';
if (blnTeam) {
html += '<th>Nom</th><th>external_id</th><th>Équipe</th><th>No équipe</th><th>Rôle</th><th>Dossard</th><th>pec_id</th>';
} else {
html += '<th>Nom</th><th>external_id</th><th>par_id</th><th>Dossard</th><th>Sexe</th><th>Raison</th>';
}
html += '</tr></thead><tbody>';
$.each(grp.items, function (__, item) {
html += '<tr>';
html += '<td>' + escHtml(item.name || '—') + '</td>';
html += '<td>' + escHtml(item.external_id || '—') + '</td>';
if (blnTeam) {
html += '<td>' + escHtml(item.team_name || '—') + '</td>';
html += '<td>' + escHtml(item.no_equipe ? ('#' + item.no_equipe) : '—') + '</td>';
html += '<td>' + escHtml(item.role_label || '—') + '</td>';
html += '<td>' + escHtml(item.no_bib ? ('#' + item.no_bib) : '—') + '</td>';
html += '<td>' + escHtml(String(item.pec_id || '')) + '</td>';
} else {
html += '<td>' + escHtml(String(item.par_id || '')) + '</td>';
html += '<td>' + escHtml(item.no_bib ? ('#' + item.no_bib) : (item.bib ? ('#' + item.bib) : '—')) + '</td>';
html += '<td>' + escHtml(item.par_sexe || '—') + '</td>';
html += '<td class="text-muted">' + escHtml(item.message || '') + '</td>';
}
html += '</tr>';
});
html += '</tbody></table></div></div>';
});
html += '</div></div></div>';
$wrap.removeClass('d-none').html(html);
}
function renderPushPreview(res) {
var $sum = $('#ct_api_push_summary');
var $status = $('#ct_api_push_status');
var $btn = $('#ct_api_btn_push');
objPushPreview = res;
if (!$sum.length && !$status.length) {
return;
}
if (!res || res.state !== 'ok') {
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
.html('<div class="text-danger">' + escHtml((res && res.message) ? res.message : 'Erreur') + '</div>');
}
if ($sum.length) {
$sum.html('');
}
$('#ct_api_push_blocked_wrap').addClass('d-none').empty();
$btn.prop('disabled', true);
return;
}
var intBlocked = res.blocked_count || 0;
var intEligible = res.transferable || 0;
var intPending = (typeof res.pending_push !== 'undefined')
? res.pending_push
: (res.to_push || 0);
var intMs1 = res.ms1_total || 0;
var blnFirst = !!res.is_first_push;
var strLastPush = res.last_push_at || null;
// Bannière employé (ludique / claire)
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo');
if (intPending > 0) {
$status.addClass('ct-api-push-status--todo');
var strTodoTitle = blnFirst
? (intPending + ' à envoyer (premier envoi)')
: (intPending + ' changement' + (intPending > 1 ? 's' : '') + ' à envoyer');
$status.html(
'<p class="ct-api-push-status__title">' + escHtml(strTodoTitle) + '</p>'
+ '<p class="ct-api-push-status__hint mb-0">Cliquez <strong>Envoyer vers ChronoTrack</strong> — seuls les nouveaux / modifiés partent.</p>'
);
} else {
$status.addClass('ct-api-push-status--ok');
var strOk = 'Tout est à jour';
if (strLastPush) {
strOk += ' <span class="text-muted font-weight-normal" style="font-size:0.85rem;">· dernier envoi '
+ escHtml(strLastPush) + '</span>';
}
$status.html(
'<p class="ct-api-push-status__title text-success">' + strOk + '</p>'
+ '<p class="ct-api-push-status__hint mb-0 text-muted">'
+ intEligible + ' éligibles · rien à envoyer pour le moment.</p>'
);
}
}
// Détails (sous les boutons, zone repliable)
var html = '<table class="table table-sm table-bordered ct-api-dash-table mb-1">';
html += '<thead><tr><th>MS1</th><th class="text-right">Total</th><th class="text-right">Éligibles</th>';
html += '<th class="text-right">Exclus</th><th class="text-right">Différentiel</th></tr></thead><tbody>';
html += '<tr><td>Participants</td>';
html += '<td class="num">' + intMs1 + '</td>';
html += '<td class="num text-primary">' + intEligible + '</td>';
html += '<td class="num text-danger">' + intBlocked + '</td>';
html += '<td class="num ' + (intPending > 0 ? 'text-warning' : 'text-success') + '">' + intPending + '</td></tr>';
html += '</tbody></table>';
if (intCtLiveCount !== null) {
html += '<p class="ct-api-dash-push mb-1 text-muted">ChronoTrack Live : <strong>'
+ intCtLiveCount + '</strong> inscription(s) '
+ '<button type="button" class="btn btn-link btn-sm p-0 align-baseline" id="ct_api_btn_ct_count_inline">recompter</button></p>';
} else {
html += '<p class="ct-api-dash-push mb-1 text-muted">ChronoTrack Live : total non chargé (bouton ci-dessous).</p>';
}
$sum.html(html);
renderBlockedPanel(res);
$btn.prop('disabled', intPending <= 0);
// MSIN-4328 — auto-réparation au load : last_push déjà posé mais 100 % des
// éligibles « à pousser » (= faux différentiel, ex. par_maj bumpé par backfill entry_id).
// Un vrai premier envoi (is_first_push) nest pas touché.
if (!blnFirst && intPending > 0 && intEligible > 0 && intPending === intEligible
&& !window._ctApiAutoMarkDone) {
window._ctApiAutoMarkDone = true;
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
.html(waitHtml('Recalage du différentiel…'));
}
$.post(strAjaxUrl, {
a: 'sync_mark_current',
eve_id: intEveId,
ok_count: intEligible
}, function (markRes) {
if (rejectIfSessionLost(markRes)) {
return;
}
loadPushPreview(true);
}, 'json').fail(function () {
window._ctApiAutoMarkDone = false;
if (intBlocked > 0) {
$('#ct_api_push_details_collapse').collapse('show');
}
});
return;
}
// Ouvre les détails seulement sil y a des exclus à corriger
if (intBlocked > 0) {
$('#ct_api_push_details_collapse').collapse('show');
}
}
function renderPushResult(res, strTitle) {
var $res = $('#ct_api_push_result');
var strHeading = strTitle || 'Résultat';
var strClass = 'text-danger';
if (res && res.state === 'warning') {
strClass = 'text-warning';
} else if (res && (res.state === 'ok' || res.state === 'partial')) {
strClass = res.state === 'ok' ? 'text-success' : 'text-warning';
}
var strHtml = '<div class="' + strClass + '"><strong>' + escHtml(strHeading) + '</strong> — '
+ escHtml((res && res.message) ? res.message : 'Erreur') + '</div>';
if (res && res.errors && res.errors.length) {
strHtml += '<p class="small text-danger mb-1 mt-2">Erreurs API ChronoTrack :</p>';
strHtml += '<ul class="text-danger small mb-0 pl-3">';
$.each(res.errors, function (_, msg) {
strHtml += '<li>' + escHtml(msg) + '</li>';
});
strHtml += '</ul>';
}
$res.html(strHtml);
}
function loadPushPreview(blnForce) {
var $sum = $('#ct_api_push_summary');
var $status = $('#ct_api_push_status');
if (!$sum.length && !$status.length) {
return;
}
if (xhrPushPreview && xhrPushPreview.readyState !== 4) {
if (!blnForce) {
return;
}
xhrPushPreview.abort();
}
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
.html(waitHtml('Vérification des changements…'));
}
xhrPushPreview = $.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 60000,
data: { a: 'sync_preview', eve_id: intEveId }
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
blnPushPreviewLoaded = true;
renderPushPreview(res);
}).fail(function (jqXHR, strStatus) {
if (strStatus === 'abort') {
return;
}
var strMsg = (strStatus === 'timeout')
? 'Délai dépassé — réessayez Actualiser.'
: 'Erreur réseau (preview).';
if ($status.length) {
$status.removeClass('ct-api-push-status--ok ct-api-push-status--todo')
.html('<div class="text-danger">' + escHtml(strMsg) + '</div>');
}
}).always(function () {
xhrPushPreview = null;
});
}
function loadCtLiveCount() {
var $btn = $('#ct_api_btn_ct_count');
var $sum = $('#ct_api_push_summary');
$btn.prop('disabled', true);
if ($sum.length && objPushPreview && objPushPreview.state === 'ok') {
// conserve le tableau MS1, indique le chargement CT
$sum.find('.ct-api-dash-push').first().replaceWith(
'<p class="ct-api-dash-push mb-1 text-muted">' + waitHtml('Comptage ChronoTrack Live…') + '</p>'
);
}
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: { a: 'sync_ct_count', eve_id: intEveId }
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
window.alert((res && res.message) ? res.message : 'Impossible de compter sur ChronoTrack Live');
return;
}
intCtLiveCount = parseInt(res.ct_total, 10);
if (isNaN(intCtLiveCount)) {
intCtLiveCount = null;
}
if (objPushPreview && objPushPreview.state === 'ok') {
renderPushPreview(objPushPreview);
}
}).fail(function (jqXHR, strStatus) {
window.alert(strStatus === 'timeout'
? 'Délai dépassé pour le comptage ChronoTrack Live.'
: 'Erreur réseau (comptage CT).');
}).always(function () {
$btn.prop('disabled', false);
});
}
function maybeLoadPushPreviewOnReveal() {
if (!$('#ct_api_push_card').length) {
return;
}
if (!blnPushPreviewLoaded) {
loadPushPreview(false);
}
}
if ($('#ct_api_push_card').length) {
if (blnInlineOnEventForm && $('#chronotrackApiCollapse').length) {
$('#chronotrackApiCollapse').on('shown.bs.collapse', maybeLoadPushPreviewOnReveal);
} else {
loadPushPreview(false);
}
function syncAutoOptionsState() {
var blnOn = $('#ct_api_auto_enabled').is(':checked');
$('#ct_api_auto_interval, #ct_api_auto_until').prop('disabled', blnClosed || !blnOn);
}
syncAutoOptionsState();
$('#ct_api_auto_enabled').on('change', syncAutoOptionsState);
$('#ct_api_btn_auto_save').on('click', function () {
var $btn = $(this);
var $msg = $('#ct_api_auto_msg');
var $hint = $('#ct_api_auto_hint');
var blnOn = $('#ct_api_auto_enabled').is(':checked');
$btn.prop('disabled', true);
$msg.removeClass('text-success text-danger').text('Enregistrement…');
$.post(strAjaxUrl, {
a: 'sync_auto_save',
eve_id: intEveId,
enabled: blnOn ? 1 : 0,
interval_min: $('#ct_api_auto_interval').val(),
until: $('#ct_api_auto_until').val()
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
$msg.addClass('text-danger').text((res && res.message) ? res.message : 'Erreur');
return;
}
$msg.addClass('text-success').text(res.message || 'Enregistré');
if (res.auto_sync) {
if (res.auto_sync.enabled && res.auto_sync.until) {
$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.');
}
}
}, 'json').fail(function () {
$msg.addClass('text-danger').text('Erreur réseau');
}).always(function () {
$btn.prop('disabled', false);
});
});
$('#ct_api_btn_push_refresh').on('click', function () {
loadPushPreview(true);
$('#ct_api_push_result').empty();
});
// MSIN-4328 — refresh léger 60 s (preview MS1 seulement, pas CT Live / pas de push)
function startStatusAutoRefresh() {
if (intStatusRefreshTimer) {
clearInterval(intStatusRefreshTimer);
}
intStatusRefreshTimer = setInterval(function () {
if (blnPushRunning) {
return;
}
if (!$('#ct_api_push_card').is(':visible')) {
return;
}
if (document.hidden) {
return;
}
loadPushPreview(true);
}, 60000);
}
startStatusAutoRefresh();
function renderSyncLogs(res) {
var $wrap = $('#ct_api_logs_wrap');
if (!$wrap.length) {
return;
}
if (!res || res.state !== 'ok') {
$wrap.removeClass('d-none').html('<div class="text-danger small">'
+ escHtml((res && res.message) ? res.message : 'Erreur logs') + '</div>');
return;
}
var tabLogs = res.logs || [];
var html = '<div class="card border-secondary"><div class="card-header py-2 d-flex justify-content-between align-items-center">';
html += '<strong class="small">Logs sync (50 derniers)</strong>';
html += '<button type="button" class="btn btn-link btn-sm p-0" id="ct_api_btn_logs_close">Fermer</button>';
html += '</div><div class="card-body py-2">';
if (!tabLogs.length) {
html += '<p class="small text-muted mb-0">Aucun log pour cet événement.</p>';
} else {
html += '<div class="ct-api-logs-scroll border rounded">';
html += '<table class="table table-sm table-striped mb-0" style="font-size:0.78rem;">';
html += '<thead class="thead-light"><tr><th>Quand</th><th>Action</th><th>Statut</th><th>par_id</th><th>Message</th></tr></thead><tbody>';
$.each(tabLogs, function (_, row) {
var strCls = (row.status === 'ok') ? 'text-success' : 'text-danger';
html += '<tr>';
html += '<td class="text-nowrap">' + escHtml(row.created_at || '') + '</td>';
html += '<td>' + escHtml(row.action || '') + '</td>';
html += '<td class="' + strCls + '">' + escHtml(row.status || '') + '</td>';
html += '<td>' + escHtml(row.par_id ? String(row.par_id) : '—') + '</td>';
html += '<td>' + escHtml(row.message || '') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
}
html += '</div></div>';
$wrap.removeClass('d-none').html(html);
}
function loadSyncLogs() {
var $wrap = $('#ct_api_logs_wrap');
$wrap.removeClass('d-none').html(waitHtml('Chargement des logs…'));
$.post(strAjaxUrl, { a: 'sync_logs', eve_id: intEveId, limit: 50 }, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
renderSyncLogs(res);
}, 'json').fail(function () {
$wrap.html('<div class="text-danger small">Erreur réseau (logs).</div>');
});
}
$('#ct_api_btn_logs').on('click', function () {
var $wrap = $('#ct_api_logs_wrap');
if ($wrap.is(':visible') && !$wrap.hasClass('d-none') && $wrap.children().length) {
$wrap.addClass('d-none').empty();
return;
}
loadSyncLogs();
});
$(document).on('click', '#ct_api_btn_logs_close', function () {
$('#ct_api_logs_wrap').addClass('d-none').empty();
});
// 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 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 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">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();
});
$(document).on('click', '#ct_api_btn_ct_count_inline', function (e) {
e.preventDefault();
loadCtLiveCount();
});
$('#ct_api_btn_mark_current').on('click', function () {
var intEligible = (objPushPreview && objPushPreview.transferable)
? parseInt(objPushPreview.transferable, 10) : 0;
var $btn = $(this);
$btn.prop('disabled', true);
$.post(strAjaxUrl, {
a: 'sync_mark_current',
eve_id: intEveId,
ok_count: intEligible || (intCtLiveCount || 0)
}, function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || res.state !== 'ok') {
window.alert((res && res.message) ? res.message : 'Échec');
return;
}
loadPushPreview(true);
}, 'json').fail(function () {
window.alert('Erreur réseau');
}).always(function () {
$btn.prop('disabled', false);
});
});
$('#ct_api_btn_push').on('click', function () {
var intPending = 0;
if (objPushPreview) {
if (typeof objPushPreview.pending_push !== 'undefined') {
intPending = objPushPreview.pending_push;
} else {
intPending = objPushPreview.to_push || objPushPreview.transferable || 0;
}
}
// MSIN-4328 — push immédiat, sans confirm
var $btn = $(this);
var $res = $('#ct_api_push_result');
var intChunk = 100;
var intOffset = 0;
var intTotal = intPending;
var intOkSum = 0;
var intErrSum = 0;
var intPostSum = 0;
var intPutSum = 0;
var tabErrAll = [];
var blnFatal = false;
$btn.prop('disabled', true);
blnPushRunning = true;
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', true);
function renderProgress(intDone, intTot, strExtra) {
var intPct = (intTot > 0) ? Math.min(100, Math.round((intDone * 100) / intTot)) : 0;
var strHtml = '<div class="mb-1"><strong>Envoi</strong> — '
+ intDone + ' / ' + intTot
+ (strExtra ? (' · ' + escHtml(strExtra)) : '')
+ '</div>';
strHtml += '<div class="progress" style="height:1.25rem;">';
strHtml += '<div class="progress-bar progress-bar-striped progress-bar-animated bg-primary" '
+ 'role="progressbar" style="width:' + intPct + '%;" aria-valuenow="' + intPct
+ '" aria-valuemin="0" aria-valuemax="100">' + intPct + '%</div>';
strHtml += '</div>';
if (intOkSum || intErrSum) {
strHtml += '<p class="small text-muted mb-0 mt-1">OK ' + intOkSum
+ ' · erreurs ' + intErrSum
+ ' · POST ' + intPostSum + ' · PUT ' + intPutSum + '</p>';
}
$res.html(strHtml);
}
function finishPush() {
blnPushRunning = false;
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
$btn.prop('disabled', false);
var strFinal = (intErrSum === 0)
? (intOkSum + ' participant(s) synchronisé(s) (POST ' + intPostSum + ' · PUT ' + intPutSum + ').')
: (intOkSum + ' OK, ' + intErrSum + ' erreur(s).');
var strState = blnFatal ? 'error' : ((intErrSum === 0) ? 'ok' : 'partial');
renderPushResult({
state: strState,
message: strFinal,
errors: tabErrAll.slice(0, 20)
});
loadPushPreview(true);
}
function pushChunk() {
renderProgress(intOffset, intTotal, 'lot en cours…');
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 300000,
data: {
a: 'sync_push',
eve_id: intEveId,
offset: intOffset,
limit: intChunk,
refresh_preview: 0
}
}).done(function (res) {
if (rejectIfSessionLost(res)) {
return;
}
if (!res || (res.state !== 'ok' && res.state !== 'partial')) {
if (res && res.total === 0) {
renderPushResult(res);
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
blnPushRunning = false;
$btn.prop('disabled', false);
return;
}
blnFatal = true;
if (res && res.message) {
tabErrAll.push(res.message);
}
finishPush();
return;
}
if (res.total) {
intTotal = parseInt(res.total, 10) || intTotal;
}
intOkSum += parseInt(res.pushed_ok || 0, 10);
intErrSum += parseInt(res.pushed_error || 0, 10);
intPostSum += parseInt(res.pushed_post || 0, 10);
intPutSum += parseInt(res.pushed_put || 0, 10);
if (res.errors && res.errors.length) {
$.each(res.errors, function (_, msg) {
if (tabErrAll.length < 20) {
tabErrAll.push(msg);
}
});
}
intOffset = parseInt(res.next_offset != null ? res.next_offset : (intOffset + (res.chunk_done || intChunk)), 10);
renderProgress(Math.min(intOffset, intTotal), intTotal, '');
if (res.done || intOffset >= intTotal) {
finishPush();
return;
}
pushChunk();
}).fail(function (jqXHR, strStatus) {
blnFatal = true;
var strDetail = strStatus === 'timeout'
? 'Délai dépassé sur un lot — relancer le push (upsert CT).'
: ('Erreur réseau sur un lot (' + strStatus
+ (jqXHR && jqXHR.status ? ', HTTP ' + jqXHR.status : '') + ').');
if (jqXHR && jqXHR.responseText) {
var strBody = String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
if (strBody) {
strDetail += ' — ' + strBody;
}
}
tabErrAll.push(strDetail);
finishPush();
});
}
renderProgress(0, intTotal, 'préparation…');
$.ajax({
url: strAjaxUrl,
type: 'POST',
dataType: 'json',
timeout: 180000,
data: { a: 'sync_push_prepare', eve_id: intEveId }
}).done(function (prep) {
if (rejectIfSessionLost(prep)) {
return;
}
if (!prep || prep.state !== 'ok') {
renderPushResult(prep || { state: 'error', message: 'Préparation échouée' });
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
blnPushRunning = false;
$btn.prop('disabled', false);
return;
}
intTotal = parseInt(prep.total, 10) || intTotal;
if (prep.chunk_size) {
intChunk = parseInt(prep.chunk_size, 10) || intChunk;
}
renderProgress(0, intTotal, 'démarrage');
pushChunk();
}).fail(function (jqXHR, strStatus) {
var strMsg = 'Erreur préparation push (' + strStatus
+ (jqXHR && jqXHR.status ? ', HTTP ' + jqXHR.status : '') + ')';
if (jqXHR && jqXHR.responseText) {
strMsg += ' — ' + String(jqXHR.responseText).replace(/\s+/g, ' ').substr(0, 180);
}
strMsg += '. Vérifiez que le code MSIN-4328 est déployé (action sync_push_prepare).';
renderPushResult({ state: 'error', message: strMsg });
$('#ct_api_btn_push_refresh, #ct_api_btn_ct_count, #ct_api_btn_logs').prop('disabled', false);
blnPushRunning = false;
$btn.prop('disabled', false);
});
});
}
})(jQuery);
</script>
<?php
}
function fxChronotrackApiAdminRenderDashboard($strLangue = 'fr', $blnInsideV2Panel = false) {
$arrRows = fxChronotrackApiListActiveConfigs();
if (!$blnInsideV2Panel) {
echo '<h1 class="h3">MSIN API ChronoTrack — Courses connectées</h1>';
}
echo '<p class="text-muted">Événements liés ou sync active. Le dashboard détaillé (diff, push) viendra en Phase 2.</p>';
if ($arrRows === null || count($arrRows) === 0) {
echo '<div class="alert alert-light">Aucune course connectée.</div>';
return;
}
echo '<table class="table table-sm table-striped"><thead><tr><th>Événement MS1</th><th>ChronoTrack</th><th>Statut</th><th></th></tr></thead><tbody>';
for ($i = 1; $i <= count($arrRows); $i++) {
$arr = $arrRows[$i];
echo '<tr>';
echo '<td>' . fxChronotrackApiAdminEsc($arr['eve_nom']) . ' <span class="text-muted">(' . intval($arr['eve_id']) . ')</span></td>';
echo '<td>' . fxChronotrackApiAdminEsc($arr['ct_event_name']) . ' <span class="text-muted">(' . intval($arr['ct_event_id']) . ')</span></td>';
echo '<td>' . fxChronotrackApiAdminEsc(fxChronotrackApiConfigStatusLabel($arr['sync_status'])) . '</td>';
echo '<td><a class="btn btn-outline-primary btn-sm" href="chronotrack_api.php?eve_id=' . intval($arr['eve_id']) . '">Configurer</a></td>';
echo '</tr>';
}
echo '</tbody></table>';
}