Files
ms1inscription-v5/php/sync_static_db.php

425 lines
21 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
/**
* CON-333 — Tableau de bord sync tables statiques (soft).
* Source : devinscription_preprod. Aucun DELETE — insert manquants + réparation poubelles info.
*/
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 (empty($_SESSION['usa_id'])) {
fxAdminRequireSuperadmLogin();
}
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');
}
function ssSoftActionLabel($strType) {
if ($strType === 'extra') {
return 'en trop (cible seulement — ne pas effacer auto)';
}
if ($strType === 'missing') {
return 'manque en cible';
}
if ($strType === 'poubelle') {
return 'poubelle réparable';
}
if ($strType === 'diff') {
return 'contenu différent — Appliquer corrections';
}
return 'contenu différent';
}
$strCsrf = fxStaticSyncEnsureCsrfToken();
$strFlash = '';
$strFlashType = 'info';
$tabPopFlash = fxStaticSyncPopFlash();
if (is_array($tabPopFlash)) {
$strFlash = $tabPopFlash['message'];
$strFlashType = $tabPopFlash['type'];
}
// CON-333 — actions soft : apply_missing | apply_content (préprod = vérité textes).
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
$strAction = trim($_POST['action'] ?? '');
if (!fxStaticSyncValidateCsrf($_POST['csrf'] ?? '')) {
fxStaticSyncSetFlash('Jeton CSRF invalide.', 'bad');
} elseif (!in_array($strAction, array('apply_missing', 'apply_content', 'apply_all', 'remove_extras'), true)) {
fxStaticSyncSetFlash(
'Action refusée. Sync destructif désactivé (CON-333). '
. 'Utiliser Appliquer manquants / corrections / Retirer en trop (catalogues).',
'bad'
);
} else {
$strTable = trim($_POST['table'] ?? '');
$strEnvId = trim($_POST['env'] ?? '');
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
if (!$tabEnv || !empty($tabEnv['is_source'])) {
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
} else {
$tabConnections = fxStaticSyncOpenConnections();
$objSource = $tabConnections['dev_preprod'];
$objTarget = $tabConnections[$strEnvId];
if (!$objSource || !$objTarget) {
fxStaticSyncSetFlash(
'Connexion BD impossible — ' . fxStaticSyncFormatConnectionError($tabEnv),
'bad'
);
} elseif ($strAction === 'apply_all') {
$tabResult = fxStaticSyncApplyAllSoft($objSource, $objTarget, $strEnvId);
$strMsg = $tabEnv['label'] . ' — ' . $tabResult['message'];
if (!empty($tabResult['lines'])) {
$strMsg .= "\n" . implode("\n", $tabResult['lines']);
}
fxStaticSyncSetFlash($strMsg, !empty($tabResult['ok']) ? 'ok' : 'bad');
} else {
$tabDef = fxStaticSyncFindTableDef($strTable);
if (!$tabDef) {
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
} else {
if ($strAction === 'apply_missing') {
$tabResult = fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, $tabDef);
} elseif ($strAction === 'remove_extras') {
$tabResult = fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, $tabDef);
} else {
$tabResult = fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, $tabDef);
}
if ($tabResult['ok']) {
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$strMsg = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
$intExtra = count($tabVerify['extra_keys']);
$intMiss = count($tabVerify['missing_keys']);
$intDiff = count($tabVerify['diff_keys']);
$strMsg .= ' Après : manque ' . $intMiss
. ', contenu diff. ' . $intDiff
. ', en trop (conservés) ' . $intExtra . '.';
if ($intMiss === 0 && $intDiff === 0 && $tabVerify['status'] === 'ok') {
fxStaticSyncSetFlash($strMsg, 'ok');
} elseif ($intMiss === 0 && $intDiff === 0) {
fxStaticSyncSetFlash($strMsg . ' Écarts restants = revue manuelle (pas de DELETE).', 'ok');
} else {
fxStaticSyncSetFlash($strMsg, 'ok');
}
} else {
fxStaticSyncSetFlash(
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
'bad'
);
}
}
}
}
}
header('Location: sync_static_db.php#sync-result');
exit;
}
$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 (soft) — CON-333</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: #e8f5e9; border: 1px solid #a5d6a7; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
.warn-box strong { color: #1b5e20; }
.proc { background: #fff; border: 1px solid #ccc; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
.proc ol { margin: 8px 0 0; padding-left: 22px; }
.proc li { margin-bottom: 4px; }
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; margin-right: 4px; 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-soft { border-color: #2e7d32; color: #1b5e20; }
.btn-content { border-color: #1565c0; color: #0d47a1; }
.btn-remove { border-color: #c62828; color: #b71c1c; }
.btn-repair { border-color: #ef6c00; color: #e65100; }
.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 — mode soft (CON-333)</h1>
<div class="warn-box">
<strong>Aucun DELETE.</strong>
On nefface jamais <code>info</code>. Catalogues accès v2 (rôles / matrice / permissions) :
manquants + corrections, et <strong>Retirer en trop</strong> si besoin — sauf rôle encore utilisé en accès promoteur.
</div>
<div class="proc">
<strong>Test complet (un environnement à la fois)</strong>
<ol>
<li>Rafraîchir cette page (nouveau code déployé).</li>
<li>Cliquer <em>TOUT APPLIQUER</em> sous la colonne voulue (commencer par <strong>Dev prod</strong>).</li>
<li>Lire le message vert/rouge en haut : succès ou échec + détail table par table.</li>
<li>Recharger : Manque=0 et Diff=0 partout = OK. « En trop » peut rester (normal, on nefface pas).</li>
<li>Lignes SAUTÉES (structure / table absente) = à corriger en Navicat, puis re-cliquer.</li>
</ol>
<form method="post" style="margin-top:12px;" onsubmit="return confirm('CON-333 — TOUT APPLIQUER sur Dev prod ?\nToutes les tables : manquants + corrections.\nAucun DELETE.');">
<input type="hidden" name="action" value="apply_all">
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
<input type="hidden" name="env" value="dev_prod">
<button type="submit" class="btn btn-content" style="font-weight:700;padding:8px 14px;">TOUT APPLIQUER → Dev prod</button>
</form>
</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.
</p>
<div id="sync-result"></div>
<?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">Il en manque X</span>
<span class="ss-bad">Il y en a X en trop</span>
<span class="ss-warn">Contenu différent</span>
<span class="ss-schema">Structures non alignées</span>
<span class="ss-muted">Table absente / connexion</span>
</div>
<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>
<?php if (!empty($tabRow['source_dupes'])) { ?>
<div class="count-hint"><?php echo (int)$tabRow['source_dupes']; ?> doublon(s) clé en source</div>
<?php } ?>
</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);
$intMissing = isset($tabCmp['missing_keys']) ? count($tabCmp['missing_keys']) : 0;
$intExtra = isset($tabCmp['extra_keys']) ? count($tabCmp['extra_keys']) : 0;
$intDiff = isset($tabCmp['diff_keys']) ? count($tabCmp['diff_keys']) : 0;
$blnCanRemoveExtras = !empty($tabDef['allow_remove_extras']);
$blnCanWrite = ($tabCmp['status'] !== 'no_connection' && $tabCmp['status'] !== 'schema_mismatch'
&& $tabCmp['status'] !== 'missing_table' && $tabCmp['status'] !== 'error');
?>
<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 ($blnCanWrite) { ?>
<div class="count-hint">
Manque <?php echo (int)$intMissing; ?>
· Diff <?php echo (int)$intDiff; ?>
· En trop <?php echo (int)$intExtra; ?>
</div>
<?php } ?>
<?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') {
$strConnErr = fxStaticSyncConnectError($tabEnv['config']['db'] ?? '');
if ($strConnErr !== '') { ?>
<div class="count-hint"><?php echo ssEsc($strConnErr); ?></div>
<?php } } ?>
<?php if ($blnCanWrite && $intMissing > 0) { ?>
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Ajouter <?php echo (int)$intMissing; ?> ligne(s) manquante(s) dans <?php echo ssEsc($tabEnv['label']); ?> ?\nAucun DELETE.');">
<input type="hidden" name="action" value="apply_missing">
<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-soft">Appliquer manquants (<?php echo (int)$intMissing; ?>)</button>
</form>
<?php } ?>
<?php if ($blnCanWrite && $intDiff > 0) { ?>
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Écraser <?php echo (int)$intDiff; ?> ligne(s) en cible avec le contenu préprod ?\nLes clés en trop en cible ne sont pas touchées. Aucun DELETE.');">
<input type="hidden" name="action" value="apply_content">
<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-content">Appliquer corrections (<?php echo (int)$intDiff; ?>)</button>
</form>
<?php } ?>
<?php if ($blnCanWrite && $blnCanRemoveExtras && $intExtra > 0) { ?>
<form method="post" style="display:inline;" onsubmit="return confirm('CON-333 — Retirer <?php echo (int)$intExtra; ?> en trop dans <?php echo ssEsc($tabEnv['label']); ?> ?\nCatalogue accès v2 seulement.\nLes rôles encore utilisés (inscriptions_eve_acces) ne seront PAS effacés.');">
<input type="hidden" name="action" value="remove_extras">
<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-remove">Retirer en trop (<?php echo (int)$intExtra; ?>)</button>
</form>
<?php } ?>
</td>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
<?php if (is_array($tabDetail) && !empty($tabDetail['compare'])) {
$tabCmp = $tabDetail['compare'];
$tabDef = fxStaticSyncFindTableDef($strDetailTable);
$tabEnv = fxStaticSyncFindEnvDef($strDetailEnv);
$intMissing = count($tabCmp['missing_keys']);
$intExtra = count($tabCmp['extra_keys']);
$intDiff = count($tabCmp['diff_keys']);
$tabMissingSamples = isset($tabDetail['samples_missing']) ? $tabDetail['samples_missing'] : array();
$tabDiffSamples = isset($tabDetail['samples_diff']) ? $tabDetail['samples_diff'] : array();
$tabExtraSamples = isset($tabDetail['samples_extra']) ? $tabDetail['samples_extra'] : array();
?>
<div class="detail-box">
<h2>Détails — <?php echo ssEsc($tabDef['label'] ?? $strDetailTable); ?> → <?php echo ssEsc($tabEnv['label'] ?? $strDetailEnv); ?></h2>
<p>
Manque <strong><?php echo (int)$intMissing; ?></strong>
· Contenu différent <strong><?php echo (int)$intDiff; ?></strong>
· En trop en cible <strong><?php echo (int)$intExtra; ?></strong>
<a class="btn" href="sync_static_db.php" style="margin-left:12px;">Fermer</a>
</p>
<?php if (!empty($tabDetail['note'])) { ?>
<p class="count-hint"><?php echo ssEsc($tabDetail['note']); ?></p>
<?php } ?>
<?php if (!empty($tabDetail['extra_context'])) { ?>
<p class="warn-box" style="margin-top:10px;"><?php echo ssEsc($tabDetail['extra_context']); ?></p>
<?php } ?>
<?php if (count($tabExtraSamples) > 0) { ?>
<h3 style="margin-top:18px;font-size:1rem;">En trop en cible (<?php echo (int)$intExtra; ?>) — explications</h3>
<ul class="detail-list">
<?php foreach ($tabExtraSamples as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
<br><?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : ssSoftActionLabel('extra')); ?>
<?php if ($tabDef['table'] === 'info' && !empty($tabSample['target_texte'])) { ?>
<br>Texte cible : <em><?php echo ssEsc($tabSample['target_texte']); ?></em>
<?php } ?>
</li>
<?php } ?>
</ul>
<?php if ($intExtra > count($tabExtraSamples)) { ?>
<p class="count-hint">Affichage limité à <?php echo count($tabExtraSamples); ?> / <?php echo (int)$intExtra; ?>.</p>
<?php } ?>
<?php } ?>
<?php if (count($tabMissingSamples) > 0) { ?>
<h3 style="margin-top:18px;font-size:1rem;">Manquants (<?php echo (int)$intMissing; ?>)</h3>
<ul class="detail-list">
<?php foreach ($tabMissingSamples as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
— <?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : 'manque'); ?>
</li>
<?php } ?>
</ul>
<?php } ?>
<?php if (count($tabDiffSamples) > 0) { ?>
<h3 style="margin-top:18px;font-size:1rem;">Contenu différent (<?php echo (int)$intDiff; ?>)</h3>
<ul class="detail-list">
<?php foreach ($tabDiffSamples as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
— <?php echo ssEsc(isset($tabSample['explain']) ? $tabSample['explain'] : 'diff'); ?>
<?php if ($tabDef['table'] === 'info') { ?>
<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 } ?>
<?php if ($intMissing === 0 && $intDiff === 0 && $intExtra === 0) { ?>
<p>Aucun écart à afficher.</p>
<?php } ?>
</div>
<?php } ?>
</body>
</html>