From aef98311f52dd576769e6e188b1d092f8a77e8da Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 10 Sep 2026 22:58:39 -0400 Subject: [PATCH 1/5] =?UTF-8?q?[ADD]=20requirement=20:=20d=C3=A9clarer=20m?= =?UTF-8?q?arkdown-it-py,=20moteur=20de=20rendu=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The researcher assistant renders the model's markdown into HTML before sanitising it, and markdown-it-py is the engine. It is present in the environment only through bandit -> rich, so a lint tool holds up the render path of a portal page: dropping bandit would drop the engine, with no diagnostic beyond an ImportError at request time. The pin is declared where functional dependencies live, not among the development ones, and matches the version already installed. --- FR --- L'assistant du chercheur rend le markdown du modèle en HTML avant de l'assainir, et markdown-it-py est le moteur. Il n'est présent dans l'environnement que par la chaîne bandit -> rich : un outil de lint porte donc le chemin de rendu d'une page portail, et retirer bandit retirerait le moteur, sans autre diagnostic qu'une ImportError au moment de la requête. L'épingle est déclarée là où vivent les dépendances fonctionnelles, non celles de développement, et reprend la version déjà installée. Assisted-by: Claude Opus 5 --- requirement/pyproject.odoo18.0_python3.12.10.toml | 1 + requirement/requirements.odoo18.0_python3.12.10.txt | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/requirement/pyproject.odoo18.0_python3.12.10.toml b/requirement/pyproject.odoo18.0_python3.12.10.toml index 9a121ec..2616169 100644 --- a/requirement/pyproject.odoo18.0_python3.12.10.toml +++ b/requirement/pyproject.odoo18.0_python3.12.10.toml @@ -84,6 +84,7 @@ lottie = "^0.7.2" lxml = "5.2.1" lxml-html-clean = "<0.4.5" mako = "^1.4.1" +markdown-it-py = "4.2.0" markdown2 = "2.5.1" markdownify = "0.13.1" markupsafe = "2.1.5" diff --git a/requirement/requirements.odoo18.0_python3.12.10.txt b/requirement/requirements.odoo18.0_python3.12.10.txt index bc7b2cb..471cd1e 100644 --- a/requirement/requirements.odoo18.0_python3.12.10.txt +++ b/requirement/requirements.odoo18.0_python3.12.10.txt @@ -101,3 +101,11 @@ factur-x<4 ; platform_machine == 's390x' # 0.4.4 n'imposent aucune borne sur lxml — vérifié sur PyPI, release par # release. À lever le jour où lxml montera. lxml-html-clean<0.4.5 + +# markdown-it-py rend le markdown du modèle de langage en HTML, avant +# assainissement, sur la page de l'assistant. Il est présent dans +# l'environnement par la chaîne bandit -> rich, donc en transitif d'un outil de +# lint : retirer bandit retirerait le moteur de rendu d'une page portail. La +# dépendance est portée ici parce qu'elle est fonctionnelle, et non de +# développement. +markdown-it-py==4.2.0 From c15d97f646d272231baf1a4e81d9411d587496c4 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 14 Sep 2026 04:21:55 -0400 Subject: [PATCH 2/5] [FIX] selenium : rendre tkinter optionnel, il ne sert qu'au coffre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tkinter ne sert qu'au sélecteur de fichier du coffre, et seulement quand aucun chemin KDBX n'est configuré. Importé en tête, il rendait tout le pilotage de navigateur inimportable sur un Python bâti sans lui — donc sur tout serveur sans paquet d'interface graphique. Sans tkinter ni chemin configuré, get_kdbx journalise et rend None, comme le fait déjà kdbx_manager. Vérifié : web_login.py --help sort en 0, et le chemin dégradé appelé directement rend None. --- EN --- tkinter is only needed by the vault file picker, and only when no KDBX path is configured. Imported at module level, it made the whole browser automation unimportable on a Python built without it — that is, on any server with no graphical toolkit package. With no tkinter and no configured path, get_kdbx logs and returns None, as kdbx_manager already does. Checked: web_login.py --help exits 0, and the degraded path called directly returns None. Assisted-by: Claude Opus 5 --- script/selenium/selenium_lib.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/script/selenium/selenium_lib.py b/script/selenium/selenium_lib.py index 6847259..ca71ab0 100644 --- a/script/selenium/selenium_lib.py +++ b/script/selenium/selenium_lib.py @@ -13,9 +13,7 @@ import subprocess import sys import tempfile import time -import tkinter as tk from pathlib import Path -from tkinter import filedialog from typing import Optional from pykeepass import PyKeePass @@ -42,6 +40,18 @@ sys.path.append(new_path) from script.config import config_file +# tkinter ne sert QU'au sélecteur de fichier du coffre, quand aucun chemin +# n'est configuré. Importé en tête, il rendait le module entier inimportable +# sur toute machine dont le Python est bâti sans lui — c'est-à-dire sur tout +# serveur sans paquet d'interface graphique — alors que pas une ligne du +# pilotage de navigateur n'en dépend. +try: + import tkinter as tk + from tkinter import filedialog +except ModuleNotFoundError: + tk = None + filedialog = None + logging.basicConfig( format=( "%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d]" @@ -443,6 +453,12 @@ class SeleniumLib(object): ["kdbx", "path"] ) if not chemin_fichier_kdbx: + if tk is None: + _logger.error( + "tkinter is not available, please fill" + f" {config_file.CONFIG_FILE}" + ) + return root = tk.Tk() root.withdraw() # Hide the main window chemin_fichier_kdbx = filedialog.askopenfilename( From aff0d509725893624b9a27766df3d1727b34ab40 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 14 Sep 2026 04:39:21 -0400 Subject: [PATCH 3/5] =?UTF-8?q?[FIX]=20selenium=20:=20accorder=20le=20priv?= =?UTF-8?q?=C3=A9=20aux=20extensions=20sans=20about:addons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firefox refuse la navigation vers about:addons depuis le contexte contenu, et le contexte chrome exige -remote-allow-system-access, que geckodriver refuse via les capabilities : les deux voies vers la case « Exécuter dans les fenêtres privées » sont fermées. La permission s'accorde à l'installation, par le champ allowPrivateBrowsing de /moz/addon/install ; 46 lignes de clics XPath disparaissent. Un geckodriver qui ignore le champ installe sans la permission au lieu d'interrompre la session. Vérifié : sombre en fenêtre privée, blanc sans extension, repli servi. --- EN --- Firefox refuses navigation to about:addons from the content context, and the chrome context demands -remote-allow-system-access, which geckodriver rejects through capabilities: both routes to the « Run in Private Windows » checkbox are closed. The permission is now granted at install time, through the allowPrivateBrowsing field of /moz/addon/install; 46 lines of XPath clicking are gone. A geckodriver that ignores the field installs without the permission instead of killing the session. Checked: dark in a private window, white with no addon, fallback hit. Assisted-by: Claude Opus 5 --- script/selenium/selenium_lib.py | 105 ++++++++++++++------------------ 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/script/selenium/selenium_lib.py b/script/selenium/selenium_lib.py index ca71ab0..3b520c9 100644 --- a/script/selenium/selenium_lib.py +++ b/script/selenium/selenium_lib.py @@ -2,6 +2,7 @@ # © 2021-2026 TechnoLibre (http://www.technolibre.ca) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +import base64 import datetime import getpass import json @@ -22,6 +23,7 @@ from selenium import webdriver from selenium.common.exceptions import ( ElementClickInterceptedException, TimeoutException, + WebDriverException, ) from selenium.webdriver import ActionChains from selenium.webdriver.common.actions.wheel_input import ScrollOrigin @@ -135,6 +137,44 @@ class SeleniumLib(object): ) self.driver.save_screenshot(file_path) + def _install_addon(self, relative_path, allow_private_browsing): + """Installe une extension, avec l'accès aux fenêtres privées si demandé. + + Prend un chemin relatif à la racine du dépôt, rend l'identifiant de + l'extension installée. + + La permission « fenêtres privées » s'accorde À L'INSTALLATION, par le + champ allowPrivateBrowsing de /moz/addon/install. Elle ne s'accorde + plus autrement : Firefox refuse la navigation vers about:addons depuis + le contexte contenu, et le contexte chrome exige + -remote-allow-system-access, que geckodriver refuse via les + capabilities — la case à cocher de l'interface est hors d'atteinte. + install_addon() de selenium n'expose pas ce champ, d'où la commande + montée à la main. + """ + path = os.path.join(new_path, relative_path) + with open(path, "rb") as addon_file: + addon = base64.b64encode(addon_file.read()).decode("UTF-8") + try: + return self.driver.execute( + "INSTALL_ADDON", + { + "addon": addon, + "temporary": True, + "allowPrivateBrowsing": allow_private_browsing, + }, + )["value"] + except WebDriverException: + # Un geckodriver qui ne connaît pas le champ rejette la requête + # entière. L'extension s'installe alors sans la permission : le + # mode sombre reste inactif en fenêtre privée, ce qui vaut mieux + # qu'une session interrompue au démarrage. + _logger.warning( + "geckodriver refuses allowPrivateBrowsing, installing" + f" {relative_path} without private window access" + ) + return self.driver.install_addon(path, temporary=True) + def configure(self, ignore_open_web=False): # Configuration pour lancer Firefox en mode de navigation privée firefox_options = webdriver.FirefoxOptions() @@ -372,67 +412,16 @@ class SeleniumLib(object): # TODO do a script to check if it's the last version if self.config.use_firefox_driver and not self.config.use_network: if not self.config.no_dark_mode: - self.driver.install_addon( - os.path.join( - new_path, "script/selenium/darkreader-firefox.xpi" - ), - temporary=True, + # Les extensions reçoivent l'accès aux fenêtres privées dès + # l'installation, seul moyen restant de le leur accorder. + allow_private = not self.config.not_private_mode + self._install_addon( + "script/selenium/darkreader-firefox.xpi", allow_private ) - self.driver.install_addon( - os.path.join( - new_path, "script/selenium/odoo_debug-4.0.xpi" - ), - temporary=True, + self._install_addon( + "script/selenium/odoo_debug-4.0.xpi", allow_private ) - if ( - not self.config.not_private_mode - and not self.config.no_dark_mode - ): - # self.driver.set_context("chrome") - self.driver.get("about:addons") - # Enable Dark Reader into incognito - # Click Extensions - self.click('//button[@viewid="addons://list/extension"]') - self.click( - "/html/body/div/div[2]/div/addon-list/section[1]/addon-card/div/div/div/div/button", - ) - self.click( - "/html/body/div/div[2]/div/addon-list/section[1]/addon-card/div/addon-options/panel-list/panel-item[5]", - ) - # Enable Run in Private Windows - try: - self.click( - "/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[6]/div/label[1]/input", - ) - except Exception: - # For old version before Firefox 138 - self.click( - "/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[5]/div/label[1]/input", - ) - - # Enable Odoo_debug into incognito - # Back - self.click( - "/html/body/div/div[2]/addon-page-header/div/div[2]/button" - ) - self.click( - "/html/body/div/div[2]/div/addon-list/section[1]/addon-card[2]/div/div/div/div/button", - ) - self.click( - "/html/body/div/div[2]/div/addon-list/section[1]/addon-card[2]/div/addon-options/panel-list/panel-item[5]", - ) - # Enable Run in Private Windows - try: - self.click( - "/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[6]/div/label[1]/input", - ) - except Exception: - # For old version before Firefox 138 - self.click( - "/html/body/div/div[2]/div/addon-card/div/addon-details/named-deck/section/div[5]/div/label[1]/input", - ) - # Ouvrez la page web if not ignore_open_web: self.driver.get(f"{self.config.url}/web") From 4e5af69969868b5a5c0a4e7a097d0e581a8c1c7f Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 14 Sep 2026 04:58:44 -0400 Subject: [PATCH 4/5] [UPD] changelog : les deux correctifs selenium et markdown-it-py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois entrées sous [Unreleased] : une sous Ajouté pour la dépendance de rendu markdown, deux sous Corrigé pour le démarrage des scripts Selenium sur un Python sans tkinter et pour le mode sombre en fenêtre privée. La source est CHANGELOG.base.md ; la paire générée par make doc_markdown suit dans le même commit, étant ce que la plupart des lecteurs ouvrent. Vérifié : les listes anglaise et française portent le même nombre de puces dans chaque section, Ajouté 16 et Corrigé 8. --- EN --- Three entries under [Unreleased]: one under Added for the markdown rendering dependency, two under Fixed for the Selenium scripts starting on a Python without tkinter and for dark mode in a private window. The source is CHANGELOG.base.md; the pair generated by make doc_markdown follows in the same commit, being what most readers actually open. Checked: the English and French lists carry the same number of bullets in every section, Added 16 and Fixed 8. Assisted-by: Claude Opus 5 --- CHANGELOG.base.md | 7 +++++++ CHANGELOG.fr.md | 4 ++++ CHANGELOG.md | 3 +++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 3ce1a3b..d7df600 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -52,6 +52,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Transform data anonymises an Odoo database too, the entry asking for the SOURCE rather than the format: an external file, a live database, or a backup zip taken from `image_db/`. A zip is never modified — it is restored into a database, anonymised there, then dumped into a NEW zip by Odoo's own backup, so the dump, the filestore and the manifest are written by the software that will read them back. A live database is modified ITSELF, which is said before the work and not after, and nothing is drawn when the write did not happen: declining otherwise handed back a zip of the untouched database, announced as anonymised. A register records which databases the entry produced, since the server does not - An anonymised number can keep its digit count, on the file anonymiser and on `anonymize.py` alike (`--keep-digits`): 8839 then draws in 1000..9999, so the copy keeps columns of the same width, which an export read by eye or imported into a bounded field asks for. Each value keeps ITS own width and not its column's — 7 stays at one digit where 12 keeps two — and below the unit the rule does not apply, 0.15 having no digit before the point. The option trades one guarantee for another: the measured extent no longer bounds anything, so a rate or a year can leave its range, and the preview SAYS so rather than letting it be found at reimport. Uniqueness is what yields when a band fills up, a duplicate of the same width beating a value of a different one, and the band never exceeds what the type RECEIVING the draw holds, which is not always the column's: an Odoo `integer` over a column PostgreSQL did not create as an integer — what an upgraded base carries — is poured into `numeric`, which has no ceiling, where `integer` stops at 2 147 483 647 and a draw past it aborts the whole write, that write being one transaction - The anonymiser asks four questions instead of eleven, seven of the eleven having had an obvious default. Answering them one by one to arrive at the same place makes prompts be passed by reflex, and a prompt passed by reflex consents to nothing; they now live behind « Advanced options? ». The short path DECLARES what it takes, in sentences rather than in yes/no, since those are decisions taken and not questions whose answer was lost. Macros and charts are asked on both paths: they are not visible in the cells +- `markdown-it-py` is declared as a functional dependency: it renders the language model's markdown to HTML, before sanitising, on the assistant page. It reached the environment transitively through `bandit` → `rich`, a lint tool, so removing a lint dependency removed a portal page's rendering engine @@ -76,6 +77,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Transform data anonymise aussi une base Odoo, l'entrée demandant la SOURCE plutôt que le format : un fichier externe, une base vivante, ou un zip de sauvegarde pris dans `image_db/`. Un zip n'est jamais modifié — il est restauré dans une base, anonymisé là, puis vidé dans un zip NEUF par la sauvegarde d'Odoo lui-même, si bien que le dump, le filestore et le manifeste sont écrits par le logiciel qui les relira. Une base vivante est modifiée ELLE-MÊME, ce qui est dit avant le travail et non après, et rien n'est tiré lorsque l'écriture n'a pas eu lieu : un renoncement rendait sinon un zip de la base intacte, annoncé comme anonymisé. Un registre inscrit les bases que l'entrée a produites, le serveur ne le disant pas - Un nombre anonymisé peut garder son nombre de chiffres, sur l'anonymiseur de fichiers comme sur `anonymize.py` (`--keep-digits`) : 8839 tire alors dans 1000..9999, si bien que la copie garde des colonnes de la même largeur, ce qu'un export relu à l'œil ou importé dans un champ borné demande. Chaque valeur garde SA largeur et non celle de sa colonne — 7 reste à un chiffre là où 12 en garde deux — et sous l'unité la règle ne s'applique pas, 0,15 n'ayant pas de chiffre avant la virgule. L'option échange une garantie contre une autre : l'étendue mesurée ne borne plus rien, donc un taux ou une année peut sortir de sa plage, et l'aperçu le DIT au lieu de le laisser découvrir à la réimportation. C'est l'unicité qui cède quand une bande se remplit, un doublon de la même largeur valant mieux qu'une valeur d'une autre, et la bande ne dépasse jamais ce que tient le type qui ACCUEILLE le tirage, lequel n'est pas toujours celui de la colonne : un `integer` d'Odoo posé sur une colonne que PostgreSQL n'a pas créée entière — ce qu'une base montée de version porte — se coule en `numeric`, qui n'a pas de plafond, là où `integer` s'arrête à 2 147 483 647 et où un tirage au-delà fait retomber toute l'écriture, celle-ci tenant en une transaction - L'anonymiseur pose quatre questions au lieu de onze, sept des onze ayant eu un défaut évident. Les répondre une par une pour arriver au même endroit fait passer les invites par réflexe, et une invite qu'on passe par réflexe ne consent à rien : elles vivent désormais derrière « Options avancées ? ». Le chemin court DIT ce qu'il prend, en phrases et non en oui/non, puisque ce sont des décisions prises et non des questions dont on aurait perdu la réponse. Macros et graphiques sont demandés dans les deux chemins : ils ne se voient pas dans les cellules +- `markdown-it-py` est déclaré en dépendance fonctionnelle : il rend en HTML le markdown du modèle de langage, avant assainissement, sur la page de l'assistant. Il arrivait dans l'environnement en transitif par `bandit` → `rich`, un outil de lint, si bien que retirer une dépendance de lint retirait le moteur de rendu d'une page portail + ## Changed @@ -102,6 +105,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Three translation keys declared twice are gone, and a check refuses the next one: a repeated key silently overwrites the previous, which had already cost a menu label - `anonymize.py` abstains from a unique numeric column and NAMES it in the report. A draw guarantees nothing on a unique column, and a number has no way out where text has one — appending the id carries uniqueness for text, but would change a number's magnitude. Two rows drawing the same number failed the UPDATE and, the whole run being one transaction, took the entire anonymisation with them. Without the report naming it, an identifying column stays in the clear with nothing saying so - Restoring a backup no longer raises `AttributeError` before running the command: `_monitoring_restore` read `self._execute` where `TODO` sets `self.execute`, and both entries leading there — the local backup and the remote one — were broken. A check refuses the next one: every `self.X` that `TODO` READS must be set by TODO or by one of its bases. Searching the name across `script/todo/` did not see the fault, another object of the package setting `_execute`; it has to be searched in the classes TODO INHERITS from +- The Selenium scripts run on a Python built without `tkinter` — any server with no graphical toolkit package. That module is only needed by the vault's file picker, so it is now optional: with no `tkinter` and no configured KDBX path, opening the vault logs an error and returns, instead of breaking the import of every browser-automation script +- Dark mode works again in a private window on a current Firefox. The « Run in Private Windows » permission is granted when the addon is installed, through the `allowPrivateBrowsing` field, rather than clicked through the `about:addons` interface: Firefox refuses navigation to `about:addons` from the content context, and the chrome context demands `-remote-allow-system-access`, which geckodriver rejects through capabilities, so both routes to that checkbox are closed. A geckodriver that ignores the field installs without the permission instead of killing the session @@ -113,6 +118,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Trois clés de traduction déclarées deux fois ont disparu, et un contrôle refuse la suivante : une clé répétée écrase la précédente en silence, ce qui avait déjà coûté une étiquette de menu - `anonymize.py` s'abstient sur une colonne numérique unique et la NOMME dans le rapport. Un tirage ne garantit rien sur une colonne unique, et un nombre n'a pas l'issue qu'a le texte — coller l'id porte l'unicité pour du texte, mais changerait la grandeur d'un nombre. Deux lignes tirant le même nombre faisaient échouer l'UPDATE et, transaction unique oblige, emportaient TOUTE l'anonymisation. Sans que le rapport la nomme, une colonne identifiante reste en clair sans que rien ne le dise - Restaurer une sauvegarde ne lève plus `AttributeError` avant de lancer la commande : `_monitoring_restore` lisait `self._execute` là où `TODO` pose `self.execute`, et les deux entrées qui y mènent — la sauvegarde locale et la distante — étaient cassées. Un contrôle refuse le suivant : tout `self.X` que `TODO` LIT doit être posé par TODO ou par une de ses bases. Chercher le nom dans tout `script/todo/` ne voyait pas la faute, un autre objet du paquet posant bien `_execute` ; il faut le chercher dans les classes dont TODO HÉRITE +- Les scripts Selenium tournent sur un Python bâti sans `tkinter` — tout serveur dépourvu de paquet d'interface graphique. Ce module ne sert qu'au sélecteur de fichier du coffre, il est donc désormais optionnel : sans `tkinter` et sans chemin KDBX configuré, l'ouverture du coffre journalise une erreur et rend la main, au lieu de casser l'import de tous les scripts de pilotage de navigateur +- Le mode sombre repart en fenêtre privée sur un Firefox récent. La permission « Exécuter dans les fenêtres privées » est accordée à l'installation de l'extension, par le champ `allowPrivateBrowsing`, au lieu d'être cochée dans l'interface `about:addons` : Firefox refuse la navigation vers `about:addons` depuis le contexte contenu, et le contexte chrome exige `-remote-allow-system-access`, que geckodriver refuse via les capabilities, si bien que les deux voies vers cette case sont fermées. Un geckodriver qui ignore le champ installe sans la permission au lieu d'interrompre la session ## Removed diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 7cf5ad5..8f41abc 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -32,6 +32,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Transform data anonymise aussi une base Odoo, l'entrée demandant la SOURCE plutôt que le format : un fichier externe, une base vivante, ou un zip de sauvegarde pris dans `image_db/`. Un zip n'est jamais modifié — il est restauré dans une base, anonymisé là, puis vidé dans un zip NEUF par la sauvegarde d'Odoo lui-même, si bien que le dump, le filestore et le manifeste sont écrits par le logiciel qui les relira. Une base vivante est modifiée ELLE-MÊME, ce qui est dit avant le travail et non après, et rien n'est tiré lorsque l'écriture n'a pas eu lieu : un renoncement rendait sinon un zip de la base intacte, annoncé comme anonymisé. Un registre inscrit les bases que l'entrée a produites, le serveur ne le disant pas - Un nombre anonymisé peut garder son nombre de chiffres, sur l'anonymiseur de fichiers comme sur `anonymize.py` (`--keep-digits`) : 8839 tire alors dans 1000..9999, si bien que la copie garde des colonnes de la même largeur, ce qu'un export relu à l'œil ou importé dans un champ borné demande. Chaque valeur garde SA largeur et non celle de sa colonne — 7 reste à un chiffre là où 12 en garde deux — et sous l'unité la règle ne s'applique pas, 0,15 n'ayant pas de chiffre avant la virgule. L'option échange une garantie contre une autre : l'étendue mesurée ne borne plus rien, donc un taux ou une année peut sortir de sa plage, et l'aperçu le DIT au lieu de le laisser découvrir à la réimportation. C'est l'unicité qui cède quand une bande se remplit, un doublon de la même largeur valant mieux qu'une valeur d'une autre, et la bande ne dépasse jamais ce que tient le type qui ACCUEILLE le tirage, lequel n'est pas toujours celui de la colonne : un `integer` d'Odoo posé sur une colonne que PostgreSQL n'a pas créée entière — ce qu'une base montée de version porte — se coule en `numeric`, qui n'a pas de plafond, là où `integer` s'arrête à 2 147 483 647 et où un tirage au-delà fait retomber toute l'écriture, celle-ci tenant en une transaction - L'anonymiseur pose quatre questions au lieu de onze, sept des onze ayant eu un défaut évident. Les répondre une par une pour arriver au même endroit fait passer les invites par réflexe, et une invite qu'on passe par réflexe ne consent à rien : elles vivent désormais derrière « Options avancées ? ». Le chemin court DIT ce qu'il prend, en phrases et non en oui/non, puisque ce sont des décisions prises et non des questions dont on aurait perdu la réponse. Macros et graphiques sont demandés dans les deux chemins : ils ne se voient pas dans les cellules +- `markdown-it-py` est déclaré en dépendance fonctionnelle : il rend en HTML le markdown du modèle de langage, avant assainissement, sur la page de l'assistant. Il arrivait dans l'environnement en transitif par `bandit` → `rich`, un outil de lint, si bien que retirer une dépendance de lint retirait le moteur de rendu d'une page portail + ## Modifié - `Assistant › [1]` n'envoie plus chaque question à une seule API distante sur un modèle figé : elle interroge le serveur configuré, et ne retombe sur le distant que lorsqu'aucun serveur local ne répond @@ -46,6 +48,8 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Trois clés de traduction déclarées deux fois ont disparu, et un contrôle refuse la suivante : une clé répétée écrase la précédente en silence, ce qui avait déjà coûté une étiquette de menu - `anonymize.py` s'abstient sur une colonne numérique unique et la NOMME dans le rapport. Un tirage ne garantit rien sur une colonne unique, et un nombre n'a pas l'issue qu'a le texte — coller l'id porte l'unicité pour du texte, mais changerait la grandeur d'un nombre. Deux lignes tirant le même nombre faisaient échouer l'UPDATE et, transaction unique oblige, emportaient TOUTE l'anonymisation. Sans que le rapport la nomme, une colonne identifiante reste en clair sans que rien ne le dise - Restaurer une sauvegarde ne lève plus `AttributeError` avant de lancer la commande : `_monitoring_restore` lisait `self._execute` là où `TODO` pose `self.execute`, et les deux entrées qui y mènent — la sauvegarde locale et la distante — étaient cassées. Un contrôle refuse le suivant : tout `self.X` que `TODO` LIT doit être posé par TODO ou par une de ses bases. Chercher le nom dans tout `script/todo/` ne voyait pas la faute, un autre objet du paquet posant bien `_execute` ; il faut le chercher dans les classes dont TODO HÉRITE +- Les scripts Selenium tournent sur un Python bâti sans `tkinter` — tout serveur dépourvu de paquet d'interface graphique. Ce module ne sert qu'au sélecteur de fichier du coffre, il est donc désormais optionnel : sans `tkinter` et sans chemin KDBX configuré, l'ouverture du coffre journalise une erreur et rend la main, au lieu de casser l'import de tous les scripts de pilotage de navigateur +- Le mode sombre repart en fenêtre privée sur un Firefox récent. La permission « Exécuter dans les fenêtres privées » est accordée à l'installation de l'extension, par le champ `allowPrivateBrowsing`, au lieu d'être cochée dans l'interface `about:addons` : Firefox refuse la navigation vers `about:addons` depuis le contexte contenu, et le contexte chrome exige `-remote-allow-system-access`, que geckodriver refuse via les capabilities, si bien que les deux voies vers cette case sont fermées. Un geckodriver qui ignore le champ installe sans la permission au lieu d'interrompre la session ## Retiré diff --git a/CHANGELOG.md b/CHANGELOG.md index 9955a01..b83a662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Transform data anonymises an Odoo database too, the entry asking for the SOURCE rather than the format: an external file, a live database, or a backup zip taken from `image_db/`. A zip is never modified — it is restored into a database, anonymised there, then dumped into a NEW zip by Odoo's own backup, so the dump, the filestore and the manifest are written by the software that will read them back. A live database is modified ITSELF, which is said before the work and not after, and nothing is drawn when the write did not happen: declining otherwise handed back a zip of the untouched database, announced as anonymised. A register records which databases the entry produced, since the server does not - An anonymised number can keep its digit count, on the file anonymiser and on `anonymize.py` alike (`--keep-digits`): 8839 then draws in 1000..9999, so the copy keeps columns of the same width, which an export read by eye or imported into a bounded field asks for. Each value keeps ITS own width and not its column's — 7 stays at one digit where 12 keeps two — and below the unit the rule does not apply, 0.15 having no digit before the point. The option trades one guarantee for another: the measured extent no longer bounds anything, so a rate or a year can leave its range, and the preview SAYS so rather than letting it be found at reimport. Uniqueness is what yields when a band fills up, a duplicate of the same width beating a value of a different one, and the band never exceeds what the type RECEIVING the draw holds, which is not always the column's: an Odoo `integer` over a column PostgreSQL did not create as an integer — what an upgraded base carries — is poured into `numeric`, which has no ceiling, where `integer` stops at 2 147 483 647 and a draw past it aborts the whole write, that write being one transaction - The anonymiser asks four questions instead of eleven, seven of the eleven having had an obvious default. Answering them one by one to arrive at the same place makes prompts be passed by reflex, and a prompt passed by reflex consents to nothing; they now live behind « Advanced options? ». The short path DECLARES what it takes, in sentences rather than in yes/no, since those are decisions taken and not questions whose answer was lost. Macros and charts are asked on both paths: they are not visible in the cells +- `markdown-it-py` is declared as a functional dependency: it renders the language model's markdown to HTML, before sanitising, on the assistant page. It reached the environment transitively through `bandit` → `rich`, a lint tool, so removing a lint dependency removed a portal page's rendering engine ## Changed @@ -47,6 +48,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Three translation keys declared twice are gone, and a check refuses the next one: a repeated key silently overwrites the previous, which had already cost a menu label - `anonymize.py` abstains from a unique numeric column and NAMES it in the report. A draw guarantees nothing on a unique column, and a number has no way out where text has one — appending the id carries uniqueness for text, but would change a number's magnitude. Two rows drawing the same number failed the UPDATE and, the whole run being one transaction, took the entire anonymisation with them. Without the report naming it, an identifying column stays in the clear with nothing saying so - Restoring a backup no longer raises `AttributeError` before running the command: `_monitoring_restore` read `self._execute` where `TODO` sets `self.execute`, and both entries leading there — the local backup and the remote one — were broken. A check refuses the next one: every `self.X` that `TODO` READS must be set by TODO or by one of its bases. Searching the name across `script/todo/` did not see the fault, another object of the package setting `_execute`; it has to be searched in the classes TODO INHERITS from +- The Selenium scripts run on a Python built without `tkinter` — any server with no graphical toolkit package. That module is only needed by the vault's file picker, so it is now optional: with no `tkinter` and no configured KDBX path, opening the vault logs an error and returns, instead of breaking the import of every browser-automation script +- Dark mode works again in a private window on a current Firefox. The « Run in Private Windows » permission is granted when the addon is installed, through the `allowPrivateBrowsing` field, rather than clicked through the `about:addons` interface: Firefox refuses navigation to `about:addons` from the content context, and the chrome context demands `-remote-allow-system-access`, which geckodriver rejects through capabilities, so both routes to that checkbox are closed. A geckodriver that ignores the field installs without the permission instead of killing the session ## Removed From 600dd8639facd6397dde33668bdb92e3e9999473 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Mon, 14 Sep 2026 05:05:52 -0400 Subject: [PATCH 5/5] =?UTF-8?q?[ADD]=20claude=20:=20activer=20le=20plugin?= =?UTF-8?q?=20superpowers=20pour=20le=20d=C3=A9p=C3=B4t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .claude/settings.json est la configuration partagée du dépôt : la clé enabledPlugins active superpowers pour tout contributeur qui ouvre Claude Code ici, sans manipulation de sa part. Le plugin apporte des skills de méthode — idéation, débogage systématique, TDD, revue de code — et un hook SessionStart qui s'exécute au démarrage de chaque session. Aucune version n'est épinglée : chaque poste prend celle que son marketplace a installée. Vérifié sur la 6.3.0, qui porte quatorze skills et ce seul hook. --- EN --- .claude/settings.json is the repository's shared configuration: the enabledPlugins key turns superpowers on for every contributor who opens Claude Code here, with nothing to do on their side. The plugin carries method skills — brainstorming, systematic debugging, TDD, code review — and a SessionStart hook that runs at the start of every session. No version is pinned: each workstation gets whatever its marketplace installed. Checked on 6.3.0, which carries fourteen skills and that one hook. Assisted-by: Claude Opus 5 --- .claude/settings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index b360c69..0b59d95 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,5 +1,8 @@ { "env": { "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1" + }, + "enabledPlugins": { + "superpowers@claude-plugins-official": true } }