Files
ms1inscription-v5/php/sync_static_db.php
stephan 709044efef Add enhanced data synchronization functions and improve user feedback
This commit introduces new functions for confirming database names during synchronization, enhancing safety when syncing to production environments. The `fxStaticSyncResolveDataStatus` and `fxStaticSyncFormatDataDiffSummary` functions are added to provide clearer status reporting and summaries of data discrepancies. Additionally, the synchronization process is refined to improve user feedback, including clearer messages regarding data alignment and discrepancies. These changes aim to enhance the robustness and clarity of the database synchronization workflow.
2026-07-06 14:58:59 -04:00

330 lines
15 KiB
PHP

<?php
/**
* Tableau de bord sync tables statiques — préprod dev, superadm seulement.
* Source : devinscription_preprod
*/
require_once('inc_start_time.php');
require_once('inc_fonctions.php');
require_once('inc_fx_messages.php');
require_once('inc_fx_static_sync.php');
if (!fxAdminStaticSyncAllowed()) {
header('HTTP/1.1 403 Forbidden');
echo 'Accès refusé.';
exit;
}
set_time_limit(300);
function ssEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
$strCsrf = fxStaticSyncEnsureCsrfToken();
$strFlash = '';
$strFlashType = 'info';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'sync') {
if (!fxStaticSyncValidateCsrf($_POST['csrf'] ?? '')) {
$strFlash = 'Jeton CSRF invalide.';
$strFlashType = 'bad';
} else {
$strTable = trim($_POST['table'] ?? '');
$strEnvId = trim($_POST['env'] ?? '');
$tabDef = fxStaticSyncFindTableDef($strTable);
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
$strFlash = 'Paramètres invalides.';
$strFlashType = 'bad';
} elseif (!fxStaticSyncWriteAllowed($strEnvId)) {
$strFlash = 'Sync désactivée vers client prod — comparaison seule.';
$strFlashType = 'bad';
} elseif (fxStaticSyncRequiresDbConfirm($strEnvId)) {
$strConfirmDb = trim($_POST['confirm_db'] ?? '');
$strExpectedDb = $tabEnv['config']['db'] ?? '';
if ($strConfirmDb !== $strExpectedDb) {
$strFlash = 'Sync annulée — nom de base incorrect (attendu : ' . $strExpectedDb . ').';
$strFlashType = 'bad';
} else {
$blnDbConfirmed = true;
}
} else {
$blnDbConfirmed = true;
}
if (!empty($blnDbConfirmed)) {
$tabConnections = fxStaticSyncOpenConnections();
$objSource = $tabConnections['dev_preprod'];
$objTarget = $tabConnections[$strEnvId];
if (!$objSource || !$objTarget) {
$strFlash = 'Connexion BD impossible.';
$strFlashType = 'bad';
} else {
$tabResult = fxStaticSyncCopyTable($objSource, $objTarget, $strTable, $tabDef);
if ($tabResult['ok']) {
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$strFlash = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
if ($tabVerify['status'] === 'ok') {
$strFlash .= ' Vérification : aligné.';
$strFlashType = 'ok';
} else {
$strFlash .= ' ATTENTION — après sync : '
. fxStaticSyncStatusLabel($tabVerify['status'], $tabVerify) . '.';
$strFlashType = 'bad';
}
} else {
$strFlash = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'];
$strFlashType = 'bad';
}
}
}
}
}
$tabConnections = fxStaticSyncOpenConnections();
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
$tabReport = fxStaticSyncBuildReport($tabConnections);
$strDetailTable = trim($_GET['detail_table'] ?? '');
$strDetailEnv = trim($_GET['detail_env'] ?? '');
$tabDetail = null;
if ($strDetailTable !== '' && $strDetailEnv !== '') {
$tabDetail = fxStaticSyncGetDiffDetail($strDetailTable, $strDetailEnv, $tabConnections);
}
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sync tables statiques</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f4f6f8; color: #222; }
h1 { font-size: 1.4rem; margin: 0 0 8px; }
.sub { color: #555; margin-bottom: 20px; font-size: 0.95rem; }
.flash { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; white-space: pre-wrap; word-break: break-word; }
.flash-ok { background: #d4edda; border: 1px solid #b7dfc3; }
.flash-bad { background: #f8d7da; border: 1px solid #f1b8be; }
.flash-info { background: #e8f0fe; border: 1px solid #c5d7fb; }
.warn-box { background: #fff3cd; border: 1px solid #ffecb5; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
.warn-box strong { color: #856404; }
table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
th, td { border: 1px solid #ddd; padding: 8px 10px; vertical-align: top; font-size: 0.9rem; }
th { background: #eef2f7; text-align: left; }
.ss-ok { background: #d4edda; color: #155724; }
.ss-warn { background: #fff3cd; color: #856404; }
.ss-bad { background: #f8d7da; color: #721c24; }
.ss-muted { background: #e9ecef; color: #495057; }
.ss-schema { background: #fde2e2; color: #7f1d1d; border: 1px solid #f5c2c2; }
.badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 0.82rem; font-weight: 600; max-width: 100%; line-height: 1.35; }
.count { font-size: 0.82rem; color: #444; margin-top: 4px; }
.count-hint { font-size: 0.78rem; color: #666; }
.btn { display: inline-block; margin-top: 6px; padding: 4px 10px; font-size: 0.82rem; border: 1px solid #888; background: #fff; border-radius: 4px; cursor: pointer; text-decoration: none; color: #222; }
.btn:hover { background: #f0f0f0; }
.btn-sync { border-color: #2e6da4; color: #1a4f7a; }
.detail-box { margin-top: 24px; background: #fff; padding: 16px; border: 1px solid #ccc; border-radius: 6px; }
.detail-box h2 { margin-top: 0; font-size: 1.1rem; }
.detail-list { margin: 0; padding-left: 18px; }
.detail-list li { margin-bottom: 8px; }
.legend { margin: 16px 0; font-size: 0.85rem; }
.legend span { display: inline-block; margin-right: 12px; padding: 2px 8px; border-radius: 4px; }
code { background: #f1f1f1; padding: 1px 4px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Sync tables statiques</h1>
<div class="warn-box">
<strong>Attention — opération destructive.</strong>
Sync = <code>DELETE</code> puis recopie complète depuis dev préprod.
<strong>Client prod : lecture seule</strong> (aucun bouton Sync).
Client préprod : confirmation avec le nom exact de la base.
Structures non alignées : corriger via Navicat avant toute sync.
</div>
<p class="sub">
Source de vérité : <strong>dev préprod</strong> (<code><?php echo ssEsc($arrConDatabasePreProd['db'] ?? ''); ?></code>).
Comparaison vers dev prod, client préprod et client prod (lecture seule pour prod).
</p>
<?php if ($strFlash !== '') { ?>
<div class="flash flash-<?php echo ssEsc($strFlashType); ?>"><?php echo ssEsc($strFlash); ?></div>
<?php } ?>
<div class="legend">
<span class="ss-ok">Données alignées</span>
<span class="ss-warn">Écarts (manque / en trop / contenu)</span>
<span class="ss-schema">Structures non alignées</span>
<span class="ss-muted">Table absente / connexion</span>
</div>
<p class="count-hint" style="margin:-8px 0 16px;">
Compteur : <strong>cible · source</strong> (lignes dans l'environnement comparé · lignes en dev préprod).
Les écarts détaillent combien de clés manquent, sont en trop ou diffèrent — indépendamment du total.
</p>
<table>
<thead>
<tr>
<th>Table</th>
<th>Source<br><small><?php echo ssEsc($tabEnvs['dev_preprod']['config']['db'] ?? ''); ?></small></th>
<?php foreach ($tabEnvs as $strEnvId => $tabEnv) {
if (!empty($tabEnv['is_source'])) {
continue;
} ?>
<th><?php echo ssEsc($tabEnv['label']); ?><br><small><?php echo ssEsc($tabEnv['config']['db'] ?? ''); ?></small></th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($tabReport as $tabRow) {
$tabDef = $tabRow['def']; ?>
<tr>
<td>
<strong><?php echo ssEsc($tabDef['label']); ?></strong><br>
<code><?php echo ssEsc($tabDef['table']); ?></code>
</td>
<td class="ss-muted">
<span class="badge"><?php echo (int)$tabRow['source_count']; ?> ligne(s)</span>
</td>
<?php foreach ($tabEnvs as $strEnvId => $tabEnv) {
if (!empty($tabEnv['is_source'])) {
continue;
}
$tabCmp = isset($tabRow['envs'][$strEnvId]) ? $tabRow['envs'][$strEnvId] : null;
if (!$tabCmp) {
continue;
}
$strClass = fxStaticSyncStatusClass($tabCmp['status']);
$strLabel = fxStaticSyncStatusLabel($tabCmp['status'], $tabCmp);
$strDbName = $tabEnv['config']['db'] ?? '';
?>
<td class="<?php echo ssEsc($strClass); ?>">
<span class="badge"><?php echo ssEsc($strLabel); ?></span>
<div class="count">
Cible <?php echo (int)$tabCmp['target_count']; ?> · Source <?php echo (int)$tabCmp['source_count']; ?>
</div>
<?php if ($tabCmp['status'] === 'schema_mismatch') {
$tabSchemaDiff = array(
'missing_in_target' => isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(),
'extra_in_target' => isset($tabCmp['schema_extra_cols']) ? $tabCmp['schema_extra_cols'] : array(),
);
$strSchemaDiff = fxStaticSyncFormatSchemaDiff($tabSchemaDiff);
if ($strSchemaDiff !== '') { ?>
<div class="count"><?php echo ssEsc($strSchemaDiff); ?></div>
<?php } } ?>
<?php if ($tabCmp['status'] !== 'ok' && $tabCmp['status'] !== 'no_connection') { ?>
<a class="btn" href="?detail_table=<?php echo urlencode($tabDef['table']); ?>&amp;detail_env=<?php echo urlencode($strEnvId); ?>">Détails</a>
<?php } ?>
<?php if ($tabCmp['status'] !== 'no_connection' && $tabCmp['status'] !== 'schema_mismatch' && fxStaticSyncWriteAllowed($strEnvId)) { ?>
<form method="post" style="display:inline;" class="ss-sync-form"
data-table="<?php echo ssEsc($tabDef['table']); ?>"
data-label="<?php echo ssEsc($tabEnv['label']); ?>"
data-db="<?php echo ssEsc($strDbName); ?>"
data-env="<?php echo ssEsc($strEnvId); ?>"
data-confirm-db="<?php echo fxStaticSyncRequiresDbConfirm($strEnvId) ? '1' : '0'; ?>">
<input type="hidden" name="action" value="sync">
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
<input type="hidden" name="table" value="<?php echo ssEsc($tabDef['table']); ?>">
<input type="hidden" name="env" value="<?php echo ssEsc($strEnvId); ?>">
<button type="submit" class="btn btn-sync">Sync →</button>
</form>
<?php } elseif (!fxStaticSyncWriteAllowed($strEnvId)) { ?>
<div class="count-hint">Lecture seule</div>
<?php } ?>
</td>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
<?php if (is_array($tabDetail) && !empty($tabDetail['compare'])) {
$tabCmp = $tabDetail['compare'];
$tabDef = fxStaticSyncFindTableDef($strDetailTable);
$tabEnv = fxStaticSyncFindEnvDef($strDetailEnv);
?>
<div class="detail-box">
<h2>Détails — <?php echo ssEsc($tabDef['label'] ?? $strDetailTable); ?> → <?php echo ssEsc($tabEnv['label'] ?? $strDetailEnv); ?></h2>
<p>
Manquantes : <?php echo count($tabCmp['missing_keys']); ?> —
Différences : <?php echo count($tabCmp['diff_keys']); ?> —
En trop : <?php echo count($tabCmp['extra_keys']); ?>
<?php if ($tabCmp['status'] === 'schema_mismatch') {
$tabSchemaDiff = array(
'missing_in_target' => isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(),
'extra_in_target' => isset($tabCmp['schema_extra_cols']) ? $tabCmp['schema_extra_cols'] : array(),
);
$strSchemaDiff = fxStaticSyncFormatSchemaDiff($tabSchemaDiff);
if ($strSchemaDiff !== '') { ?>
<br><?php echo ssEsc($strSchemaDiff); ?>
<?php } } ?>
<a class="btn" href="sync_static_db.php" style="margin-left:12px;">Fermer</a>
</p>
<?php if (!empty($tabDetail['samples'])) { ?>
<ul class="detail-list">
<?php foreach ($tabDetail['samples'] as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
(<?php
echo ssEsc($tabSample['type'] === 'extra' ? 'en trop'
: ($tabSample['type'] === 'missing' ? 'manque' : 'contenu diff.'));
?>)
<?php if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') { ?>
<br>Source : <em><?php echo ssEsc($tabSample['source_texte']); ?></em>
<br>Cible : <em><?php echo ssEsc($tabSample['target_texte']); ?></em>
<?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<p>Aucun écart à afficher.</p>
<?php } ?>
</div>
<?php } ?>
</body>
<script>
(function () {
document.querySelectorAll('.ss-sync-form').forEach(function (form) {
form.addEventListener('submit', function (e) {
var table = form.getAttribute('data-table');
var label = form.getAttribute('data-label');
var db = form.getAttribute('data-db');
var env = form.getAttribute('data-env');
var needDb = form.getAttribute('data-confirm-db') === '1';
var msg = 'DESTRUCTIF : effacer la table « ' + table + ' » sur '
+ label + ' (' + db + ') et recopier depuis dev préprod ?';
if (!window.confirm(msg)) {
e.preventDefault();
return;
}
if (needDb) {
var typed = window.prompt(
'Environnement client — tapez le nom EXACT de la base cible pour confirmer :',
''
);
if (typed !== db) {
window.alert('Nom de base incorrect. Sync annulée.');
e.preventDefault();
return;
}
var input = form.querySelector('input[name="confirm_db"]');
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = 'confirm_db';
form.appendChild(input);
}
input.value = typed;
}
});
});
})();
</script>
</html>