CON-333 — Update documentation and workflow for database promotion processes, emphasizing the restriction of SQL execution to dev preprod environments only. Enhance clarity on deployment steps and introduce guidelines for removing excess roles and permissions during synchronization. Increment version code.

This commit is contained in:
2026-07-23 09:16:19 -04:00
parent 9697b37263
commit 5ceeec7fdd
5 changed files with 407 additions and 61 deletions

View File

@ -42,11 +42,12 @@ En-tête obligatoire en tête de fichier :
```sql
-- MSIN-xxxx — Titre court
-- Contexte / prérequis (scripts à exécuter avant)
-- Notes prod (idempotent, safe pilote, etc.)
-- Notes : exécution UNIQUEMENT sur dev préprod ; autres env = Navicat structure + sync_static_db
```
- Indiquer l'ordre d'exécution si plusieurs scripts forment une migration.
- Préférer les scripts **idempotents** quand c'est possible (pattern déjà utilisé dans `sql/`).
- **Ne jamais** documenter / conseiller dexécuter ces scripts hors **dev préprod** (voir `projet/10-workflow.mdc` — Promotion BD).
## Résumé des changements (agent)
@ -54,6 +55,6 @@ En-tête obligatoire en tête de fichier :
1. **Quoi** — comportement ajouté ou corrigé.
2. **Où** — fichiers principaux touchés.
3. **Déploiement** — SQL, `_VERSION_CODE`, config, étapes manuelles.
3. **Déploiement** — SQL **dev préprod seulement**, puis Navicat structure + outil sync pour les autres env ; `_VERSION_CODE` / config / étapes manuelles.
Ne pas créer de fichiers README ou docs hors demande explicite.

View File

@ -1,5 +1,5 @@
---
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy, promotion BD
alwaysApply: true
---
@ -8,10 +8,25 @@ alwaysApply: true
## Numéro JIRA (MSIN-xxxx)
- Au début d'une tâche de développement (nouvelle feature, correctif, refactor), **demander le numéro JIRA** si l'utilisateur ne l'a pas fourni.
- Préfixe courant features : `MSIN-xxxx`. Continuité / ops BD peut aussi être `CON-xxxx`.
- Ne pas bloquer une simple question ou une lecture du code — seulement quand on s'apprête à modifier des fichiers.
- Référencer le ticket dans le code et les scripts SQL : `// MSIN-4290` ou `-- MSIN-4379 — description`.
- Nommer les nouveaux scripts SQL : `sql/MSIN-xxxx-description-courte.sql`.
## Promotion BD (règle absolue)
Ordre **non négociable** pour faire passer structure + données vers dautres environnements :
1. **Scripts SQL** (`sql/MSIN-*.sql`) → exécutés **uniquement** sur **dev préprod**.
2. **Navicat** → copie de **structure** (préprod → autres env : dev prod, client préprod, client prod).
3. **Outil soft sync** (`php/sync_static_db.php`, CON-333) → peupler / aligner les **données** (manquants + corrections).
**Interdit** : demander ou suggérer de « rejouer le SQL » sur prod, préprod client, ou tout env autre que **dev préprod**.
- Table absente / Erreur lecture ailleurs = dabord **structure Navicat** depuis préprod, **puis** sync outil.
- `info` : jamais de DELETE (auto-création `afficheTexte`). Soft only.
- Catalogues (droits, pages, doc…) : pas dauto-création ; lalignement données passe par loutil, pas par un second import SQL.
## Portée des changements
- **Changement minimal** : corriger uniquement ce qui est demandé.
@ -28,5 +43,6 @@ Quand du code voisin contredit le pattern du fichier ou du module :
## Fin de tâche
- Résumer ce qui a changé (fichiers, comportement, SQL à exécuter).
- Mentionner les étapes manuelles restantes (déploiement, import SQL, cache).
- Résumer ce qui a changé (fichiers, comportement).
- Pour le SQL : rappeler quil sexécute **seulement en dev préprod**, puis structure Navicat + sync outil pour le reste — **pas** « exécuter ce script en prod ».
- Mentionner les étapes manuelles restantes (déploiement code, Navicat, sync soft, cache).

View File

@ -267,31 +267,34 @@ function fxStaticSyncFetchRowsByKey($objDb, array $tabDef) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => null);
}
$sql = !empty($tabDef['join_role_code'])
? ('SELECT erp.role_id, erp.perm_key, r.role_code'
. ' FROM `inscriptions_eve_role_permissions` erp'
. ' INNER JOIN `inscriptions_eve_roles` r ON r.role_id = erp.role_id'
. ' ORDER BY r.role_code, erp.perm_key')
: ('SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys));
if (!empty($tabDef['join_role_code'])) {
if (!fxStaticSyncTableExists($objDb, 'inscriptions_eve_roles')) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT erp.role_id, erp.perm_key, r.role_code'
. ' FROM `inscriptions_eve_role_permissions` erp'
. ' INNER JOIN `inscriptions_eve_roles` r ON r.role_id = erp.role_id'
. ' ORDER BY r.role_code, erp.perm_key';
$tabResults = $objDb->fxGetResults($sql);
} else {
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys);
$tabResults = $objDb->fxGetResults($sql);
}
// CON-333 — clsMysql::fxGetResults renvoie null si 0 ligne (pas une erreur SQL).
if (!is_array($tabResults)) {
$tabResults = array();
}
$tabKeyed = array();
$intDupes = 0;
$intPhysical = 0;
if (!is_array($tabResults)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'query_failed');
}
foreach ($tabResults as $row) {
if (!is_array($row)) {
continue;
@ -1010,6 +1013,163 @@ function fxStaticSyncRepairInfoPoubelle($objSource, $objTarget, array $tabDef =
);
}
/**
* CON-333 — Retirer les lignes « en trop » (catalogue accès v2).
* info : jamais. Rôles : refuse si encore utilisés dans inscriptions_eve_acces.
*/
function fxStaticSyncRemoveExtras($objSource, $objTarget, $strTable, array $tabDef = null) {
if (!$tabDef || empty($tabDef['allow_remove_extras'])) {
return array('ok' => false, 'message' => 'Retrait des en trop non autorisé pour cette table.');
}
if ($strTable === 'info') {
return array('ok' => false, 'message' => 'Interdit : jamais de DELETE sur info.');
}
$tabPrep = fxStaticSyncPrepareSoftWrite($objSource, $objTarget, $strTable, $tabDef);
if (empty($tabPrep['ok'])) {
return $tabPrep;
}
$tabExtras = array();
foreach ($tabPrep['target_rows'] as $strKey => $tabEntry) {
if (!isset($tabPrep['source_rows'][$strKey])) {
$tabExtras[$strKey] = $tabEntry['row'];
}
}
$intExtra = count($tabExtras);
if ($intExtra === 0) {
return array('ok' => true, 'message' => 'Aucun en trop à retirer.', 'deleted' => 0, 'skipped' => 0);
}
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
$intDeleted = 0;
$intSkipped = 0;
$tabSkipReasons = array();
foreach ($tabExtras as $strKey => $row) {
if ($strTable === 'inscriptions_eve_roles') {
$intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0;
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
if ($intRoleId <= 0) {
$intSkipped++;
$tabSkipReasons[] = $strCode . ' (role_id invalide)';
continue;
}
$intUsed = 0;
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_acces')) {
$intUsed = (int)$objTarget->fxGetVar(
'SELECT COUNT(*) FROM inscriptions_eve_acces WHERE role_id=' . $intRoleId
);
}
if ($intUsed > 0) {
$intSkipped++;
$tabSkipReasons[] = $strCode . ' (encore ' . $intUsed . ' accès promoteur — non effacé)';
continue;
}
// Nettoyer dabord les liens matrice orphelins de ce rôle.
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) {
$tabDelMat = fxStaticSyncExecQuery(
$objTarget,
'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId
);
if (!$tabDelMat['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'DELETE matrice orpheline : ' . $tabDelMat['error']);
}
}
$strSql = 'DELETE FROM `inscriptions_eve_roles` WHERE role_id=' . $intRoleId
. ' AND role_code=\'' . $objTarget->fxEscape($strCode) . '\' LIMIT 1';
} elseif ($strTable === 'inscriptions_eve_role_permissions') {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '';
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
$intRoleId = isset($row['role_id']) ? (int)$row['role_id'] : 0;
if ($strPerm === '' || ($intRoleId <= 0 && $strCode === '')) {
$intSkipped++;
continue;
}
if ($intRoleId <= 0) {
$intRoleId = (int)$objTarget->fxGetVar(
"SELECT role_id FROM inscriptions_eve_roles WHERE role_code='"
. $objTarget->fxEscape($strCode) . "' LIMIT 1"
);
}
if ($intRoleId <= 0) {
$intSkipped++;
$tabSkipReasons[] = $strCode . '+' . $strPerm . ' (rôle introuvable)';
continue;
}
$strSql = 'DELETE FROM `inscriptions_eve_role_permissions` WHERE role_id=' . $intRoleId
. ' AND perm_key=\'' . $objTarget->fxEscape($strPerm) . '\' LIMIT 1';
} elseif ($strTable === 'inscriptions_eve_permissions') {
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '';
if ($strPerm === '') {
$intSkipped++;
continue;
}
// Si encore référencée dans la matrice cible, ne pas casser.
$intMat = 0;
if (fxStaticSyncTableExists($objTarget, 'inscriptions_eve_role_permissions')) {
$intMat = (int)$objTarget->fxGetVar(
"SELECT COUNT(*) FROM inscriptions_eve_role_permissions WHERE perm_key='"
. $objTarget->fxEscape($strPerm) . "'"
);
}
if ($intMat > 0) {
$intSkipped++;
$tabSkipReasons[] = $strPerm . ' (encore ' . $intMat . ' lien(s) matrice)';
continue;
}
$strSql = 'DELETE FROM `inscriptions_eve_permissions` WHERE perm_key=\''
. $objTarget->fxEscape($strPerm) . '\' LIMIT 1';
} else {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'Table non gérée pour retrait en trop.');
}
$tabDel = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabDel['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
return array('ok' => false, 'message' => 'DELETE en trop : ' . $tabDel['error']);
}
$intDeleted++;
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
$strMessage = $intDeleted . ' en trop retiré(s) (alignement catalogue sur préprod).';
if ($intSkipped > 0) {
$strMessage .= ' ' . $intSkipped . ' conservé(s) (encore utilisés) : '
. implode(' ; ', array_slice($tabSkipReasons, 0, 8));
}
return array(
'ok' => true,
'message' => $strMessage,
'deleted' => $intDeleted,
'skipped' => $intSkipped,
);
}
/**
* CON-333 — Ancien Sync destructif (DELETE + INSERT) : désactivé.
* Conservé pour ne pas casser dappels résiduels — renvoie toujours une erreur.
@ -1150,6 +1310,90 @@ function fxStaticSyncFormatKeyPreview($strKey) {
return $strKey;
}
/**
* CON-333 — Expliquer une ligne « en trop » (présente en cible, absente en préprod).
*/
function fxStaticSyncExplainExtraRow(array $tabDef, array $row = null) {
$strTable = $tabDef['table'];
if (!is_array($row)) {
return 'Présente seulement en cible — absente de la préprod (source).';
}
if ($strTable === 'info') {
$strClef = isset($row['info_clef']) ? (string)$row['info_clef'] : '';
$strLang = isset($row['info_langue']) ? (string)$row['info_langue'] : '';
$strPrg = isset($row['info_prg']) ? (string)$row['info_prg'] : '';
$strTexte = isset($row['info_texte']) ? (string)$row['info_texte'] : '';
$blnPoub = ($strTexte !== '' && $strTexte === $strClef);
if ($blnPoub) {
return 'Auto-créée en cible (texte = clé) — prg « ' . $strPrg . ' » / ' . $strLang
. '. Nexiste pas en préprod ; souvent résidu après trou info.';
}
$strPreview = function_exists('mb_substr') ? mb_substr($strTexte, 0, 80) : substr($strTexte, 0, 80);
return 'Libellé seulement en cible (pas en préprod) — prg « ' . $strPrg . ' » / ' . $strLang
. ' : « ' . $strPreview . ' ». Soit ajouté seulement ici, soit jamais repris en préprod.';
}
if ($strTable === 'inscriptions_eve_roles') {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
$strLab = isset($row['role_label_fr']) ? (string)$row['role_label_fr'] : '';
return 'Rôle seulement en cible : « ' . $strCode . ' »'
. ($strLab !== '' ? ' (' . $strLab . ')' : '')
. '. Pas dans la préprod — ancien rôle ou créé hors sync.';
}
if ($strTable === 'inscriptions_eve_role_permissions') {
$strCode = isset($row['role_code']) ? (string)$row['role_code'] : '?';
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?';
return 'Droit accordé seulement en cible : rôle « ' . $strCode . ' » → « ' . $strPerm . ' ». '
. 'Absent de la matrice préprod — soit préprod incomplète, soit droit en trop à retirer plus tard.';
}
if ($strTable === 'inscriptions_pages') {
$strCode = isset($row['pag_code']) ? (string)$row['pag_code'] : '?';
return 'Page promoteur seulement en cible : « ' . $strCode . ' ».';
}
if ($strTable === 'inscriptions_eve_permissions') {
$strPerm = isset($row['perm_key']) ? (string)$row['perm_key'] : '?';
return 'Permission catalogue seulement en cible : « ' . $strPerm . ' ».';
}
return 'Ligne seulement en cible (clé métier absente en préprod).';
}
/** CON-333 — Échantillon décart enrichi pour lUI. */
function fxStaticSyncBuildSample(array $tabDef, $strKey, $strType, $tabSrcEntry, $tabTgtEntry) {
$tabSrc = is_array($tabSrcEntry) && isset($tabSrcEntry['row']) ? $tabSrcEntry['row'] : null;
$tabTgt = is_array($tabTgtEntry) && isset($tabTgtEntry['row']) ? $tabTgtEntry['row'] : null;
$tabSample = array(
'key' => $strKey,
'type' => $strType,
'source' => $tabSrc,
'target' => $tabTgt,
'explain' => '',
);
if ($strType === 'extra') {
$tabSample['explain'] = fxStaticSyncExplainExtraRow($tabDef, is_array($tabTgt) ? $tabTgt : null);
} elseif ($strType === 'missing') {
$tabSample['explain'] = 'Absente en cible — présente en préprod (Appliquer manquants).';
} elseif ($strType === 'poubelle') {
$tabSample['explain'] = 'Poubelle en cible (texte = clé) — préprod a un vrai libellé (Appliquer corrections).';
} else {
$tabSample['explain'] = 'Même clé, contenu différent (Appliquer corrections = préprod gagne).';
}
if ($tabDef['table'] === 'info' && ($strType === 'diff' || $strType === 'poubelle' || $strType === 'extra')) {
$tabSample['source_texte'] = is_array($tabSrc) && isset($tabSrc['info_texte']) ? $tabSrc['info_texte'] : '';
$tabSample['target_texte'] = is_array($tabTgt) && isset($tabTgt['info_texte']) ? $tabTgt['info_texte'] : '';
}
return $tabSample;
}
function fxStaticSyncEnsureCsrfToken() {
if (empty($_SESSION['ms1_static_sync_csrf'])) {
$_SESSION['ms1_static_sync_csrf'] = bin2hex(random_bytes(16));
@ -1318,53 +1562,81 @@ function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) {
$tabCompare = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore);
if (!empty($tabDef['join_role_code'])) {
$tabSchema['compare_cols'] = $tabDef['keys'];
}
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
$tabPoubelle = isset($tabCompare['poubelle_keys']) ? $tabCompare['poubelle_keys'] : array();
$intExtra = count($tabCompare['extra_keys']);
$strExtraContext = '';
if ($strTable === 'inscriptions_eve_role_permissions' && $intExtra > 0) {
$strExtraContext = 'La préprod n\'a que ' . (int)$tabCompare['source_count']
. ' lien(s) matrice. Les « en trop » sont des droits présents en cible seulement — '
. 'à revoir un par un (préprod incomplète vs anciens droits).';
} elseif ($strTable === 'info' && $intExtra > 0) {
$strExtraContext = 'Clés info seulement en cible : souvent créées à chaud (afficheTexte) '
. 'ou jamais reprises en préprod. Liste ci-dessous : poubelle vs vrai libellé.';
} elseif ($intExtra > 0) {
$strExtraContext = 'Lignes présentes seulement en cible. Soft sync ne les efface pas — '
. 'décider : garder, reporter en préprod, ou retirer plus tard (mode catalogue).';
}
$tabDetail = array(
'compare' => $tabCompare,
'samples' => array(),
'note' => 'CON-333 — Les clés « en trop » en cible ne sont jamais effacées automatiquement.',
'samples_missing' => array(),
'samples_diff' => array(),
'samples_extra' => array(),
'note' => 'CON-333 — Les « en trop » sont listés et expliqués ; ils ne sont pas oubliés.',
'extra_context' => $strExtraContext,
);
// Priorité daffichage : manquants, poubelles, autres diffs, extras (revue manuelle).
$tabKeysToShow = array_slice(
array_merge(
$tabCompare['missing_keys'],
$tabPoubelle,
array_diff($tabCompare['diff_keys'], $tabPoubelle),
$tabCompare['extra_keys']
),
0,
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' => $strType,
'source' => isset($tabSourceData['rows'][$strKey]['row']) ? $tabSourceData['rows'][$strKey]['row'] : null,
'target' => isset($tabTargetData['rows'][$strKey]['row']) ? $tabTargetData['rows'][$strKey]['row'] : null,
foreach (array_slice($tabCompare['missing_keys'], 0, 80) as $strKey) {
$tabDetail['samples_missing'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'missing',
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
null
);
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'] : '';
}
$tabDetail['samples'][] = $tabSample;
}
foreach (array_slice(array_diff($tabCompare['diff_keys'], $tabPoubelle), 0, 80) as $strKey) {
$tabDetail['samples_diff'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'diff',
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
);
}
foreach (array_slice($tabPoubelle, 0, 40) as $strKey) {
$tabDetail['samples_diff'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'poubelle',
isset($tabSourceData['rows'][$strKey]) ? $tabSourceData['rows'][$strKey] : null,
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
);
}
foreach (array_slice($tabCompare['extra_keys'], 0, 300) as $strKey) {
$tabDetail['samples_extra'][] = fxStaticSyncBuildSample(
$tabDef,
$strKey,
'extra',
null,
isset($tabTargetData['rows'][$strKey]) ? $tabTargetData['rows'][$strKey] : null
);
}
$tabDetail['samples'] = array_merge(
$tabDetail['samples_missing'],
$tabDetail['samples_diff'],
$tabDetail['samples_extra']
);
return $tabDetail;
}

View File

@ -43,12 +43,15 @@ function fxStaticSyncTableDefinitions() {
'label' => 'Accès v2 — permissions',
'keys' => array('perm_key'),
'ignore_cols' => array(),
// CON-333 — catalogue : les en trop peuvent être retirés pour coller à la préprod.
'allow_remove_extras' => true,
),
array(
'table' => 'inscriptions_eve_roles',
'label' => 'Accès v2 — rôles',
'keys' => array('role_code'),
'ignore_cols' => array('role_id', 'role_created_at', 'role_updated_at'),
'allow_remove_extras' => true,
),
array(
'table' => 'inscriptions_eve_role_permissions',
@ -57,6 +60,7 @@ function fxStaticSyncTableDefinitions() {
'keys' => array('role_code', 'perm_key'),
'ignore_cols' => array('role_id'),
'join_role_code' => true,
'allow_remove_extras' => true,
),
array(
'table' => 'inscriptions_pages',

View File

@ -60,10 +60,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if (!fxStaticSyncValidateCsrf($_POST['csrf'] ?? '')) {
fxStaticSyncSetFlash('Jeton CSRF invalide.', 'bad');
} elseif (!in_array($strAction, array('apply_missing', 'apply_content', 'apply_all'), true)) {
} 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 / Appliquer corrections / Tout appliquer.',
. 'Utiliser Appliquer manquants / corrections / Retirer en trop (catalogues).',
'bad'
);
} else {
@ -97,6 +97,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
} 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);
}
@ -177,6 +179,7 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
.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; }
@ -192,8 +195,8 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
<h1>Sync tables statiques — mode soft (CON-333)</h1>
<div class="warn-box">
<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).
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">
@ -272,6 +275,7 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
$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');
?>
@ -322,6 +326,15 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
<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>
@ -336,6 +349,9 @@ if ($strDetailTable !== '' && $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>
@ -348,20 +364,57 @@ if ($strDetailTable !== '' && $strDetailEnv !== '') {
<?php if (!empty($tabDetail['note'])) { ?>
<p class="count-hint"><?php echo ssEsc($tabDetail['note']); ?></p>
<?php } ?>
<?php if (!empty($tabDetail['samples'])) { ?>
<?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 ($tabDetail['samples'] as $tabSample) { ?>
<?php foreach ($tabExtraSamples as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
(<?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>
<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 } else { ?>
<?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>