diff --git a/script/analyse/analyse_view_custom.py b/script/analyse/analyse_view_custom.py index 7fdb22d..3f1d584 100755 --- a/script/analyse/analyse_view_custom.py +++ b/script/analyse/analyse_view_custom.py @@ -502,8 +502,8 @@ def collect( # et ce qu'il rapporte est fiable. « all » compare toute vue ayant # un arch_fs, ce qui trouve la dérive qu'aucun drapeau ne signale — # une vue réécrite en SQL direct — mais au prix d'un plancher de - # bruit MESURÉ : sur une base 18.0 fraîchement installée, 160 des - # 974 vues à arch_fs diffèrent déjà. read_arch_from_file rend le + # bruit : sur une base 18.0 fraîchement installée, 160 des 974 + # vues à arch_fs diffèrent déjà. read_arch_from_file rend le # XML brut du fichier, alors que la base porte l'arch APRÈS # traitement au chargement : un attribut « groups » est consommé, # un est appliqué. En « all », un @@ -707,7 +707,7 @@ def render(data, verbose=False, top=TOP_DEFAULT, category=None, hints=True): f"🔬 {t('Customised views')} — {data['database']} (Odoo {version}" f"{', ' + t('from a backup') if data.get('source') == 'backup' else ''})", "", - f" {t("Views"):<38}: {data['n_views']}", + f" {t('Views'):<38}: {data['n_views']}", ] for name in CATEGORIES: if counts.get(name): diff --git a/script/analyse/check_migration_quality.py b/script/analyse/check_migration_quality.py index 8c5ee59..835db07 100755 --- a/script/analyse/check_migration_quality.py +++ b/script/analyse/check_migration_quality.py @@ -19,9 +19,8 @@ lecture seule, plutôt que de rejouer quoi que ce soit. Pourquoi pas en démarrant Odoo ------------------------------ Six démarrages coûteraient une heure, écriraient dans les bases et -demanderaient de basculer le checkout à chaque palier. Mesuré : la même -inspection en SQL prend moins d'une demi-seconde par base, et ne touche à -rien. Ce qu'on y perd — les modèles abstraits, les champs calculés — ne +demanderaient de basculer le checkout à chaque palier. La même inspection +en SQL prend moins d'une demi-seconde par base, et ne touche à rien. Ce qu'on y perd — les modèles abstraits, les champs calculés — ne se compare pas d'une version à l'autre de toute façon. Ce qui compte le plus @@ -184,12 +183,12 @@ def version_of(database, dct): # Ce que la migration écrit dans le journal d'une étape autour d'un test : # -# [2026-08-26 03:19:44.166204] $ .venv…/python3 ./script/…/smoke.py -d … -# [2026-08-26 03:19:59.846406] -> 1 -# [2026-08-26 03:19:59.847489] [test] smoke_public_url -> 1 +# [] $ .venv…/python3 ./script/…/smoke.py -d … +# [] -> 1 +# [] [test] smoke_public_url -> 1 # -# Entre le « $ » et le « -> », RIEN : mesuré sur trois exécutions du même -# test, la sortie de l'outil n'est pas capturée. Ce qui explique l'échec +# Entre le « $ » et le « -> », RIEN : la sortie de l'outil n'est pas +# capturée. Ce qui explique l'échec # est donc AVANT, dans ce qu'Odoo écrivait juste avant qu'on le teste — # et c'est pour cela que l'extrait remonte, au lieu de descendre. MARQUEUR_COMMANDE = "] $ " @@ -451,7 +450,7 @@ def table_counts(database): Construite côté serveur puis exécutée d'un bloc : huit cents requêtes séparées coûteraient huit cents allers-retours, là où celle-ci prend - quatre dixièmes de seconde — mesuré sur une base de 890 tables. + quatre dixièmes de seconde sur une base de 890 tables. """ fabrique = run_psql( database, @@ -546,11 +545,11 @@ def inspect(database): etat["field_module"] = {cle: sorted(v) for cle, v in dct_origine.items()} # Lesquels PORTENT une donnée. Un champ `store=false` n'a jamais eu # de colonne : sa disparition ne perd rien, et il pesait jusqu'à 90 % - # du seau « non déclarés par OpenUpgrade ». Mesuré au palier 16 → 17 : - # `__last_update` à lui seul comptait pour 397 des 565. + # du seau « non déclarés par OpenUpgrade ». Au palier 16 → 17, + # `__last_update` compte à lui seul pour 397 des 565. # - # `store` existe de la 12 à la 18 et n'est jamais NULL — vérifié sur - # les sept bases — donc le renseignement est fiable partout. + # `store` existe de la 12 à la 18 et n'y est jamais NULL : le + # renseignement est donc fiable partout. stockes = run_psql( database, "SELECT model || '.' || name FROM ir_model_fields WHERE store", @@ -560,8 +559,8 @@ def inspect(database): # # Une pièce jointe dont `res_field` ne nomme aucun champ vivant est # DÉJÀ illisible : Odoo lève un KeyError en la contrôlant. Ce ne sont - # pas des données, ce sont des débris. Mesuré sur une chaîne 12 → 18 : - # la dette naît aux paliers 13 et 14, reste gelée pendant trois + # pas des données, ce sont des débris. Sur une chaîne 12 → 18, la + # dette naît aux paliers 13 et 14, reste gelée pendant trois # paliers, et la 18 ramasse 452 lignes d'un coup — qui se lisent # alors comme 452 pertes. # @@ -700,8 +699,7 @@ def render_detail(diff, categorie, colour=False): (gagnes, "+", "ok"), ): lignes.append( - f"── {paint(symbole, teinte, colour)} {len(lst)}" - f" {t(categorie)} ──" + f"── {paint(symbole, teinte, colour)} {len(lst)} {t(categorie)} ──" ) lignes.extend(f" {nom}" for nom in lst) lignes.append("") @@ -724,8 +722,7 @@ def render_missing(etat, colour=False, limit=60): return f"✅ {t('every attachment file is present')}" lignes = [ paint( - f"❌ {absents}" - f" {t('attachment files missing from the filestore')}", + f"❌ {absents} {t('attachment files missing from the filestore')}", "fail", colour, ), @@ -1019,8 +1016,8 @@ def overlay_declared( champs["model_gone"].append(cle) continue # AVANT « non analysé » : « sans donnée propre » est une raison - # plus forte que « hors du champ d'OpenUpgrade ». Mesuré au - # palier 16 → 17, le placement avant fait tomber `not_analysed` + # plus forte que « hors du champ d'OpenUpgrade ». Au palier + # 16 → 17, le placement avant fait tomber `not_analysed` # de 181 à 47 sans changer `undeclared` — le seau résiduel se # réduit alors au risque réel : des champs qui AVAIENT des # données, dans des modules dont OpenUpgrade ne peut rien dire. @@ -1256,11 +1253,10 @@ RENAME_RATIO = 0.75 def looks_renamed(un, deux): """Les deux noms se ressemblent-ils assez pour être le même sujet ? - Deux garde-fous ont été essayés et rejetés, mesurés sur une vraie - migration. Le seul nombre de lignes accouplait - `account_account_tag_account_tax_template_rel` à `dms_directory` : les - deux comptaient sept lignes. Un mot commun d'au moins cinq lettres - accouplait `cleanup_purge_wizard_menu` à + Deux garde-fous plus simples ne tiennent pas. Le seul nombre de lignes + accouple `account_account_tag_account_tax_template_rel` à + `dms_directory` dès qu'ils en comptent autant. Un mot commun d'au moins + cinq lettres accouple `cleanup_purge_wizard_menu` à `cleanup_create_indexes_line` — « cleanup » ne dit rien. La ressemblance d'ENSEMBLE tranche : `muk_dms_directory` et @@ -1315,9 +1311,9 @@ SANS_DONNEE_PROPRE = ("id",) # Pourquoi une pièce jointe a disparu — DÉDUIT, jamais déclaré. # # SEMANTIC_MAP ne peut pas porter ceci : elle nomme une TABLE, et la -# cause n'est pas la table, ce sont ces lignes-là. Mesuré sur une chaîne -# 12 → 18 : des 516 lignes parties au palier 18, 452 avaient perdu leur -# champ et 63 leur enregistrement. Une entrée « ir_attachment / pruned » +# cause n'est pas la table, ce sont ces lignes-là. Sur une chaîne 12 → 18, +# des 516 lignes parties au palier 18, 452 ont perdu leur champ et 63 leur +# enregistrement. Une entrée « ir_attachment / pruned » # aurait rangé les 516 sous « perte attendue » — et la 517e avec. ATTACHMENT_KIND = ( ("field_debt", "their field was already gone before this step", "dim"), @@ -1443,8 +1439,9 @@ def render_text(lst_snapshot, colour=None, limit=8): f" ⚠️ {etat['database']} : {t('database not found')}" ) continue + etape = paint(f"{etat['odoo']:<6}", "step", colour) lignes.append( - f" {paint(f'{etat['odoo']:<6}', 'step', colour)}" + f" {etape}" f" {etat['database']:<34}" f" {len(etat['installed']):>4} {t('modules')}" f" · {len(etat['model']):>4} {t('models')}" diff --git a/script/systemd/install_daemon.py b/script/systemd/install_daemon.py index 0369940..ca90639 100755 --- a/script/systemd/install_daemon.py +++ b/script/systemd/install_daemon.py @@ -13,10 +13,10 @@ from pathlib import Path # # Lancé seul, systemd doit EXÉCUTER le fichier, et l'échoue en « 203/EXEC » # dans quatre cas au moins : bit x absent, shebang qui ne résout pas, /home -# monté noexec, SELinux refusant l'execve. Vécu sur openSUSE s390x — le -# processus mourait en 3 ms, sans jamais entrer dans le script, ce qui rend le -# diagnostic très pénible : aucune sortie, et un code qui ressemble à une -# erreur d'application. +# monté noexec, SELinux refusant l'execve. L'échec est alors MUET : le +# processus meurt avant d'entrer dans le script, sans une ligne de sortie, et +# 203 ressemble à une erreur d'application — le diagnostic part donc dans la +# mauvaise direction. # # Passé à bash, run.sh n'est plus qu'une DONNÉE lue : les quatre causes # disparaissent ensemble, y compris noexec et SELinux, qui ne portent que sur @@ -116,10 +116,9 @@ def main(): args.config_name or f"erplibre_{el_user}_{os.path.basename(os.getcwd())}" ) - exec_param = ( - " " - + f" {w_cmd("-d", args.database or "")} {w_cmd("-p", args.port or "")}".strip() - ) + db_param = w_cmd("-d", args.database or "") + port_param = w_cmd("-p", args.port or "") + exec_param = " " + f" {db_param} {port_param}".strip() # Render the systemd service file content unit_content = UNIT_TEMPLATE.format( diff --git a/script/todo/assistant_menu.py b/script/todo/assistant_menu.py index 0852065..7439189 100644 --- a/script/todo/assistant_menu.py +++ b/script/todo/assistant_menu.py @@ -32,6 +32,7 @@ Le dépôt n'a ni pager, ni progression sur place : la sortie s'ajoute ligne à ligne. Une réponse longue se ferme sur une ligne de pied, jamais sur un défilement piloté. """ + from __future__ import annotations import os @@ -386,8 +387,7 @@ class AssistantMenuMixin: choices = [ { "prompt_description": ( - f"{t('Here (127.0.0.1)')}" - f" ({t('11 ports, instant')})" + f"{t('Here (127.0.0.1)')} ({t('11 ports, instant')})" ) }, { @@ -805,9 +805,8 @@ class AssistantMenuMixin: f" {t('Look somewhere else')} · {t('Type an address')}" f" · {t('Carry on with the OpenAI API (key from the vault)')}" ) - print( - f" 💡 {t('A local server: \"ollama serve\" listens on 11434.')}" - ) + astuce = t('A local server: "ollama serve" listens on 11434.') + print(f" 💡 {astuce}") def _llm_server_card(self): """Ce que le serveur en usage annonce savoir faire. @@ -1051,8 +1050,7 @@ class AssistantMenuMixin: if session is None: return commande = ( - f"{shlex.quote(chemin)} --resume" - f" {shlex.quote(session.session_id)}" + f"{shlex.quote(chemin)} --resume {shlex.quote(session.session_id)}" ) if not getattr(self.execute, "cmd_source_default", ""): print(t("No terminal can be opened here. Paste this command:")) @@ -1097,8 +1095,7 @@ class AssistantMenuMixin: 1 for _, verdict, _ in self._llm_apparier(gpts) if verdict == "ok" ) return ( - f"{t('gpt tools')} ({len(gpts)}," - f" {compatibles} {t('compatible')})" + f"{t('gpt tools')} ({len(gpts)}, {compatibles} {t('compatible')})" ) def _llm_apparier(self, gpts): diff --git a/script/todo/qemu_install.py b/script/todo/qemu_install.py index 5daf1ac..7bc009e 100644 --- a/script/todo/qemu_install.py +++ b/script/todo/qemu_install.py @@ -506,10 +506,12 @@ class QemuInstallMixin: # ERREUR, alors que son étape finale écrit encore l'autorité, les # variables et le sudoers : attendre l'unité, faute de quoi elles # arrivent après cette session, qui vivra sans elles. - + attente_cloud_final() + "; " + + attente_cloud_final() + + "; " # Les variables du cache sont écrites par cloud-init PENDANT # l'attente : cette session, ouverte avant, ne les a pas reçues. - + cache_env_reload() + "; " + + cache_env_reload() + + "; " # ICI, et nulle part avant. Le faisceau que ces exports désignent # est écrit par cloud-init lui aussi : mesuré sur une VM, la # session ssh est acceptée une seconde avant qu'il existe, donc @@ -599,31 +601,28 @@ class QemuInstallMixin: # Miroirs openSUSE préférés, du plus proche au dernier recours. Le # redirecteur officiel n'est PAS géographique pour cette distribution : - # mesuré depuis Montréal sur les métadonnées oss s390x (15 Mo), - # download.opensuse.org met 23,8 s — il sert depuis l'Europe — contre - # 2,7 s pour mirrors.rit.edu. Les trois familles dnf, elles, choisissent - # déjà un miroir canadien toutes seules ; rien à faire de ce côté. + # download.opensuse.org sert depuis l'Europe, là où un miroir + # nord-américain répond en une fraction du temps sur les mêmes + # métadonnées. Les trois familles dnf, elles, choisissent déjà un miroir + # proche toutes seules ; rien à faire de ce côté. # # Chaque miroir est SONDÉ sur le chemin de l'architecture ET du produit # courants, puis le premier qui répond gagne. C'est nécessaire : aucun ne - # réplique tout. Relevé le 2026-08-12 — - # csclub Leap oui, Tumbleweed non (404) - # rit.edu zsystems oui ; injoignable ce jour-là (curl 7) - # leaseweb Tumbleweed x86_64 et Leap oui, ports zsystems non - # D'où plusieurs entrées plutôt qu'une : avec la seule rit.edu, sa panne - # renvoyait tout le monde sur download.opensuse.org, servi d'Europe. - # Ordonnées par proximité de Montréal. Aucun sondage concluant : on garde - # les dépôts de l'image, donc le comportement d'avant. + # réplique tout — l'un sert Leap mais pas Tumbleweed, l'autre les ports + # zsystems mais pas x86_64. D'où plusieurs entrées plutôt qu'une : avec un + # seul miroir, sa panne renvoie tout le monde sur le redirecteur, servi + # d'Europe. Aucun sondage concluant : on garde les dépôts de l'image, + # donc le comportement d'avant. _QEMU_ZYPPER_MIRRORS = ( "https://mirror.csclub.uwaterloo.ca/opensuse", "https://mirrors.rit.edu/opensuse", "https://mirror.us.leaseweb.net/opensuse", ) - # Miroirs Arch canadiens, du plus rapide au suivant. Mesuré depuis - # Montréal sur extra.db : quantum5 2,0 s, xenyth 7,1 s, contre 8,0 s pour - # geo.mirror.pkgbuild.com — le miroir « géographique » officiel n'est donc - # pas le meilleur ici. Arch n'est proposé qu'en amd64 dans le catalogue, + # Miroirs Arch canadiens, du plus rapide au suivant. Le miroir + # « géographique » officiel, geo.mirror.pkgbuild.com, n'est pas le plus + # rapide depuis l'Amérique du Nord : d'où une liste explicite plutôt que + # lui. Arch n'est proposé qu'en amd64 dans le catalogue, # et ces deux-là ne servent que x86_64 (Arch Linux ARM a ses propres # miroirs) : la garde d'architecture le dit quand même. _QEMU_PACMAN_MIRRORS = ( @@ -1219,13 +1218,13 @@ class QemuInstallMixin: # couvre les quatre gestionnaires (Arch l'a dans extra, Debian et Ubuntu ne # l'ont qu'en snap — coupé ici —, Fedora et openSUSE pas du tout). # - # La ligne COMMUNITY, et non le produit unifié. Mesuré dans une VM : - # « code=PCC&latest » sert maintenant pycharm-2025.3, le build unifié, qui + # La ligne COMMUNITY, et non le produit unifié. + # « code=PCC&latest » sert le build unifié, qui # s'arrête sur sa licence — son journal dit « NoValidIdeLicense » puis # « Get licenses: request requires authentication », et le projet ne # s'ouvre jamais. Aucune ouverture, donc aucun .idea, donc rien à - # configurer ensuite. Community ne demande aucun compte, et elle est - # toujours publiée et corrigée : 2025.2.6.2 date du 2026-07-29. + # configurer ensuite. Community ne demande aucun compte, et elle reste + # publiée et corrigée. # # Aucun numéro figé ici : on prend la plus récente archive # « pycharm-community- » du flux officiel des versions, pour @@ -1248,7 +1247,7 @@ class QemuInstallMixin: # INDÉPENDANTS, l'un ne se déduit pas de l'autre, et le flux updates.xml de # Google ne publie ni l'un ni l'autre. On lit donc l'URL sur la page # officielle, qui la porte en clair, et on retombe sur celle-ci si la page - # change de forme. Relevée et vérifiée (HTTP 200) le 2026-08-17. + # change de forme. Ce repli est une URL figée : elle vieillit. _QEMU_ANDROID_URL = ( "https://dl.google.com/dl/android/studio/ide-zips/2026.1.3.8/" "android-studio-quail3-patch1-linux.tar.gz" @@ -1310,6 +1309,12 @@ class QemuInstallMixin: Tout le bloc est gardé : un IDE qui ne s'installe pas ne doit pas faire échouer l'installation d'ERPLibre, qui elle a duré une heure.""" el_dir = self._qemu_install_dir(prod) + # Hors de la f-string : une expression sur deux lignes dans ses + # accolades ne parse qu'à partir de 3.12 (PEP 701). + idea_note = t( + "open the project once and close PyCharm; the .idea " + "it writes is what the install configures" + ) return ( f'echo "== {t("Installing PyCharm (long)")} =="; ' "{ " @@ -1378,8 +1383,7 @@ class QemuInstallMixin: # PyCharm n'a évidemment jamais ouvert le dépôt. + f'echo " {t("PyCharm installed:")} /opt/pycharm ' f'({t("command")} pycharm, {t("project")} {el_dir})"; ' - f'echo " {t("open the project once and close PyCharm; the .idea " - "it writes is what the install configures")}"; ' + f'echo " {idea_note}"; ' f'}} || echo " ⚠ {t("PyCharm not installed (see above)")}"; ' ) @@ -1676,8 +1680,9 @@ class QemuInstallMixin: # Aucune n'est empaquetée par une distribution : on passe donc par le site. # # L'archive dépend de la version de GNOME Shell, et ce n'est pas une - # précaution de principe : mesuré le 2026-08-17, le même point d'entrée - # sert gTile v59 pour GNOME 46, v62 pour GNOME 48 et v52 pour GNOME 3.38. + # précaution de principe : le même point d'entrée sert une archive + # différente selon la version — gTile v59 pour GNOME 46, v62 pour + # GNOME 48, v52 pour GNOME 3.38. # Une URL figée poserait donc, tôt ou tard, une archive faite pour une # autre version. # @@ -2097,8 +2102,8 @@ class QemuInstallMixin: # peut rendre 0 sans avoir rien produit. # DEUX emplacements, et il faut les deux. Avec une ABI injectée, # AGP écrit dans « intermediates/apk/debug » et non dans - # « outputs/apk/debug » : mesuré, une compilation RÉUSSIE était - # rapportée « aucun APK produit » parce que je ne regardais que le + # « outputs/apk/debug » : une compilation RÉUSSIE se rapporte + # « aucun APK produit » dès que le contrôle ne regarde que le # second. Un contrôle qui cherche au mauvais endroit ne vaut pas # mieux que pas de contrôle. f"apk=$(ls {el_dir}/mobile/erplibre_home_mobile/android/app/build"