diff --git a/.cursor/rules/projet/10-documentation.mdc b/.cursor/rules/projet/10-documentation.mdc index e28399d..b767af8 100644 --- a/.cursor/rules/projet/10-documentation.mdc +++ b/.cursor/rules/projet/10-documentation.mdc @@ -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 d’exé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. diff --git a/.cursor/rules/projet/10-workflow.mdc b/.cursor/rules/projet/10-workflow.mdc index 96624ed..89d1e85 100644 --- a/.cursor/rules/projet/10-workflow.mdc +++ b/.cursor/rules/projet/10-workflow.mdc @@ -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 d’autres 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 = d’abord **structure Navicat** depuis préprod, **puis** sync outil. +- `info` : jamais de DELETE (auto-création `afficheTexte`). Soft only. +- Catalogues (droits, pages, doc…) : pas d’auto-création ; l’alignement données passe par l’outil, 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 qu’il s’exé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). diff --git a/php/inc_fx_static_sync.php b/php/inc_fx_static_sync.php index dcf97fe..600c0e3 100644 --- a/php/inc_fx_static_sync.php +++ b/php/inc_fx_static_sync.php @@ -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 d’abord 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 d’appels 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 + . '. N’existe 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 l’UI. */ +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é d’affichage : 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; } diff --git a/php/inc_fx_static_sync_config.php b/php/inc_fx_static_sync_config.php index e9bfe44..3a57025 100644 --- a/php/inc_fx_static_sync_config.php +++ b/php/inc_fx_static_sync_config.php @@ -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', diff --git a/php/sync_static_db.php b/php/sync_static_db.php index 7377b3f..c64afae 100644 --- a/php/sync_static_db.php +++ b/php/sync_static_db.php @@ -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 !== '') {
info. Catalogues accès v2 (rôles / matrice / permissions) :
+ manquants + corrections, et Retirer en trop si besoin — sauf rôle encore utilisé en accès promoteur.
Affichage limité à / .
+ + + + 0) { ?> +Aucun écart à afficher.