CON-333 — Implement soft synchronization for static tables, allowing for insertion of missing rows and updates to existing records without deletion. Introduce new functions for handling insert columns and info row checks. Update dashboard to reflect non-destructive actions and improve user messaging for clarity. Increment version code.

This commit is contained in:
2026-07-23 08:53:44 -04:00
parent c154196090
commit 18b667d24b
3 changed files with 488 additions and 141 deletions

View File

@ -521,24 +521,64 @@ function fxStaticSyncGetTableEngine($objDb, $strTable) {
return !empty($row['ENGINE']) ? strtoupper($row['ENGINE']) : '';
}
function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) {
/**
* CON-333 — Colonnes à écrire en INSERT soft (hors PK / timestamps ignore_cols).
* Les clés métier restent incluses (elles ne sont pas dans ignore_cols).
*/
function fxStaticSyncInsertColumns(array $tabSchema, array $tabDef) {
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabCols = array();
foreach ($tabSchema['copy_cols'] as $strCol) {
if (!in_array($strCol, $tabIgnore, true)) {
$tabCols[] = $strCol;
}
}
return $tabCols;
}
/** CON-333 — Ligne info « poubelle » (texte = clé, créé à chaud par afficheTexte). */
function fxStaticSyncInfoRowIsPoubelle(array $row) {
if (!isset($row['info_clef']) || !isset($row['info_texte'])) {
return false;
}
return (string)$row['info_texte'] === (string)$row['info_clef'];
}
/** CON-333 — Source a un vrai libellé (pas poubelle / pas vide). */
function fxStaticSyncInfoRowHasRealTexte(array $row) {
if (!isset($row['info_clef']) || !isset($row['info_texte'])) {
return false;
}
$strTexte = trim((string)$row['info_texte']);
if ($strTexte === '') {
return false;
}
return $strTexte !== (string)$row['info_clef'];
}
/**
* CON-333 — Prépare le schéma + jeux de lignes source/cible pour écriture soft.
* Aucune écriture. Retourne ok=false si structure / connexion invalide.
*/
function fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, array $tabDef = null) {
if (!$objSource || !$objTarget) {
return array('ok' => false, 'message' => 'Connexion BD invalide.');
}
if (!$tabDef) {
return array('ok' => false, 'message' => 'Table non configurée.');
}
if (!fxStaticSyncTableExists($objSource, $strTable)) {
return array('ok' => false, 'message' => 'Table absente dans la source.');
}
if (!fxStaticSyncTableExists($objTarget, $strTable)) {
return array('ok' => false, 'message' => 'Table absente dans la cible — exécuter le DDL d\'abord.');
}
if (!$tabDef) {
return array('ok' => false, 'message' => 'Table non configurée.');
}
$tabIgnore = ($tabDef && isset($tabDef['ignore_cols'])) ? $tabDef['ignore_cols'] : array();
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, $tabIgnore);
if (!fxStaticSyncSchemasAligned($tabSchema)) {
@ -546,106 +586,113 @@ function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef
'ok' => false,
'message' => 'Structures non alignées — '
. fxStaticSyncFormatSchemaDiff($tabSchema)
. '. Aligner les structures (Navicat) avant toute sync. Aucune donnée modifiée.',
. '. Aligner les structures (Navicat) avant. Aucune donnée modifiée.',
);
}
$tabCopyCols = $tabSchema['copy_cols'];
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
if (count($tabCopyCols) === 0) {
$tabFetchCheck = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
if (count($tabFetchCheck['rows']) > 0) {
return array(
'ok' => false,
'message' => 'Aucune colonne commune entre source et cible.',
);
if (!empty($tabSourceData['error']) && $tabSourceData['error'] !== null) {
return array('ok' => false, 'message' => 'Lecture source : ' . $tabSourceData['error']);
}
if (!empty($tabTargetData['error']) && $tabTargetData['error'] !== null
&& $tabTargetData['error'] !== 'missing_table') {
return array('ok' => false, 'message' => 'Lecture cible : ' . $tabTargetData['error']);
}
return array(
'ok' => true,
'schema' => $tabSchema,
'source_rows' => $tabSourceData['rows'],
'target_rows' => $tabTargetData['rows'],
'source_dupes' => isset($tabSourceData['duplicate_count']) ? (int)$tabSourceData['duplicate_count'] : 0,
);
}
/**
* CON-333 — INSERT uniquement les clés absentes en cible. Jamais de DELETE.
* Ne touche pas aux lignes déjà présentes (même poubelles — voir repair).
*/
function fxStaticSyncApplyMissingRows($objSource, $objTarget, $strTable, array $tabDef = null) {
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabInsertCols = fxStaticSyncInsertColumns($tabPrep['schema'], $tabDef);
if (count($tabInsertCols) === 0) {
return array('ok' => false, 'message' => 'Aucune colonne insérable (hors ignore_cols).');
}
$tabMissingRows = array();
foreach ($tabPrep['source_rows'] as $strKey => $tabEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
$tabMissingRows[$strKey] = $tabEntry['row'];
}
}
$tabFetch = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
$tabRowsToCopy = $tabFetch['rows'];
$intDupes = $tabFetch['duplicate_count'];
$intMissing = count($tabMissingRows);
if ($intMissing === 0) {
return array(
'ok' => true,
'message' => 'Aucun manquant à ajouter.',
'inserted' => 0,
'skipped_extra' => count($tabPrep['target_rows']) - count(array_intersect_key($tabPrep['target_rows'], $tabPrep['source_rows'])),
);
}
// Préparer les INSERT avant toute écriture destructive (évite une table vide si échec).
$strColList = '`' . implode('`,`', $tabInsertCols) . '`';
$intBatchSize = 50;
$tabBatch = array();
$tabInsertBatches = array();
$intCopied = count($tabRowsToCopy);
if (count($tabCopyCols) > 0 && count($tabRowsToCopy) > 0) {
$strColList = '`' . implode('`,`', $tabCopyCols) . '`';
$intBatchSize = 50;
$tabBatch = array();
foreach ($tabRowsToCopy as $row) {
if (!is_array($row)) {
foreach ($tabMissingRows as $row) {
if (!is_array($row)) {
continue;
}
$tabVals = array();
foreach ($tabInsertCols as $strCol) {
if ($strTable === 'info' && $strCol === 'info_creation') {
$tabVals[] = 'NOW()';
continue;
}
$tabVals = array();
foreach ($tabCopyCols as $strCol) {
if (!array_key_exists($strCol, $row) || $row[$strCol] === null) {
$tabVals[] = 'NULL';
} else {
$tabVals[] = "'" . $objTarget->fxEscape($row[$strCol]) . "'";
}
}
$tabBatch[] = '(' . implode(',', $tabVals) . ')';
if (count($tabBatch) >= $intBatchSize) {
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
$tabBatch = array();
if (!array_key_exists($strCol, $row) || $row[$strCol] === null) {
$tabVals[] = 'NULL';
} else {
$tabVals[] = "'" . $objTarget->fxEscape($row[$strCol]) . "'";
}
}
if (count($tabBatch) > 0) {
$tabBatch[] = '(' . implode(',', $tabVals) . ')';
if (count($tabBatch) >= $intBatchSize) {
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
$tabBatch = array();
}
}
if (count($tabBatch) > 0) {
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
}
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
$blnFkDisabled = false;
$tabFkOff = fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=0');
if (!$tabFkOff['ok']) {
return array('ok' => false, 'message' => 'FK checks : ' . $tabFkOff['error']);
}
$blnFkDisabled = true;
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
// DELETE plutôt que TRUNCATE : compatible FK (InnoDB) avec checks désactivés.
$tabDelete = fxStaticSyncExecQuery($objTarget, 'DELETE FROM `' . $strTable . '`');
if (!$tabDelete['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
return array('ok' => false, 'message' => 'Vidage table : ' . $tabDelete['error']);
}
foreach ($tabInsertBatches as $strSql) {
$tabInsert = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabInsert['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
} else {
return array(
'ok' => false,
'message' => 'Insertion : ' . $tabInsert['error']
. ' — la table cible peut être vide (MyISAM, pas de rollback).',
);
}
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
return array('ok' => false, 'message' => 'Insertion : ' . $tabInsert['error'] . ' (rollback effectué).');
return array(
'ok' => false,
'message' => 'Insertion manquants : ' . $tabInsert['error']
. ($blnTransactional ? ' (rollback — aucune ligne ajoutée).' : ''),
);
}
}
@ -653,26 +700,217 @@ function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
$strMessage = $intCopied . ' ligne(s) copiée(s).';
if ($intDupes > 0) {
$strMessage .= ' (' . $intDupes . ' doublon(s) ignoré(s) en source — même clé métier.)';
$strMessage = $intMissing . ' ligne(s) ajoutée(s) (INSERT manquants uniquement, aucun DELETE).';
if (!empty($tabPrep['source_dupes'])) {
$strMessage .= ' (' . (int)$tabPrep['source_dupes'] . ' doublon(s) clé en source ignoré(s).)';
}
return array(
'ok' => true,
'message' => $strMessage,
'rows' => $intCopied,
'inserted' => $intMissing,
);
}
/**
* CON-333 — WHERE métier (toutes les colonnes keys).
*/
function fxStaticSyncSqlWhereBusinessKeys($objDb, array $row, array $tabKeys) {
$tabParts = array();
foreach ($tabKeys as $strCol) {
if (!array_key_exists($strCol, $row) || $row[$strCol] === null) {
$tabParts[] = '`' . $strCol . '` IS NULL';
} else {
$tabParts[] = '`' . $strCol . "`='" . $objDb->fxEscape($row[$strCol]) . "'";
}
}
return implode(' AND ', $tabParts);
}
/**
* CON-333 — UPDATE des clés déjà présentes mais contenu différent (préprod = vérité textes).
* Inclut les corrections de libellés et les poubelles. Ne DELETE rien. Ninsère pas les manquants.
*/
function fxStaticSyncApplyContentUpdates($objSource, $objTarget, $strTable, array $tabDef = null) {
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabUpdateCols = fxStaticSyncInsertColumns($tabPrep['schema'], $tabDef);
$tabKeys = $tabDef['keys'];
$tabUpdateCols = array_values(array_filter($tabUpdateCols, function ($strCol) use ($tabKeys) {
return !in_array($strCol, $tabKeys, true);
}));
if (count($tabUpdateCols) === 0) {
return array('ok' => false, 'message' => 'Aucune colonne de contenu à mettre à jour.');
}
$tabSchemaCols = $tabPrep['schema']['copy_cols'];
$intUpdated = 0;
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
foreach ($tabPrep['source_rows'] as $strKey => $tabSrcEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
continue;
}
if ($tabPrep['target_rows'][$strKey]['hash'] === $tabSrcEntry['hash']) {
continue;
}
$tabSrc = $tabSrcEntry['row'];
$tabTgt = $tabPrep['target_rows'][$strKey]['row'];
$tabSets = array();
foreach ($tabUpdateCols as $strCol) {
if (!array_key_exists($strCol, $tabSrc) || $tabSrc[$strCol] === null) {
$tabSets[] = '`' . $strCol . '`=NULL';
} else {
$tabSets[] = '`' . $strCol . "`='" . $objTarget->fxEscape($tabSrc[$strCol]) . "'";
}
}
if ($strTable === 'info' && in_array('info_maj', $tabSchemaCols, true)) {
$tabSets[] = '`info_maj`=NOW()';
}
$strSql = 'UPDATE `' . $strTable . '` SET ' . implode(',', $tabSets)
. ' WHERE ' . fxStaticSyncSqlWhereBusinessKeys($objTarget, $tabTgt, $tabKeys);
$tabUpd = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabUpd['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'UPDATE corrections : ' . $tabUpd['error']);
}
$intUpdated++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
return array(
'ok' => true,
'message' => $intUpdated . ' ligne(s) mise(s) à jour depuis la source (corrections / diffs, aucun DELETE).',
'updated' => $intUpdated,
);
}
/**
* CON-333 — info seulement : UPDATE des poubelles cible quand la source a un vrai texte.
* Nécrase jamais un libellé déjà OK. Ne DELETE rien.
*/
function fxStaticSyncRepairInfoPoubelle($objSource, $objTarget, array $tabDef = null) {
if (!$tabDef) {
$tabDef = fxStaticSyncFindTableDef('info');
}
if (!$tabDef || $tabDef['table'] !== 'info') {
return array('ok' => false, 'message' => 'Réparation poubelle réservée à la table info.');
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, 'info', $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabUpdateCols = array(
'info_texte', 'info_aide', 'info_description', 'info_trie', 'info_actif',
'info_option1', 'info_option2', 'info_option3',
);
$tabSchemaCols = $tabPrep['schema']['copy_cols'];
$tabUpdateCols = array_values(array_intersect($tabUpdateCols, $tabSchemaCols));
$intRepaired = 0;
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, 'info') === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
foreach ($tabPrep['source_rows'] as $strKey => $tabSrcEntry) {
if (!isset($tabPrep['target_rows'][$strKey])) {
continue;
}
$tabSrc = $tabSrcEntry['row'];
$tabTgt = $tabPrep['target_rows'][$strKey]['row'];
if (!fxStaticSyncInfoRowIsPoubelle($tabTgt) || !fxStaticSyncInfoRowHasRealTexte($tabSrc)) {
continue;
}
$tabSets = array();
foreach ($tabUpdateCols as $strCol) {
if (!array_key_exists($strCol, $tabSrc) || $tabSrc[$strCol] === null) {
$tabSets[] = '`' . $strCol . '`=NULL';
} else {
$tabSets[] = '`' . $strCol . "`='" . $objTarget->fxEscape($tabSrc[$strCol]) . "'";
}
}
if (in_array('info_maj', $tabSchemaCols, true)) {
$tabSets[] = '`info_maj`=NOW()';
}
$strWhere = '`info_clef`=\'' . $objTarget->fxEscape($tabTgt['info_clef']) . '\''
. ' AND `info_langue`=\'' . $objTarget->fxEscape($tabTgt['info_langue']) . '\''
. ' AND `info_prg`=\'' . $objTarget->fxEscape($tabTgt['info_prg']) . '\''
. ' AND `info_texte`=`info_clef`';
$strSql = 'UPDATE `info` SET ' . implode(',', $tabSets) . ' WHERE ' . $strWhere;
$tabUpd = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabUpd['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'UPDATE poubelle : ' . $tabUpd['error']);
}
$intRepaired++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
return array(
'ok' => true,
'message' => $intRepaired . ' poubelle(s) réparée(s) depuis la source (aucun DELETE, libellés OK inchangés).',
'repaired' => $intRepaired,
);
}
/**
* CON-333 — Ancien Sync destructif (DELETE + INSERT) : désactivé.
* Conservé pour ne pas casser dappels résiduels — renvoie toujours une erreur.
*/
function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) {
return array(
'ok' => false,
'message' => 'CON-333 — Sync destructif désactivé (DELETE interdit). '
. 'Utiliser « Appliquer manquants » et/ou « Réparer poubelles ».',
);
}
@ -817,12 +1055,31 @@ function fxStaticSyncCompareEnvPair($objSource, $objTarget, array $tabDef) {
'missing_keys' => array(),
'extra_keys' => array(),
'diff_keys' => array(),
'poubelle_keys' => array(),
'schema_missing_cols' => array(),
'schema_extra_cols' => array(),
);
}
return fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema);
$tabCmp = fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema);
// CON-333 — poubelles info réparables (cible brute, source OK) — jamais auto-supprimées.
$tabPoubelle = array();
if ($tabDef['table'] === 'info') {
foreach ($tabSourceData['rows'] as $strKey => $tabSrcEntry) {
if (!isset($tabTargetData['rows'][$strKey])) {
continue;
}
$tabSrc = $tabSrcEntry['row'];
$tabTgt = $tabTargetData['rows'][$strKey]['row'];
if (fxStaticSyncInfoRowIsPoubelle($tabTgt) && fxStaticSyncInfoRowHasRealTexte($tabSrc)) {
$tabPoubelle[] = $strKey;
}
}
}
$tabCmp['poubelle_keys'] = $tabPoubelle;
return $tabCmp;
}
function fxStaticSyncBuildReport($tabConnections) {
@ -891,27 +1148,44 @@ function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) {
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
$tabPoubelle = isset($tabCompare['poubelle_keys']) ? $tabCompare['poubelle_keys'] : array();
$tabDetail = array(
'compare' => $tabCompare,
'samples' => array(),
'note' => 'CON-333 — Les clés « en trop » en cible ne sont jamais effacées automatiquement.',
);
// Priorité daffichage : manquants, poubelles, autres diffs, extras (revue manuelle).
$tabKeysToShow = array_slice(
array_merge($tabCompare['missing_keys'], $tabCompare['diff_keys'], $tabCompare['extra_keys']),
array_merge(
$tabCompare['missing_keys'],
$tabPoubelle,
array_diff($tabCompare['diff_keys'], $tabPoubelle),
$tabCompare['extra_keys']
),
0,
30
40
);
foreach ($tabKeysToShow as $strKey) {
$strType = 'diff';
if (in_array($strKey, $tabCompare['extra_keys'], true)) {
$strType = 'extra';
} elseif (in_array($strKey, $tabCompare['missing_keys'], true)) {
$strType = 'missing';
} elseif (in_array($strKey, $tabPoubelle, true)) {
$strType = 'poubelle';
}
$tabSample = array(
'key' => $strKey,
'type' => in_array($strKey, $tabCompare['extra_keys'], true) ? 'extra'
: (in_array($strKey, $tabCompare['missing_keys'], true) ? 'missing' : 'diff'),
'type' => $strType,
'source' => isset($tabSourceData['rows'][$strKey]['row']) ? $tabSourceData['rows'][$strKey]['row'] : null,
'target' => isset($tabTargetData['rows'][$strKey]['row']) ? $tabTargetData['rows'][$strKey]['row'] : null,
);
if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') {
if ($tabDef['table'] === 'info' && ($strType === 'diff' || $strType === 'poubelle')) {
$tabSample['source_texte'] = isset($tabSample['source']['info_texte']) ? $tabSample['source']['info_texte'] : '';
$tabSample['target_texte'] = isset($tabSample['target']['info_texte']) ? $tabSample['target']['info_texte'] : '';
}

View File

@ -128,6 +128,18 @@ function fxStaticSyncTableDefinitions() {
'keys' => array('reg_id'),
'ignore_cols' => array(),
),
// CON-333 — seeds traçabilité fiche (MSIN-4464) ; log runtime exclu.
array(
'table' => 'inscriptions_fiche_audit_settings',
'label' => 'Audit fiche — réglages',
'keys' => array('setting_id'),
'ignore_cols' => array('setting_updated_at'),
),
array(
'table' => 'inscriptions_fiche_audit_fields',
'label' => 'Audit fiche — champs',
'keys' => array('field_key'),
'ignore_cols' => array('field_updated_at'),
),
);
}

View File

@ -1,8 +1,8 @@
<?php
/**
* Tableau de bord sync tables statiques — préprod dev, superadm seulement.
* Source : devinscription_preprod
* 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');
@ -26,6 +26,24 @@ 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';
@ -36,9 +54,18 @@ if (is_array($tabPopFlash)) {
$strFlashType = $tabPopFlash['type'];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'sync') {
// 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'), true)) {
fxStaticSyncSetFlash(
'Action refusée. Sync destructif désactivé (CON-333). '
. 'Utiliser Appliquer manquants / Appliquer corrections.',
'bad'
);
} else {
$strTable = trim($_POST['table'] ?? '');
$strEnvId = trim($_POST['env'] ?? '');
@ -58,17 +85,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
'bad'
);
} else {
$tabResult = fxStaticSyncCopyTable($objSource, $objTarget, $strTable, $tabDef);
if ($strAction === 'apply_missing') {
$tabResult = fxStaticSyncApplyMissingRows($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'];
if ($tabVerify['status'] === 'ok') {
$strMsg .= ' Vérification : aligné.';
$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 {
$strMsg .= ' ATTENTION — après sync : '
. fxStaticSyncStatusLabel($tabVerify['status'], $tabVerify) . '.';
fxStaticSyncSetFlash($strMsg, 'bad');
fxStaticSyncSetFlash($strMsg, 'ok');
}
} else {
fxStaticSyncSetFlash(
@ -100,7 +137,7 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sync tables statiques</title>
<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; }
@ -109,8 +146,11 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
.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; }
.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; }
@ -122,9 +162,11 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
.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 { 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-sync { border-color: #2e6da4; color: #1a4f7a; }
.btn-soft { border-color: #2e7d32; color: #1b5e20; }
.btn-content { border-color: #1565c0; color: #0d47a1; }
.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; }
@ -136,11 +178,25 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
</head>
<body>
<h1>Sync tables statiques</h1>
<h1>Sync tables statiques — mode soft (CON-333)</h1>
<div class="warn-box">
<strong>Attention — opération destructive.</strong>
Sync = recopie complète depuis dev préprod (efface la cible). Un clic sur Sync.
<strong>Aucun DELETE.</strong>
On nefface jamais la cible. Actions : <strong>manquants</strong> (INSERT) et <strong>corrections</strong> (UPDATE depuis préprod).
Les clés présentes seulement en cible restent (à revoir manuellement).
</div>
<div class="proc">
<strong>Procédure test (dev préprod → dev prod)</strong>
<ol>
<li>Structures alignées (Navicat) — déjà fait.</li>
<li>Lire le tableau : manque / poubelles / en trop.</li>
<li>Pour chaque table concernée : <em>Appliquer manquants</em>.</li>
<li>Textes / contenu corrigés en préprod : <em>Appliquer corrections</em>.</li>
<li>« En trop » = revue manuelle — ne pas supprimer auto.</li>
<li>Quand OK ici → même outil plus tard vers client préprod / prod.</li>
</ol>
</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.
@ -197,12 +253,24 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
}
$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;
$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(),
@ -220,13 +288,22 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
if ($strConnErr !== '') { ?>
<div class="count-hint"><?php echo ssEsc($strConnErr); ?></div>
<?php } } ?>
<?php if ($tabCmp['status'] !== 'no_connection' && $tabCmp['status'] !== 'schema_mismatch') { ?>
<form method="post" style="display:inline;">
<input type="hidden" name="action" value="sync">
<?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-sync">Sync →</button>
<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 } ?>
</td>
@ -240,44 +317,28 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
$tabCmp = $tabDetail['compare'];
$tabDef = fxStaticSyncFindTableDef($strDetailTable);
$tabEnv = fxStaticSyncFindEnvDef($strDetailEnv);
$intMissing = count($tabCmp['missing_keys']);
$intExtra = count($tabCmp['extra_keys']);
$intDiff = count($tabCmp['diff_keys']);
?>
<div class="detail-box">
<h2>Détails — <?php echo ssEsc($tabDef['label'] ?? $strDetailTable); ?> → <?php echo ssEsc($tabEnv['label'] ?? $strDetailEnv); ?></h2>
<p>
<?php
$intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count'];
if ($intDelta < 0) {
echo 'Il en manque ' . abs($intDelta) . ' (cible ' . (int)$tabCmp['target_count']
. ' · source ' . (int)$tabCmp['source_count'] . ')';
} elseif ($intDelta > 0) {
echo 'Il y en a ' . $intDelta . ' en trop (cible ' . (int)$tabCmp['target_count']
. ' · source ' . (int)$tabCmp['source_count'] . ')';
} else {
echo 'Même nombre de lignes, contenu différent (cible ' . (int)$tabCmp['target_count']
. ' · source ' . (int)$tabCmp['source_count'] . ')';
}
?>
<?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 } } ?>
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['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') { ?>
(<?php echo ssEsc(ssSoftActionLabel($tabSample['type'])); ?>)
<?php if ($tabDef['table'] === 'info' && ($tabSample['type'] === 'diff' || $tabSample['type'] === 'poubelle')) { ?>
<br>Source : <em><?php echo ssEsc($tabSample['source_texte']); ?></em>
<br>Cible : <em><?php echo ssEsc($tabSample['target_texte']); ?></em>
<?php } ?>