Commit graph

928 commits

Author SHA1 Message Date
f20e21b7d2 [ADD] cache qemu : nettoyage par âge et plafond de taille
Le cache ne diminuait jamais de lui-même et vit sur le disque de
l'orchestrateur : un parc qui déploie chaque jour finissait par le remplir,
sans que rien ne le signale. Deux règles existent désormais, éteintes par
défaut et réglées depuis le menu : l'âge du dernier usage, et un plafond de
taille qui efface le moins récemment servi jusqu'à tenir dessous, objets et
dépôts git confondus. Un minuteur systemd quotidien les applique, et une
réinstallation garde les valeurs choisies. Le menu montre d'abord ce qui
partirait.

--- EN ---

The cache never shrank by itself and lives on the orchestrator's disk: a site
deploying every day filled it up with nothing to say so. Two rules now exist,
off by default and set from the menu: the age of last use, and a size ceiling
that erases the least recently served until the cache fits under it, objects
and git mirrors alike. A daily systemd timer applies them, and a reinstall
keeps the chosen values. The menu first shows what would go.

Assisted-by: Claude Opus 5
2026-09-16 05:36:13 -04:00
4b60dcba30 [ADD] déploiement qemu : couper l'audit npm, traduire l'exception
L'audit de npm interroge un service qu'aucun cache ne rejoue : amont coupé,
il échoue à chaque installation sans rien vérifier. Une VM déployée hors
ligne reçoit donc NPM_CONFIG_AUDIT=false, écrit dans /etc/environment et
relu après cloud-init ; une VM en ligne garde son audit. Le retrait d'une
exception du détournement parle désormais la langue du menu, son message
allant à l'erreur standard tandis que la sortie standard garde les règles
que nft lit, qui ne se traduisent jamais.

--- EN ---

The npm audit queries a service no cache can replay: upstream cut, it fails
every install without checking anything. A VM deployed offline therefore gets
NPM_CONFIG_AUDIT=false, written in /etc/environment and read back after
cloud-init; an online VM keeps its audit. Removing a redirection exception
now speaks the menu's language, its message going to standard error while
standard output keeps the rules nft reads, which are never translated.

Assisted-by: Claude Opus 5
2026-09-16 05:36:13 -04:00
90ef1d24e7 [ADD] cache qemu : revalider les index, ranger chaque représentation
Sur une installation complète, les index pip, les métadonnées npm et le
bundle de repo pesaient environ 110 Mo par VM, repris en entier à chaque
fois. Un index déjà gardé part désormais avec son ETag : l'amont juge chaque
requête, et un « 304 » sert le corps depuis le disque. Sous « Vary: Accept »,
chaque représentation est rangée à part, faute de quoi deux clients qui
alternent les retéléchargent tour à tour. Les métadonnées PEP 658 de pip,
nommées « .whl.metadata », sont figées comme l'archive qu'elles décrivent.

--- EN ---

On a full install the pip indexes, npm metadata and repo bundle weighed about
110 MB per VM, taken whole every time. An index already kept now leaves with
its ETag: upstream judges every request, and a « 304 » serves the body from
disk. Under « Vary: Accept » each representation is stored apart, without
which two clients alternating between them re-download each in turn. The
PEP 658 metadata of pip, named « .whl.metadata », are as immutable as the
archive they describe.

Assisted-by: Claude Opus 5
2026-09-16 05:36:13 -04:00
cd2b8266d4 [FIX] requirement erplibre : déclarer openpyxl, sauter sans lui
Three Transform data tests wrote an Excel workbook with openpyxl, which the
.venv.erplibre requirements never declared, so the unit suite reported three
errors on every install. openpyxl is now declared there. The tests skip when it
cannot be imported in their own interpreter, since a venv built before this
line still lacks it; the dedicated engine venv is not the right probe, as the
tests write their workbook in-process. Checked: 707 Transform tests pass, and
with openpyxl made unimportable the three are skipped, not errors.

--- FR ---

Trois tests de Transform data écrivaient un classeur Excel avec openpyxl, que
les dépendances de .venv.erplibre ne déclaraient pas : la suite unitaire
rendait trois erreurs sur toute installation. openpyxl y est désormais
déclaré. Les tests se sautent quand il ne s'importe pas dans leur interpréteur,
un venv bâti avant cette ligne ne l'ayant toujours pas ; le venv dédié du
moteur n'est pas la bonne sonde, les tests écrivant leur classeur sur place.
Vérifié : 707 tests de Transform passent, et openpyxl rendu inimportable, les
trois sont sautés et non en erreur.

Assisted-by: Claude Opus 5
2026-09-16 05:36:13 -04:00
fb2f71477e [ADD] cache qemu : messages du binaire en anglais ou en français
Le binaire ne parlait que français alors que le menu suit EL_LANG : un
opérateur anglophone lisait un statut qu'il ne comprenait pas. Chaque message
humain passe par T(), dont la clé EST le texte français, et un catalogue
anglais le traduit ; la langue vient de --lang, puis d'EL_LANG, le français
restant le défaut. Le menu demande le français pour ce qu'il RELIT, ses
motifs ne connaissant que cette langue. Un test refuse un message sans
traduction, une traduction orpheline et un verbe de format qui diverge.

--- EN ---

The binary spoke French only while the menu follows EL_LANG: an English
reader got a status he could not read. Every human message goes through T(),
whose key IS the French text, and an English catalogue translates it; the
language comes from --lang, then EL_LANG, French remaining the default. The
menu asks for French for what it READS BACK, its patterns knowing that
language alone. A test refuses a message without translation, an orphan
translation, and a format verb that drifts.

Assisted-by: Claude Opus 5
2026-09-16 02:59:01 -04:00
977722b7cd [ADD] proxmox deploy : accélération 3D, bibliothèques sondées et posées
A Proxmox VM always used the serial console as its display, so nothing inside
it could render. The form offers 3D: the VM gets « --vga virtio-gl » and the
account joins the render and video groups in the guest, which « qm set »
cannot write; without them GL falls back to software while VIRGL reports
success. The box is offered only where the host has a render node, VIRGL, GL
and EGL, Proxmox refusing to start the VM otherwise once its disk is written.
What is missing is named with its package, and a button installs it, then
reads the host again. Covered by test/test_proxmox_form.py.

--- FR ---

Une VM Proxmox prenait toujours la console série pour affichage : rien ne
pouvait y rendre. Le formulaire offre la 3D : la VM reçoit « --vga virtio-gl »
et le compte entre dans les groupes render et video dans l'invité, ce que
« qm set » ne sait pas écrire ; sans eux, GL retombe en logiciel alors que
VIRGL annonce un succès. La case n'est offerte que si l'hôte a un nœud de
rendu, VIRGL, GL et EGL, Proxmox refusant sinon de démarrer la VM une fois son
disque écrit. Ce qui manque est nommé avec son paquet, et un bouton le pose
puis relit l'hôte. Couvert par test/test_proxmox_form.py.

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00
01c5290e30 [FIX] qemu install : poser mise, pyenv et starship, verdict par outil
Without pipefail, « curl … | sh » returns the status of sh, 0 on empty input:
a failed download passed for an install, its fallback never ran, and the
failure surfaced later as « command not found ». mise and pyenv are downloaded
into a mktemp file, then run, and a check refuses any fallback behind a pipe
without pipefail. starship's installer runs as root under a root timeout, never
reaching the « sudo -v » that sudo-rs refuses, and its shell hook is guarded.
Each tool then reports the version found or « not installed », without
tripping « set -e ».

--- FR ---

Sans pipefail, « curl … | sh » rend le statut de sh, 0 sur une entrée vide :
un téléchargement raté passait pour une pose, son repli ne tournait jamais, et
l'échec se lisait plus loin sous « command not found ». mise et pyenv sont
téléchargés dans un fichier tiré par mktemp, puis exécutés, et un contrôle
refuse tout repli derrière un tube sans pipefail. L'installateur de starship
tourne en root sous un délai root, sans atteindre le « sudo -v » que sudo-rs
refuse, et son crochet de shell est gardé. Chaque outil dit ensuite la version
trouvée ou « non installé », sans faire tomber « set -e ».

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00
7e3d0d1f88 [IMP] menu proxmox : une icône à chaque entrée qui n'en portait pas
Ten of the eighteen entries carried no pictogram, and the eye picks one out
of such a list before it reads. Each takes the icon its siblings already use
elsewhere in the tool — 📋 lists, 🧹 cleans, 📊 measures, 🔍 previews, 📥
downloads, 🗂 catalogues, 🖥 tunnels a desktop — so the same picture keeps
meaning the same thing from one menu to the next. A table in the tests holds
the pairing, in both languages, and checks the menu still shows the entries
it freezes.

--- FR ---

Dix des dix-huit entrées ne portaient aucun pictogramme, et l'œil s'y repère
avant de lire. Chacune reçoit l'icône que ses semblables portent déjà
ailleurs dans l'outil — 📋 liste, 🧹 nettoie, 📊 mesure, 🔍 prévisualise, 📥
télécharge, 🗂 catalogue, 🖥 ouvre un bureau —, si bien qu'une même image
garde le même sens d'un menu à l'autre. Un tableau dans les tests fige le
couple, dans les deux langues, et vérifie que le menu affiche encore les
entrées qu'il fige.

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00
1d891e6e33 [IMP] tests longs : confirmer avant de créer ou de détruire des machines
The long-test menu printed the command and ran it at once: a mistyped digit
created real VMs, and « --detruire » removes machines with their disks. The
command stays printed, since it is what makes the question answerable, and the
prompt follows it. Warning and question are chosen from the arguments, so a
destruction is never confirmed with the words of a launch. A dry run or a
report creates nothing and asks nothing. Covered by
test/test_longtest_confirm.py.

--- FR ---

Le menu des tests longs affichait la commande et l'exécutait aussitôt : un
chiffre tapé de travers créait de vraies VM, et « --detruire » efface des
machines avec leurs disques. La commande reste affichée, c'est elle qui rend
la question répondable, et l'invite la suit. L'avertissement et la question se
choisissent sur les arguments : une destruction n'est jamais confirmée avec
les mots d'un lancement. Un plan à blanc ou un rapport ne crée rien et ne
demande rien. Couvert par test/test_longtest_confirm.py.

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00
814251f19f [FIX] déploiement qemu : amorcer Fedora en BIOS, sa chaîne UEFI se fige
Aucune VM Fedora ne démarrait : pas de console, pas de bail DHCP, une machine
« en cours d'exécution » qui ne fait rien. Le micrologiciel charge et DÉMARRE
le chargeur — il l'annonce — puis se fige sans écrire un octet sur le disque.
La même image en BIOS démarre son noyau : ce n'est donc ni l'image, saine, ni
sa partition EFI, dont le chemin de repli est bien là. Ni l'entropie ni la
machine q35 n'y changent rien. Une table nomme les distributions concernées et
« --bios » l'emporte toujours, pour l'hôte qui n'a pas OVMF.

--- EN ---

No Fedora VM would start: no console, no DHCP lease, a machine "running" that
does nothing. The firmware loads and STARTS the loader — it says so — then
freezes without writing a byte to disk. The same image under BIOS boots its
kernel: so it is neither the image, which is sound, nor its EFI partition,
whose fallback path is present. Neither entropy nor the q35 machine changes
anything. A table names the distributions concerned, and "--bios" always
wins, for the host that has no OVMF.

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00
2892606690 [FIX] déploiement qemu : nom d'hôte valide, fuseau connu de l'invité
Deux réglages que cloud-init applique au premier démarrage, et qui échouaient
tous les deux SANS arrêter le déploiement. Un nom d'hôte n'accepte ni souligné
ni point, là où un nom de domaine libvirt les tolère : la VM gardait le nom
générique de son image. Un alias de fuseau hérité — la forme que plusieurs
distributions récentes ont reléguée à un paquet séparé — faisait marquer
l'exécution de cloud-init en erreur et laissait la machine en UTC, ce qui ne
se voit qu'après coup sur des horodatages à +0000. Le nom est nettoyé, le
fuseau rendu canonique par la table d'alias de tzdata.

--- EN ---

Two settings cloud-init applies at first boot, both of which failed WITHOUT
stopping the deployment. A hostname accepts neither underscore nor dot, where
a libvirt domain name tolerates them: the VM kept its image's generic name. A
legacy timezone alias — the form several recent distributions moved to a
separate package — marked the cloud-init run as failed and left the machine in
UTC, which only shows up later on +0000 timestamps. The name is cleaned, the
timezone made canonical through tzdata's alias table.

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00
a7706adf3d [ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.

--- FR ---

Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.

Assisted-by: Claude Opus 5
2026-09-14 16:46:15 -04:00
ddde76f0cf [FIX] qemu deploy : l'unité de l'agent invité ne finit plus en échec
Dans le script du service qui pose qemu-guest-agent, « && » et « || » se
lisent à égalité de gauche à droite : une pose apt réussie enchaînait sur
« dnf install », puis sur le « command -v » d'un gestionnaire absent, qui
rend 127 sous dash. Le service finissait en échec après avoir posé
l'agent. Chaque branche est désormais entre accolades. Vérifié : le script
extrait de la vraie configuration cloud-init rend 0 avec apt-get, dnf ou
pacman factice.

--- EN ---

In the script of the service installing qemu-guest-agent, "&&" and "||"
have equal precedence, left to right: a successful apt install went on to
"dnf install", then to the "command -v" of a missing manager, which
returns 127 under dash. The service ended in failure after installing the
agent. Each branch is now braced. Checked: the script extracted from the
real cloud-init configuration returns 0 with a fake apt-get, dnf or
pacman.

Assisted-by: Claude Opus 5
2026-09-14 16:32:42 -04:00
f6cc0bfc53 [FIX] qemu install : attendre le verrou apt d'apt-daily et de l'agent
A desktop install repeated « Could not get lock » for minutes, then could run
on an index never refreshed. Disabling the apt-daily timer does not interrupt
the apt-get it already started, so its service is stopped too. The guest agent
is laid by a detached unit that runs its own apt-get update: « cloud-init
status --wait » says done while that lock is still held, so the wait covers
that unit. Retries come back every two seconds, DPkg::Lock::Timeout not
covering the list lock, and an update that never succeeds says so at once.

--- FR ---

Une installation de bureau répétait « Impossible d'obtenir le verrou » pendant
des minutes, puis pouvait tourner sur un index jamais rafraîchi. Désactiver le
minuteur apt-daily n'interrompt pas l'apt-get qu'il a lancé : son service est
donc arrêté aussi. L'agent invité est posé par une unité détachée qui fait son
propre apt-get update : « cloud-init status --wait » dit « done » pendant que
ce verrou est tenu, et l'attente couvre donc cette unité. La reprise repasse
toutes les deux secondes, DPkg::Lock::Timeout ne couvrant pas le verrou des
listes, et un update qui n'aboutit pas le dit sur-le-champ.

Assisted-by: Claude Opus 5
2026-09-14 16:32:42 -04:00
41bb10ab65 [FIX] script todo : lire « default » là où le fichier l'écrit ainsi
L'étiquette « - Default » ne paraissait jamais, ni au choix de version ni au
choix d'environnement : les deux lectures interrogeaient « Default » avec une
majuscule, quand conf/supported_version_erplibre.json écrit « default ».
dict.get rend None sur une clé absente, sans rien dire — la version par
défaut passait donc pour une version ordinaire, et rien ne le signalait.

Le garde-fou ne vérifie pas ce seul cas : il apparie les deux côtés, et toute
clé que le menu interroge doit exister dans le fichier. Vérifié : 4 tests,
dont deux tombent si la majuscule revient.

--- EN ---

The « - Default » label never appeared, neither on the version choice nor on
the environment one: both reads asked for « Default » with a capital, while
conf/supported_version_erplibre.json writes « default ». dict.get returns None
on a missing key without a word — the default version therefore looked like an
ordinary one, and nothing said otherwise.

The guard does not check that single case: it pairs both sides, and every key
the menu asks for must exist in the file. Checked: 4 tests, two of which fail
if the capital comes back.

Assisted-by: Claude Opus 5
2026-09-14 16:31:01 -04:00
c98c69db1c [IMP] hygiène : lire aussi les commentaires Go
L'outil n'inspectait que « # » : sondé avec des motifs interdits, il restait
muet sur un « .go » et sortait 0. Du Go étant entré dans le dépôt, ses
commentaires n'étaient relus par personne d'autre que leur auteur, alors que
la convention vaut pour tout commentaire.

Trois pièges, et le premier est celui qui compte : « // » ouvre un
commentaire SAUF dans une chaîne, et « https:// » en porte deux. La chaîne
brute entre accents graves n'échappe rien, et « /* … */ » couvre plusieurs
lignes. Vérifié : 8 cas neufs, dont l'URL qui ne doit rien ouvrir.

--- EN ---

The tool only inspected « # »: probed with forbidden patterns, it stayed
silent on a « .go » and exited 0. Now that Go has entered the repository, its
comments were read by nobody but their author, while the convention covers
every comment.

Three traps, and the first is the one that matters: « // » opens a comment
EXCEPT inside a string, and « https:// » carries two of them. The raw string
between backticks escapes nothing, and « /* … */ » spans several lines.
Checked: 8 new cases, including the URL that must open nothing.

Assisted-by: Claude Opus 5
2026-09-14 16:31:01 -04:00
aff0d50972 [FIX] selenium : accorder le privé aux extensions sans 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 : 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
2026-09-14 05:10:46 -04:00
c15d97f646 [FIX] selenium : rendre tkinter optionnel, il ne sert qu'au coffre
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
2026-09-14 05:10:46 -04:00
04b9717e96 [ADD] transform data : anonymiser un fichier externe ou une base Odoo
Partager un fichier ou une base demandait d'anonymiser à la main,
colonne par colonne, ou de ne pas partager. L'entrée ouvre un fichier
externe — Excel, Access, CSV, XML, JSON — ou une base Odoo, dit ce
qu'elle porte, puis en tire une copie. L'original n'est jamais touché.

Deux règles gouvernent ce qui sort. Une colonne n'est laissée intacte
que sur son CONTENU mesuré, hors onze étiquettes structurelles : décider
sur l'étiquette recopiait les colonnes les plus identifiantes en les
annonçant protégées. Et la copie est relue octet par octet, un classeur
portant de la donnée en une douzaine d'endroits hors des cellules.

Vérifié : 5 748 tests — 13 rouges, déjà rouges sur master.

--- EN ---

Sharing a file or a database meant anonymising by hand, column by
column, or not sharing. The entry opens an external file — Excel,
Access, CSV, XML, JSON — or an Odoo database, says what it holds, then
draws a copy of it. The original is never touched.

Two rules govern what comes out. A column is left alone on its measured
CONTENT, bar eleven structural labels: deciding on the label copied the
most identifying columns verbatim while announcing them as protected.
And the copy is reread byte by byte, a workbook holding data in a dozen
places that are not cells.

Checked: 5,748 tests — 13 red, already red on master.

Assisted-by: Claude Opus 5
2026-09-11 00:06:39 -04:00
c0b24925bf [ADD] hygiène : signaler un nom de machine pleinement qualifié
L'outil voyait les adresses, les courriels et les chemins de compte, mais pas
les NOMS — la classe que la règle interdit au même titre, et celle qu'un
assistant qui parcourt le réseau multiplie. Le rapport annonçait « 0
identifiant » sur un fichier qui en portait un.

Un signal à relire, jamais une trouvaille : un nom d'hôte NU ne se distingue
mécaniquement ni d'un mot ordinaire ni du nom d'un logiciel, donc seule la
forme qualifiée se reconnaît, contre une liste FERMÉE de suffixes. En sortent
l'en-tête de copyright, les domaines de la RFC 2606, et toute adresse portée
par une URL. Vérifié : 54 tests, et 21 signaux sur le dépôt, tous des miroirs.

--- EN ---

The tool saw addresses, e-mails and account paths, but not NAMES — the class
the rule forbids just as much, and the one an assistant sweeping the network
multiplies. The report announced "0 identifying" on a file that carried one.

A signal to re-read, never a finding: a BARE host name is mechanically
indistinguishable from an ordinary word or a piece of software, so only the
qualified form is recognised, against a CLOSED list of suffixes. Out of it go
the copyright header, the RFC 2606 domains, and any address carried by a URL.
Checked: 54 tests, and 21 signals across the repository, all mirrors.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
19410e5b73 [FIX] todo : une invite n'écrit plus son deux-points deux fois
`click.prompt` ajoute son propre « : » à l'étiquette qu'on lui donne, et
quinze étiquettes le portaient déjà. Le menu le plus vu du logiciel demandait
donc « Commande :: », et sept invites de déploiement à distance affichaient
un deux-points suivi d'un autre.

Le suffixe est désormais explicite partout où l'étiquette ponctue elle-même,
et sa valeur se déduit de l'étiquette : rien quand elle porte déjà l'espace
qui suit, un espace quand le deux-points est nu. Un contrôle lit l'arbre et
résout les DEUX langues, le français mettant une espace avant le deux-points
là où l'anglais n'en met pas — chercher dans le texte du code ne verrait que
la clé anglaise.

--- EN ---

`click.prompt` adds its own ":" to the label it is given, and fifteen labels
already carried one. The most-seen menu of the software therefore asked
"Command:: ", and seven remote-deployment prompts showed a colon followed by
another.

The suffix is now explicit wherever the label punctuates itself, and its value
follows from the label: nothing when it already carries the trailing space, a
space when the colon is bare. A check reads the tree and resolves BOTH
languages, French putting a space before the colon where English does not —
searching the code text would only see the English key.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
ea6391a781 [FIX] asyncio : rendre --max_process lançable sur Python 3.10 et plus
Le drapeau `--max_process` de deux scripts passe par ce pool, et il ne
tournait plus du tout. Deux retraits d'API le traversaient : `loop=` a quitté
`asyncio.wait` en 3.10, où le passer lève un TypeError, et
`asyncio.get_event_loop()` lève hors d'une loop en marche depuis 3.14, où il
ne faisait qu'avertir — donc la classe n'était même plus instanciable, alors
que l'aide annonce toujours l'option.

La loop n'est plus créée à l'instanciation mais à l'exécution, et `close` ne
ferme que celle que la classe a ouverte : une loop reçue en argument
appartient à l'appelant, qui compte encore dessus. Vérifié : 7 tests sur de
vraies coroutines, et les deux appels d'origine lèvent toujours à part.

--- EN ---

The `--max_process` flag of two scripts goes through this pool, and it no
longer ran at all. Two API removals crossed it: `loop=` left `asyncio.wait` in
3.10, where passing it raises a TypeError, and `asyncio.get_event_loop()`
raises outside a running loop since 3.14, where it merely warned before — so
the class was not even constructible, while the help still advertises the
option.

The loop is no longer created at construction but at run time, and `close`
only closes the one the class opened: a loop received as an argument belongs to
the caller, who still counts on it. Checked: 7 tests on real coroutines, and
both original calls still raise on their own.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
d335293d85 [FIX] i18n : retirer trois clés déclarées deux fois, et l'interdire
Trois clés étaient déclarées deux fois dans un littéral de trois mille
entrées. Rien n'avertit et rien ne lève : la seconde gagne, et le prix s'est
déjà payé en étiquette de menu, la traduction lue n'étant pas celle qu'on
venait d'écrire au premier endroit. Les trois portaient la même traduction des
deux côtés, donc aucun écran ne change ; c'est le piège qui part.

Le contrôle passe de « pas plus de trois » à « aucune », et il vit désormais
avec les tests d'internationalisation plutôt qu'avec ceux d'un menu. Il lit
l'ARBRE : une fois le dictionnaire construit, le doublon a déjà disparu et il
n'y a plus rien à demander.

--- EN ---

Three keys were declared twice in a literal of three thousand entries.
Nothing warns and nothing raises: the second wins, and the price was already
paid on a menu label, the translation read not being the one just written at
the first place. All three carried the same translation on both sides, so no
screen changes; what goes is the trap.

The check moves from "no more than three" to "none at all", and it now lives
with the internationalisation tests rather than with a menu's. It reads the
TREE: once the dictionary is built the duplicate is already gone, and there is
nothing left to ask.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
314ab134d7 [FIX] todo : onze menus sans miette, et un contrôle qui les voit
Onze sous-menus n'avaient pas d'étiquette de fil d'Ariane. Leur segment
manquait à l'en-tête, et la télémétrie les traitait comme des COMMANDES : ils
apparaissaient en feuille, sous leur nom de méthode brut.

Le contrôle qui devait l'attraper cherchait dans la table de répartition du
menu Exécution, donc il ne voyait que les sous-menus atteints depuis là — un
menu ouvert depuis ailleurs, ou défini dans un mixin, n'était jamais examiné.
Il cherche maintenant par ce que le code APPELLE, dans tout le paquet, et
trois exemptions périmées ont disparu avec lui. Cinq écrans restent exemptés
et disent pourquoi : ce sont des actions qui posent une question.

--- EN ---

Eleven submenus had no breadcrumb label. Their segment was missing from the
header, and telemetry treated them as COMMANDS: they showed up as leaves,
under their raw method name.

The check meant to catch this looked in the Execute menu's dispatch table, so
it only saw submenus reached from there — a menu opened from elsewhere, or
defined in a mixin, was never examined. It now searches by what the code
CALLS, across the whole package, and three stale exemptions went with it. Five
screens stay exempt and say why: they are actions that ask a question.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
2df3be2df8 [FIX] ssh config : une seule décision sur ce qu'est un nom de machine
Trois lecteurs de ~/.ssh/config tranchaient la même question autrement : l'un
comparait le mot-clé avec la casse, un autre exigeait un espace là où une
tabulation est légale, un troisième laissait passer le motif nié `!nom`. Un
alias déclaré « host » en minuscules était donc vu par l'un et invisible aux
deux autres, ce qui se lit comme une panne intermittente de la découverte.

La décision passe dans un module qui ne lit aucun fichier. Il rend None sur ce
qui n'est pas une déclaration et une liste — possiblement vide — sur ce qui en
est une : « Host * » ne nomme aucune machine mais reste une déclaration, sinon
ses directives se rattachent au bloc précédent. Vérifié : 19 tests, et le menu
reconnaît toujours le serveur du réseau par sa source SSH.

--- EN ---

Three readers of ~/.ssh/config settled the same question differently: one
compared the keyword case-sensitively, another required a space where a tab is
legal, a third let the negated pattern `!name` through. An alias declared with
a lowercase "host" was therefore seen by one and invisible to the other two,
which reads as intermittent discovery failure.

The decision moves into a module that reads no file. It returns None for what
is not a declaration and a list — possibly empty — for what is: "Host *" names
no machine yet remains a declaration, otherwise its directives attach to the
previous block. Checked: 19 tests, and the menu still recognises the server on
the network through its SSH source.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
85ac83cc75 [FIX] conventions : retirer un nom d'organisation des exemples
Un nom d'organisation servait d'exemple dans une docstring et dans quatre
tests — nom de base, chemin de compte, préfixe de copie. La règle du dépôt
l'interdit partout hors de `private/`, et ces fichiers suivent le dépôt en
amont : sur un fork rendu public, l'exemple le devient aussi.

Un test le fige pour toujours, et trois des cinq sites servaient justement à
démontrer qu'un chemin de compte ou un nom est refusé. Une valeur inventée
démontre aussi bien, et celle-ci ne paraît nulle part ailleurs. La docstring
perd son exemple : il n'illustrait que « long », et la phrase le dit
maintenant. Le détecteur du dépôt ne voit pas cette classe — il n'annonçait
rien ici.

--- EN ---

An organisation's name served as an example in one docstring and in four
tests — a database name, an account path, a copy prefix. The repository's rule
forbids it anywhere outside `private/`, and these files follow the repository
upstream: on a fork made public, the example becomes public too.

A test freezes it forever, and three of the five sites existed precisely to
demonstrate that an account path or a name is refused. An invented value
demonstrates just as well, and this one appears nowhere else. The docstring
loses its example: it only illustrated "long", which the sentence now says.
The repository's detector does not see this class — it reported nothing here.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
7c83dd1fc8 [FIX] qemu : lire les baux dnsmasq sans invite de mot de passe
La lecture passait par « sudo sh -c cat » sans jamais demander si le privilège
était nécessaire. Elle est appelée une fois par VM pour afficher une liste, et
toutes les trois secondes pendant dix minutes par l'attente d'une VM : l'invite
root tombait donc en boucle au milieu d'un écran, ce qui entraîne à taper un
mot de passe root dans ce qui le demande.

La lecture directe passe d'abord et suffit sur une installation standard, ces
fichiers d'état étant en 0644 là où le « .conf » voisin est en 0600. Le
privilège n'est tenté qu'ensuite, avec « -n », qui échoue au lieu de demander ;
un répertoire interdit rend un glob vide, d'où l'essai même sans chemin trouvé.
Les deux appelants se replient déjà sur une autre source. Vérifié : 12 tests.

--- EN ---

The read went through "sudo sh -c cat" without ever asking whether the
privilege was needed. It is called once per VM to display a list, and every
three seconds for ten minutes while waiting on a VM: the root prompt therefore
landed in a loop in the middle of a screen, which trains someone to type a root
password into whatever asks for it.

The direct read comes first and suffices on a standard install, those status
files being 0644 where the neighbouring ".conf" is 0600. The privilege is only
tried afterwards, with "-n", which fails instead of asking; a forbidden
directory returns an empty glob, hence the attempt even with no path found.
Both callers already fall back to another source. Checked: 12 tests.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
eb28952d6c [FIX] execute : caviarder les clés d'API et les jetons Bearer
Le filtre ne connaissait que trois noms de variable — mot de passe, secret,
jeton — donc `OPENAI_API_KEY=` partait en clair dans le terminal, dans les
journaux et dans toute sortie CI qui les capture. Un jeton d'en-tête échappait
aux deux règles par construction : il ne porte ni nom d'option ni nom de
variable, il suit le mot « Bearer ».

Le filtre couvre maintenant `API_KEY` côté variables et `Authorization:
Bearer|Basic` côté en-têtes, sans casse, la valeur allant jusqu'au premier
blanc. Il protège au même titre la restauration de base, qui l'appelle sur
six sorties. Reste le dernier rempart et non le premier : argv est lisible par
tout compte de la machine, où aucun caviardage n'atteint.

--- EN ---

The filter knew only three variable names — password, secret, token — so
`OPENAI_API_KEY=` went out in the clear to the terminal, to the logs and to
any CI output capturing them. A header token escaped both rules by
construction: it carries neither an option name nor a variable name, it
follows the word "Bearer".

The filter now covers `API_KEY` on the variable side and `Authorization:
Bearer|Basic` on the header side, case-insensitively, the value running to the
first blank. It protects database restore just as much, which calls it on six
outputs. It stays the last line of defence, not the first: argv is readable by
every account on the machine, where no redaction reaches.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
8fc026c5c7 [ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.

Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.

Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.

--- EN ---

The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.

Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.

Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.

Assisted-by: Claude Opus 5
2026-09-09 07:35:15 -04:00
9ac66f97f4 [REM] vpn : retirer la recherche de préréglage que personne n'appelle
`presets.load(identifiant)` n'avait aucun appelant hors de son propre
test : une interface publique inventée pour un besoin qui ne s'est pas
présenté, et un test qui n'existait que parce qu'elle existait. Le menu
liste par `load_all` et choisit par rang.

Elle reviendra le jour où un CLI prendra un préréglage par son nom, avec
le test que ce besoin justifiera. Vérifié : 48 tests des préréglages
verts, et aucun appel résiduel dans script/ ni test/.

--- EN ---

`presets.load(identifier)` had no caller outside its own test: a public
interface invented for a need that never arose, and a test that existed
only because the interface did. The menu lists through `load_all` and
picks by rank.

It comes back the day a CLI takes a preset by name, with the test that
need will justify. Checked: 48 preset tests green, and no remaining call
in script/ or test/.

Assisted-by: Claude Opus 5
2026-09-08 09:41:29 -04:00
ae2909dd0f [FIX] vpn : ne plus demander la route par défaut à qui ne la pose pas
Le formulaire demandait « tout le trafic ? » à un pilote dont le SERVEUR
décide du routage, ne faisait rien de la réponse, et `status` la jugeait
quand même : un ✗ permanent sur un tunnel sain, et un profil annonçant
« tout le trafic » sans l'obtenir.

Un drapeau, sur le modèle de celui du MTU, dit quels pilotes posent cette
route. Les autres ne sont ni interrogés ni jugés, et un drapeau laissé à
vrai n'est plus conservé. L'honorer serait pire qu'inutile : forcer une
route par défaut contre une passerelle en tunnel scindé donne un trou
noir, une passerelle ne routant pas ce qu'elle n'a pas annoncé. Les routes
déclarées, elles, restent honorées — le formulaire le dit.

--- EN ---

The form asked "all traffic?" of a driver whose SERVER decides the
routing, did nothing with the answer, and `status` judged it anyway: a
permanent ✗ on a healthy tunnel, and a profile announcing "all traffic"
without getting it.

A flag, modelled on the MTU one, says which drivers lay that route. The
others are neither asked nor judged, and a flag left true is no longer
kept. Honouring it would be worse than useless: forcing a default route
against a split-tunnel gateway gives a black hole, a gateway not routing
what it never advertised. Declared routes are still honoured — the form
says so.

Assisted-by: Claude Opus 5
2026-09-08 09:41:17 -04:00
ea07c71e04 [FIX] vpn : juger un profil monté sur son interface, et le marquer
Le verdict se fondait sur le seul fichier d'état, que RIEN n'efface quand
un tunnel meurt sans passer par « down » — machine redémarrée, processus
tué, session expirée. Un état laissé derrière était annoncé monté sur
l'écran même qui déclarait le processus mort et l'interface absente.

L'interface arbitre désormais : son nom vient de l'état s'il y en a un,
sinon du pilote pour ceux qui la nomment d'avance, si bien qu'un tunnel
monté hors de l'outil est vu aussi. Sshuttle n'en crée aucune, là le
processus reste seul juge. La liste des profils le marque, connecter un
profil déjà monté demande confirmation, et déconnecter un profil déjà
tombé le dit sans l'empêcher : « down » nettoie l'état laissé.

--- EN ---

The verdict rested on the state file alone, which NOTHING clears when a
tunnel dies without going through "down" — machine rebooted, process
killed, session expired. A leftover state was reported as mounted on the
very screen declaring the process gone and the interface absent.

The interface now arbitrates: its name comes from the state when there is
one, otherwise from drivers that name it in advance, so a tunnel brought
up outside the tool is seen too. Sshuttle creates none, and there the
process stays the only judge. The profile list marks it, connecting an
already-mounted profile asks first, and disconnecting an already-down one
says so without refusing: "down" clears the leftover state.

Assisted-by: Claude Opus 5
2026-09-07 23:27:35 -04:00
aa14942709 [ADD] vpn : parcourir les profils AnyConnect au lieu de taper le chemin
Personne ne retient « /opt/cisco/secureclient/vpn/profile ». Le chemin
tapé oblige à le connaître ; le parcours seul n'aide pas quand le fichier
vient d'ailleurs, d'un courriel ou du portail d'un site. Les deux, dans
cet ordre, et la saisie reprend la main dès qu'on sort du parcours sans
rien choisir ou qu'on en rapporte un répertoire.

Le parcours n'est proposé que si l'un des répertoires connus existe :
l'ouvrir sur un chemin absent afficherait une liste vide, ce qui ressemble
à une panne. Vérifié : 7 tests unitaires, dont chacun des replis.

--- EN ---

Nobody remembers "/opt/cisco/secureclient/vpn/profile". A typed path
requires knowing it; browsing alone does not help when the file comes
from elsewhere, from an e-mail or a site's portal. Both, in that order,
and typing takes over as soon as one leaves the browser without choosing
anything, or brings back a directory from it.

Browsing is offered only if one of the known directories exists: opening
it on an absent path would show an empty list, which looks like a fault.
Checked: 7 unit tests, one for each fallback.

Assisted-by: Claude Opus 5
2026-09-07 22:19:22 -04:00
5aabbe4b87 [ADD] vpn install : proposer et poser le greffon SSO
openconnect refuse les passerelles qui exigent un navigateur intégré, sur
« No SSO handler » : les distributions le bâtissent sans webview. Rien
n'installait le greffon qui fait cette étape, que le pilote attendait
pourtant — une machine paraissait équipée sans l'être.

La question n'est posée que pour le pilote qui peut s'en servir, et
seulement quand le greffon manque. Son amont est arrêté depuis 2023 :
épingles intenables sur un Python récent, Qt et lxml pris de la
distribution, correctif rejoué à chaque installation. Le venv appartient
à l'utilisateur, non à root, qui n'a ni affichage ni trousseau. Vérifié :
installation depuis rien en 6,5 s, paquets éprouvés sur debian et ubuntu.

--- EN ---

openconnect refuses gateways demanding an embedded browser, on "No SSO
handler": distributions build it without a webview. Nothing installed the
helper that performs that step, which the driver expected all the same —
a machine looked equipped without being so.

The question is asked only for the driver that can use it, and only when
the helper is missing. Its upstream has been unmaintained since 2023:
pins unsatisfiable on a recent Python, Qt and lxml taken from the
distribution, a patch replayed on every install. The venv belongs to the
user, not root, which has neither display nor keyring. Checked: install
from nothing in 6.5 s, package names proven on debian and ubuntu only.

Assisted-by: Claude Opus 5
2026-09-07 22:19:22 -04:00
9250d4d401 [ADD] vpn openconnect : groupe d'URL, SSO délégué, mot de passe borné
Deux mécanismes désignent un service sur un concentrateur : un chemin
d'URL, une valeur de menu déroulant. Les confondre rend le formulaire d'un
AUTRE service — identifiants justes refusés, rien ne désignant le groupe.
Et certains ne comparent que les N premiers caractères du mot de passe,
que le profil déclare désormais sans jamais rien tronquer.

Une passerelle qui exige un navigateur intégré arrête openconnect sur
« No SSO handler », les distributions le bâtissant sans webview. Un greffon
fait l'étape web et rend un cookie ; le pilote monte lui-même, et le profil
garde son interface, son état et son diagnostic.

Vérifié : 249 tests unitaires, un tunnel monté contre une passerelle SAML.

--- EN ---

Two mechanisms designate a service on one concentrator: a URL path, and a
dropdown value. Confusing them hands over ANOTHER service's login form —
correct credentials refused, with nothing pointing at the group. And some
compare only the first N characters of the password, which the profile now
declares without ever truncating anything.

A gateway demanding an embedded browser stops openconnect on "No SSO
handler", distributions building it without a webview. A helper does the
web step and returns a cookie; the driver mounts the tunnel itself, so the
profile keeps its interface, its state and its diagnosis.

Checked: 249 unit tests, and one tunnel mounted against a SAML gateway.

Assisted-by: Claude Opus 5
2026-09-04 06:26:34 -04:00
7b749e43bb [ADD] vpn : préréglages de site, importés d'un profil AnyConnect
Un site publie sa passerelle, son protocole et son groupe de connexion.
Les retaper sur chaque poste, c'est se tromper un jour sur le seul champ
qui décide du service joint. Un préréglage est un profil PARTIEL, sans
identifiant ni secret : c'est ce qui lui permet de circuler.

Les répertoires sont lus dans l'ordre et le plus tardif gagne sur un même
identifiant, pour qu'un site corrige un gabarit livré sans modifier de
fichier suivi, donc sans conflit au prochain pull. Un fichier illisible
est signalé et sauté : sinon la panne se lit « aucun préréglage » alors
qu'il y en a dix. Un profil AnyConnect porte déjà les trois balises
utiles, il est donc lu directement. Vérifié : 19 tests unitaires.

--- EN ---

A site publishes its gateway, protocol and connection group. Retyping
them on every machine means getting the one field that decides which
service you reach wrong some day. A preset is a PARTIAL profile, with no
username and no secret: that is what lets it be handed around.

Directories are read in order and the latest wins on the same identifier,
so a site can fix a shipped template without editing a tracked file, and
without a conflict on the next pull. An unreadable file is reported and
skipped: otherwise the fault reads as "no preset" when ten exist. An
AnyConnect profile already carries the three useful tags, so it is read
directly. Checked: 19 unit tests.

Assisted-by: Claude Opus 5
2026-09-04 06:26:34 -04:00
50187ece85 [ADD] vpn install : vérifier la présence de vpnc-script
openconnect appelle vpnc-script pour poser les routes et le DNS. Il vient
d'un paquet distinct, dont le nom change de famille en famille et qui
l'installe à des endroits différents. C'est un FICHIER qu'on cherche, pas
un binaire du PATH : le contrôle des binaires ne peut donc pas le voir.

Sans lui, la session s'ouvre, openconnect démarre, et l'interface tun
n'apparaît jamais. La panne se manifeste trois étages au-dessus du paquet
absent, sur un symptôme qui ne l'accuse pas. L'installateur nomme
désormais le paquet à poser, par famille de distribution.

--- EN ---

openconnect calls vpnc-script to lay down routes and DNS. It ships in a
separate package whose name varies by family and which installs it at
different paths. It is a FILE we look for, not a binary on the PATH, so
the binary check cannot see it.

Without it the session opens, openconnect starts, and the tun interface
never appears. The failure surfaces three stages above the missing
package, on a symptom that does not accuse it. The installer now names
the package to install, per distribution family.

Assisted-by: Claude Opus 5
2026-09-04 06:26:34 -04:00
e14356a51e [ADD] release : publier 1.8.0, section de changelog et version déclarée
Les 39 puces du non-publié décrivent du travail fini qu'aucun numéro ne
permettait de citer. La section 1.8.0 les porte, avec la note de migration que
réclament le choix de l'interpréteur Python et le retrait d'Ubuntu 20.04 et
22.04. La version déclarée suit, comme le veut doc/RELEASE.base.md : le
fichier semver que lit env_var.sh, les huit pyproject, npm, CLAUDE.md et la
note des versions supportées. La ligne de contracts/compatibility.json reste
en 1.7.0, elle atteste un appariement mobile éprouvé et non la version
courante. Le commentaire de todo.py ne nomme plus de version, celle du dépôt
étant marquée « default » dans le fichier des versions supportées.
Vérifié : ERPLIBRE_VERSION=1.8.0, parité 17/8/10/2/2, tests de version verts.

--- EN ---

The unreleased list's 39 entries describe finished work no number could cite.
The 1.8.0 section carries them, with the migration note that choosing the
Python interpreter and dropping Ubuntu 20.04 and 22.04 call for. The declared
version follows, as doc/RELEASE.base.md wants: the semver file env_var.sh
reads, the eight pyproject files, npm, CLAUDE.md and the supported-version
note. The contracts/compatibility.json row stays at 1.7.0, attesting a tested
mobile pairing and not the current version. The todo.py comment names no
version any more, the repository's own being marked « default » in the
supported-version file. Checked: ERPLIBRE_VERSION=1.8.0, 17/8/10/2/2 parity,
version tests green.

Assisted-by: Claude Opus 5
2026-09-04 05:57:20 -04:00
f0a1457c20 [UPD] version : porter la version déclarée d'ERPLibre à 1.7.0
Le tag v1.7.0 existe depuis mars et chaque déclaration disait encore 1.6.0.
« .erplibre-semver-version » est la source que env_var.sh lit dans
ERPLIBRE_VERSION. « pyproject.toml » est un lien vers le fichier de la version
d'Odoo active : les huit portent la version d'ERPLibre, en laisser sept à
1.6.0 ferait dépendre la version déclarée de la bascule d'Odoo. Les étiquettes
d'images docker restent à 1.6.0 : elles nomment des images publiées, empreinte
de compilation comprise, que la construction n'a pas encore produites.
Vérifié : env_var.sh rend 1.7.0, les quatre JSON se relisent, et les tests de
version et de configuration passent.

--- EN ---

Tag v1.7.0 has existed since March and every declaration still said 1.6.0.
`.erplibre-semver-version` is the source env_var.sh reads into
ERPLIBRE_VERSION. `pyproject.toml` is a link to the file of the active Odoo
version: all eight carry the ERPLibre version, and leaving seven at 1.6.0
would make the declared version depend on an Odoo switch. The docker image
tags stay at 1.6.0: they name published images, build hash included, that the
build step has not produced yet. Checked: env_var.sh renders 1.7.0, the four
JSON files reload, and the version and configuration tests pass.

Assisted-by: Claude Opus 5
2026-09-04 05:34:45 -04:00
228ea36723 [FIX] install arch : poser less, sans quoi git échoue à paginer
Le script pose git, et sur Arch « less » n'est ni dans le groupe « base » ni
une dépendance ferme de git : pacman le donne pour OPTIONNEL et ne l'installe
donc jamais. Git appelle pourtant son paginateur par défaut, si bien que log,
diff, show et tag s'arrêtent sur « unable to execute pager 'less' » sans rien
afficher — sur une image cloud comme sur toute installation minimale. Debian
et Ubuntu l'obtiennent par les recommandations de leur paquet git, notion que
pacman n'a pas. Constaté par « pacman -Si base » et « pacman -Sii less » ;
bash -n sur le script, dont le « less » npm reste le préprocesseur CSS.

--- EN ---

The script poses git, and on Arch « less » is neither in the « base » group nor
a firm dependency of git: pacman lists it as OPTIONAL and therefore never
installs it. Git calls its default pager all the same, so log, diff, show and
tag stop on « unable to execute pager 'less' » displaying nothing — on a cloud
image as on any minimal install. Debian and Ubuntu get it through their git
package's recommendations, a notion pacman does not have. Checked with
« pacman -Si base » and « pacman -Sii less »; bash -n on the script, whose npm
« less » stays the CSS preprocessor.

Assisted-by: Claude Opus 5
2026-09-04 04:51:56 -04:00
e7d630b330 [ADD] qemu réseau : voir et recréer le sous-réseau des VM
Une VM prend son adresse dans le /24 de son réseau et en sort par le .1,
porté par le pont : déplacer ce /24 sous une VM allumée lui laisse un bail
qui ne mène nulle part, et abattre le réseau détache son tap, que libvirt
n'y remet pas. network_qemu.py tient les trois gestes dans l'ordre — arrêter
les VM attachées, redéfinir le réseau sur le préfixe voulu, rallumer celles
qu'il a arrêtées, elles seules. Une VM sourde au shutdown annule la
redéfinition plutôt que d'y perdre son pont ; --force-off coupe le courant.
Un préfixe qui recouvre ce que l'hôte route déjà est refusé.
Vérifié : 13 tests neufs, la numérotation du menu, une passe à blanc.

--- EN ---

A VM takes its address in its network's /24 and leaves through the .1,
carried by the bridge: moving that /24 under a live VM leaves it a lease
leading nowhere, and tearing the network down detaches its tap, which
libvirt does not put back. network_qemu.py holds the three steps in order —
shut down the attached VMs, redefine the network on the wanted prefix, start
again the ones it stopped, those only. A VM deaf to the shutdown cancels the
redefinition rather than losing its bridge; --force-off cuts the power.
A prefix overlapping what the host already routes is refused.
Checked: 13 new tests, the menu numbering, one dry run.

Assisted-by: Claude Opus 5
2026-09-04 03:31:22 -04:00
82c651f1df [FIX] qemu réseau : ne plus compter le pont d'un réseau contre lui-même
Un réseau libvirt démarré porte et route son /24 sur son pont, et ce pont
comptait dans « ce que l'hôte occupe déjà » : le verdict était donc
« collision » sur toute machine où le réseau tournait, quel que soit son
sous-réseau. Tout déploiement passe par cette vérification, qui abattait
alors le réseau et le déplaçait sur un /24 libre là où rien n'entrait en
conflit — les VM attachées y perdaient passerelle et pont. Le pont du réseau
examiné en est écarté, chaque adresse étant rattachée à son interface ; un
nom de pont illisible n'écarte rien. Le XML de net-define passe par un
fichier temporaire imprévisible, retiré même quand virsh échoue.
Vérifié : 31 tests, dont 10 neufs, que le retrait de l'exclusion casse.

--- EN ---

A started libvirt network carries and routes its /24 on its bridge, and that
bridge counted as « what the host already occupies »: the verdict was
therefore « collision » on every machine where the network ran, whatever
subnet it served. Every deployment goes through that check, which then tore
the network down and moved it onto a free /24 where nothing conflicted — the
attached VMs lost gateway and bridge. The bridge of the network being
examined is now excluded, each address being tied to its own interface; an
unreadable bridge name excludes nothing. The XML for net-define goes through
an unpredictable temporary file, removed even when virsh fails.
Checked: 31 tests, 10 of them new, which removing the exclusion breaks.

Assisted-by: Claude Opus 5
2026-09-04 03:31:08 -04:00
416a0c8631 [IMP] qemu deploy : constater le motif du sudo, le dire avant l'invite
sudo ne dit jamais ce qu'il sert à faire : son invite tombe entre deux lignes
de journal, et l'on tape un mot de passe sans savoir s'il porte sur libvirt,
sur un paquet ou sur un fichier — d'autant qu'appartenir au groupe libvirt a
l'air de suffire. Il ne suffit pas, et libvirt n'y est pour rien : le disque
et le seed s'écrivent dans le pool par défaut, répertoire de root où le
groupe ne donne pas l'écriture. Le motif est constaté, non déduit :
l'écriture s'essaie, une ACL pouvant l'accorder là où le mode semble la
refuser. Dit une fois avant la première commande privilégiée, et en dernière
ligne du récapitulatif. Vérifié : 18 tests, root compris, à qui rien n'est dit.

--- EN ---

sudo never states what it is about to do: its prompt lands between two log
lines, and one types a password without knowing whether it covers libvirt, a
package or a file — the more so as being in the libvirt group looks like it
should be enough. It is not, and libvirt is not the reason: the disk and the
seed are written into the default pool, a root-owned directory where the
group grants no write right. The reason is checked, not deduced: writing is
tried, since an ACL can grant it where the mode seems to refuse it. Said once
before the first privileged command, and last on the review page. Checked: 18
tests, root included, which is told nothing.

Assisted-by: Claude Opus 5
2026-09-04 02:27:51 -04:00
4b320dc1de [FIX] qemu setup-host : ne plus priver l'hôte de réseau au redémarrage
Le réseau « default » de libvirt sert 192.168.122.0/24, et toute VM déployée
par ce dépôt y vit : son pont prendrait la première adresse du /24, celle de
sa passerelle. virsh refuse ce démarrage tant que la route est là ; au
démarrage, libvirtd monte ses réseaux avant le bail DHCP, et plus rien ne la
signale. L'autostart était armé même après un net-start refusé, d'où un hôte
sans réseau au redémarrage suivant. Il ne s'arme plus qu'en l'absence de
collision, le réseau est déplacé par redéfinition — ni pont ni module du
noyau — et un actif en collision est abattu. L'état, cherché en anglais quand
virsh traduit, se lisait toujours éteint : LC_ALL=C. Vérifié sur un hôte en
collision, puis 21 tests.

--- EN ---

libvirt's `default` network serves 192.168.122.0/24, and every VM this
repository deploys lives there: its bridge would take the /24's first
address, which is that machine's gateway. virsh refuses such a start while
the route is there; at boot, libvirtd raises its networks before the DHCP
lease, and nothing signals the collision. Autostart was armed even after a
refused net-start, hence a host with no network at the next boot. It is armed
only where no collision remains, the network is moved by redefinition — no
bridge, no kernel module — and an active collision is torn down. State, read
in English where virsh translates, always read as off: LC_ALL=C. Checked on a
colliding host, then 21 tests.

Assisted-by: Claude Opus 5
2026-09-04 02:05:35 -04:00
44dc08c06c [IMP] qemu deploy : pré-configurer la VM, et dire ce qui s'y installe
L'option des outils d'assistance posait trois installateurs amont, puis
laissait tout à retaper : hook global de rtk, zdiff3, hooks git du dépôt,
commandes Claude, activation du venv. Elle les pose, en deux temps parce que
hooks et gabarits VIVENT dans le dépôt ; le complément suit le clone, rend
toujours 0 — un confort ne fait pas échouer une VM — et sans clone la moitié
manquante est nommée. core.editor n'est posé qu'à défaut, l'hôte transmettant
déjà le sien. L'aide « ? » et F1 dit ce que chaque option installe, là où un
libellé de case porte trois des huit poses. Vérifié : 51 tests, les deux
écrans montés sans terminal, « bash -n » sur les fragments distants.

--- EN ---

The AI tools box posed three upstream installers, then left every setting to
retype: rtk's global hook, zdiff3, the checkout's git hooks, the Claude
commands, activating the venv. It poses them, in two phases because hooks and
templates LIVE in the checkout; the complement follows the clone, always
returns 0 — a comfort must not fail a VM — and with no clone the missing half
is named. core.editor is posed only where there is none, the host already
transmitting its own. The `?` and F1 help says what each option installs,
where a checkbox label carries three of this one's eight poses. Checked: 51
tests, both screens mounted headless, `bash -n` on the remote fragments.

Assisted-by: Claude Opus 5
2026-09-04 02:03:52 -04:00
a014e1eb30 [IMP] script todo : donner une icône aux entrées du menu RTK
Le menu RTK est le seul dont les entrées n'affichaient rien avant leur
libellé : six lignes que seul leur numéro distinguait. Les six glyphes sont
pris parmi ceux que le fichier emploie déjà plutôt que choisis pour eux-mêmes
— un caractère récent s'affiche en tofu sur un terminal dont la police ne le
connaît pas. L'icône va dans la chaîne traduite, comme partout ailleurs dans
le menu, donc les deux langues la portent.

Vérifié : rendu en français et en anglais ; chacune des six clés ne sert
qu'ici, aucun autre menu n'est touché.

--- EN ---

The RTK menu is the only one whose entries showed nothing before their label:
six lines told apart by their number alone. The six glyphs are taken from
those the file already uses rather than picked for themselves — a recent
character renders as tofu on a terminal whose font does not know it. The icon
goes into the translated string, as everywhere else in the menu, so both
languages carry it.

Checked: rendering in French and English; each of the six keys is used here
only, so no other menu is affected.

Assisted-by: Claude Opus 5
2026-09-04 00:38:37 -04:00
6c1d985686 [FIX] script todo : starship, URL corrigée, curl qui refuse le HTML
L'URL amont de starship pointait sur un chemin qui n'existe pas : le serveur
rend 404 avec une page HTML de 34 ko, que « curl -sS » écrit tout de même sur
sa sortie en rendant 0. Cette page arrivait donc sur l'entrée de « sh », qui
s'arrêtait sur « Syntax error » à sa deuxième ligne — un diagnostic qui ne dit
ni l'URL fautive ni que rien n'a été posé. « -f » fait taire curl et rendre 22
sur toute réponse d'erreur ; les quatre autres installateurs amont du fichier
l'utilisaient déjà.

Vérifié : le « --help » de l'installateur est atteint, sans rien installer.

--- EN ---

The upstream starship URL pointed at a path that does not exist: the server
answers 404 with a 34 kB HTML page, which `curl -sS` writes to its output
anyway while exiting 0. That page reached the input of `sh`, which stopped on
`Syntax error` at its second line — a diagnosis naming neither the wrong URL
nor the fact that nothing was installed. `-f` makes curl write nothing and
return 22 on any error response; the four other upstream installers in the
file already used it.

Checked: the installer's `--help` is reached, with nothing installed.

Assisted-by: Claude Opus 5
2026-09-04 00:38:29 -04:00
af38810da5 [FIX] script execute : afficher l'invite sans attendre le saut de ligne
« readline » ne rend la main qu'au saut de ligne : une invite qui n'en
porte pas restait retenue jusqu'à la ligne suivante, écrite après la
réponse. Toute question « [o/N] » d'une commande du menu se voyait donc
après coup, et l'on y répondait à l'aveugle. La lecture se fait par blocs,
le reliquat part à l'écran aussitôt et stdout est vidé ; un décodeur
incrémental protège les accents coupés entre deux lectures.

Vérifié : invite visible avant la frappe, sans double affichage ; 13 cas de
non-régression ; 4546 tests, seuls les 9 échecs préexistants de qemu_proxmox.

--- EN ---

`readline` only returns at a newline: a prompt carrying none stayed held
back until the next line, written after the answer. Every "[o/N]" question
asked by a menu command was therefore seen after the fact, and answered
blind. Reading now happens in chunks, the trailing fragment reaches the
screen at once and stdout is flushed; an incremental decoder protects
accented characters split across two reads.

Checked: prompt visible before typing, with no double display; 13 regression
cases; 4546 tests, only the 9 pre-existing qemu_proxmox failures.

Assisted-by: Claude Opus 5
2026-09-04 00:24:09 -04:00
b8361c30e6 [FIX] git remote : ignorer un dépôt fautif, lister les écarts à la fin
Un répertoire vidé à la main passe `os.path.isdir` puis fait lever
l'ouverture du dépôt, ce qui emportait tout le reste du lot. Ces états —
clone interrompu, `.git` effacé — sont courants sur un checkout de
développement, et sans rapport avec les dépôts qui attendent leur remote.
Les écarts sont rendus plutôt qu'affichés au fil de l'eau : noyés dans la
trace d'un lot de plus de cent dépôts, ils ne se voient plus. La sortie
reste nulle, comme déjà pour un répertoire absent.
Vérifié dans des dépôts jetables ; 6 tests neufs, dont 2 échouent si le
garde-fou est retiré.

--- EN ---

A directory emptied by hand passes `os.path.isdir` then makes opening the
repository raise, which took down the whole rest of the batch. Such states —
an interrupted clone, a deleted `.git` — are common on a development
checkout, and unrelated to the repositories awaiting their remote.
Deviations are returned rather than printed as they happen: drowned in the
trace of a batch of over a hundred repositories, they can no longer be seen.
The exit code stays zero, as it already was for a missing directory.
Checked in throwaway repositories; 6 new tests, 2 of which fail if the guard
is removed.

Assisted-by: Claude Opus 5
2026-09-04 03:48:39 +00:00
a60baf743e [FIX] commit msg : chercher lib_identifiant sans masquer GitPython
Le répertoire ajouté est `script/`, qui porte un `git/` : en tête de
`sys.path`, il fait résoudre `import git` vers ce paquet plutôt que vers
GitPython, pour tout module importé ENSUITE dans le même processus — une
bibliothèque tierce disparaît parce qu'une des nôtres porte le même nom.
En queue, `lib_identifiant` se trouve toujours et les paquets installés
gardent la priorité ; la racine du dépôt suit la même règle, elle porte un
`test/` homonyme d'un paquet standard.
Mesuré : `unittest discover -p 'test_git*.py'` passe de 49 tests et une
erreur d'import à 84 verts ; le hook commit-msg reste vérifié.

--- EN ---

The directory being added is `script/`, which holds a `git/`: at the head of
`sys.path` it makes `import git` resolve to that package rather than to
GitPython, for every module imported AFTERWARDS in the same process — a
third-party library vanishes because one of ours bears the same name.
At the tail, `lib_identifiant` is still found and installed packages keep
their precedence; the repository root follows the same rule, it holds a
`test/` sharing its name with a standard library package.
Measured: `unittest discover -p 'test_git*.py'` goes from 49 tests and one
import error to 84 green; the commit-msg hook stays verified.

Assisted-by: Claude Opus 5
2026-09-04 03:48:36 +00:00
eb5d6a72a0 [FIX] git tool : garder le dépôt racine quand le manifeste manque
Le traitement de `add_root` est en FIN de fonction, et deux retours
anticipés lui sautaient par-dessus : sans manifeste, la liste revenait vide
alors que la racine était demandée. Le script qui réécrit les remotes ne
trouvait donc rien à faire sur un checkout sans `.repo`, et l'annonçait
comme un succès.
L'ajout de la racine et le tri passent par un point de sortie unique, ce
qui supprime la copie qui gardait le défaut dans une variante sur deux. Une
origine absente vaut l'URL par défaut plutôt qu'une exception.
Vérifié dans un dépôt jetable, dans les deux sens et sans origine ; 4 tests
neufs, qui échouent si le retour anticipé revient.

--- EN ---

The `add_root` handling sits at the END of the function, and two early
returns jumped over it: with no manifest, the list came back empty although
the root had been asked for. The script that rewrites the remotes therefore
found nothing to do on a checkout without `.repo`, and reported it as a
success.
Adding the root and sorting now go through a single exit point, which drops
the copy that kept the fault in one variant out of two. A missing origin
falls back to the default URL rather than raising.
Checked in a throwaway repository, both ways and with no origin; 4 new
tests, which fail if the early return comes back.

Assisted-by: Claude Opus 5
2026-09-04 03:48:32 +00:00
315e45e5f0 [ADD] menu vpn : créer un profil, déposer les secrets, monter le tunnel
`vpn.py` savait tout faire en ligne de commande, mais il fallait écrire le
profil JSON à la main et déposer les secrets dans KeePassXC soi-même. Le menu
pose les questions que le pilote choisi déclare, et lui seul : ajouter une
technologie n'ajoute pas une ligne ici.

Le coffre est manipulé EN PROCESSUS — le mot de passe maître est déjà en
mémoire, le redemander à un sous-processus serait une saisie de plus à chaque
geste. Montage, démontage et diagnostic passent au contraire par `vpn.py` en
sous-processus : ils durent, ils parlent, et la sortie en direct est ce qui
rend une montée de tunnel suivable.
Vérifié : 37 tests sur le menu, aucune saisie réelle.

--- EN ---

`vpn.py` could already do everything from the command line, but the JSON
profile had to be written by hand and the secrets filed into KeePassXC by
hand. The menu asks the questions the chosen driver declares, and only those:
adding a technology adds no line here.

The vault is handled IN PROCESS — the master password is already in memory,
and asking a subprocess for it again would be one more entry at every step.
Raising, tearing down and diagnosing go through `vpn.py` as a subprocess
instead: they last, they talk, and live output is what makes a tunnel coming
up followable.
Checked: 37 tests on the menu, no real input.

Assisted-by: Claude Opus 5
2026-09-04 03:43:02 +00:00
197d19d61e [ADD] vpn : cinq pilotes, secrets en coffre, diagnostic étagé
Le dépôt n'avait aucun moyen de monter un tunnel VPN ni de dire pourquoi il
refuse de monter. Cinq technologies libres, un pilote chacune, derrière un
`vpn.py` qui monte, démonte et diagnostique.

Ce qui n'est pas secret — hôte, utilisateur, routes, MTU — vit dans une
configuration JSON lisible ; clés pré-partagées et mots de passe vivent dans
un coffre KeePassXC. Un profil se montre et se partage sans donner de quoi
monter le tunnel. Les secrets s'écrivent en tmpfs sous 0700, jamais sur un
disque persistant. Le diagnostic part du noyau et remonte, pour que la
première ligne fausse soit la cause et non une conséquence.
Vérifié : 138 tests, dont le rendu de chaque fichier généré.

--- EN ---

The repository had no way to raise a VPN tunnel, nor to say why one refuses
to come up. Five free technologies, one driver each, behind a `vpn.py` that
raises, tears down and diagnoses.

What is not secret — host, user, routes, MTU — lives in readable JSON
configuration; pre-shared keys and passwords live in a KeePassXC vault. A
profile can be shown and shared without handing over the means to raise the
tunnel. Secrets are written to tmpfs at 0700, never to a persistent disk.
Diagnosis starts at the kernel and climbs, so the first false line is the
cause and not a consequence.
Checked: 138 tests, including the rendering of every generated file.

Assisted-by: Claude Opus 5
2026-09-04 03:42:49 +00:00
bdc0e19302 [UPD] tests : nommer le répertoire comme frontière de la suite
Le lanceur prend TOUT `test/test_*.py` par glob, et l'en-tête disait
pourquoi : une liste de préfixes oublie ce qu'on ajoute. Il ne disait pas où
mettre ce qui doit rester dehors, si bien qu'une famille de tests nouvelle
semblait avoir quelque chose à déclarer ici.

La frontière est un RÉPERTOIRE : ce qui ne doit pas être lancé vit ailleurs
que dans `test/`. Rien à déclarer, donc rien à oublier de déclarer.

--- EN ---

The launcher takes ALL of `test/test_*.py` by glob, and the header said why:
a list of prefixes forgets whatever gets added. It did not say where to put
what must stay out, so a new family of tests looked as though it had
something to declare here.

The boundary is a DIRECTORY: what must not run lives somewhere other than
`test/`. Nothing to declare, hence nothing to forget to declare.

Assisted-by: Claude Opus 5
2026-09-04 03:42:27 +00:00
ae9463ebcb [FIX] kdbx : nommer le coffre avant que getpass ne pose sa question
L'invite de `getpass` part vers le terminal, la ligne qui nomme le coffre
vers la sortie standard — un TUBE dès qu'un menu nous lance. Sans vidage, les
deux ressortaient dans le désordre : « Mot de passe du coffre : Coffre
KeePass : <chemin> », la question avant ce dont elle parle.

Le bouchon de `print` des tests accepte désormais la signature complète de
`print` : sans `**k`, ajouter un `flush` au code testé faisait échouer six
tests sur une différence étrangère à ce qu'ils vérifient.

--- EN ---

The `getpass` prompt goes to the terminal, the line naming the vault to
standard output — a PIPE as soon as a menu launches us. Without a flush the
two came out in the wrong order: "Vault password: KeePass vault: <path>", the
question before what it is about.

The tests' `print` stub now accepts the full signature of `print`: without
`**k`, adding a `flush` to the code under test failed six tests over a
difference foreign to what they check.

Assisted-by: Claude Opus 5
2026-09-04 03:42:18 +00:00
cedf702226 [FIX] kdbx : ouvrir le coffre sans tkinter, donc sur un serveur
Sur une machine sans tkinter — tout serveur — `get_kdbx()` rendait None en
journalisant « pykeepass is not installed », alors que pykeepass était là.
Les deux imports partageaient un seul `try` : l'absence de tkinter mettait
aussi `PyKeePass` à None, et le coffre restait inouvrable même avec chemin et
mot de passe configurés.

tkinter ne sert qu'au sélecteur de fichier, quand aucun chemin n'est
configuré ; deux blocs séparés le rendent à ce seul rôle.

--- EN ---

On a machine without tkinter — every server — `get_kdbx()` returned None
while logging "pykeepass is not installed", though pykeepass was present.
Both imports shared one `try`: a missing tkinter set `PyKeePass` to None as
well, and the vault stayed unopenable even with path and password configured.

tkinter serves only the file picker, when no path is configured; two separate
blocks give it back that single role.

Assisted-by: Claude Opus 5
2026-09-04 03:42:00 +00:00
86c4576f01 [IMP] script todo : icônes des cinq commandes du menu Claude
Le menu qui déploie les commandes d'assistant était le dernier resté nu,
et cinq lignes qui ouvrent toutes sur le même mot se lisent une par une.
Chaque signe dit ce que la commande fait — message, fusion, plan, code,
inventaire — plutôt que de répéter qu'il s'agit d'une commande.

Les cinq emoji rendent sur deux colonnes, d'où l'espace unique qui les
sépare du libellé. Aucun ne servait ailleurs : un même signe pour deux
concepts distincts se lit comme un lien qui n'existe pas.

--- EN ---

The menu deploying assistant commands was the last one left bare, and
five lines all opening on the same word are read one by one. Each sign
says what the command does — message, merge, plan, code, inventory —
instead of repeating that it is a command.

The five emoji render two columns wide, hence the single space before
the label. None was in use elsewhere: one sign for two distinct concepts
reads as a link that does not exist.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
68eff72243 [ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.

Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.

--- EN ---

One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.

Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
988ea6bcd5 [REF] todo shell : une table unique pour les installateurs amont
Les mêmes outils se posent à DEUX endroits : sur l'hôte par le menu Shell,
et bientôt dans une VM par le déploiement QEMU. Une URL amont recopiée
dérive dès que l'une des deux copies change, et c'est la seconde qu'on
oublie — celle qui n'a pas de menu pour la montrer.

Les noms de classe restent : ils sont l'interface, et les tests les
interrogent. Aucun comportement ne change.

--- EN ---

The same tools are installed in TWO places: on the host from the Shell
menu, and soon inside a VM from the QEMU deployment. A copied upstream
URL drifts as soon as one of the two copies changes, and the forgotten
one is the second — the one with no menu to show it.

The class names stay: they are the interface, and the tests read them.
No behaviour changes.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
b474b6d65e [FIX] qemu 3D : donner à l'invité l'accès au nœud de rendu
Le matériel virtuel accéléré ne suffisait pas. Dans l'invité, le nœud de
rendu appartient à « root:render » en 0660 et le compte créé n'y était
pas : toute application GL retombait sur le rendu logiciel alors que la
négociation VIRGL avait réussi, et rien ne le signalait. En session
graphique locale, logind pose une ACL pour l'utilisateur du siège ; en
SSH ou en tty, le cas d'une VM de ce parc, personne ne la pose.

Les groupes sont DÉCLARÉS avant d'être utilisés : « useradd -G » échoue
sur un nom inconnu, et cloud-init ne crée alors pas le compte du tout —
la VM démarre inaccessible. « render » manque des images anciennes. Même
parité côté preseed, par « groupadd -f ». « --gpu off » n'ajoute rien.

--- EN ---

Accelerated virtual hardware was not enough. In the guest the render
node is « root:render » at 0660 and the created account was not in it:
every GL application fell back to software rendering although VIRGL
negotiation had succeeded, with nothing to say so. On a local graphical
session logind sets an ACL for the seat's user; over SSH or on a tty,
which is what these VMs get, nobody sets one.

Groups are DECLARED before use: « useradd -G » fails on an unknown name
and cloud-init then creates no account at all — the VM boots
unreachable. « render » is missing from older images. The preseed keeps
parity via « groupadd -f ». « --gpu off » adds nothing.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
612d943671 [ADD] qemu diagnostic : l'état 3D de chaque VM, ABI figée comprise
Le rapport disait ce que l'HÔTE sait faire, jamais ce que chaque VM
recevra au prochain démarrage. Trois valeurs y répondent ensemble et
aucune seule : le type de vidéo, « accel3d », et le device figé par
libvirt, qui l'emporte sur les deux autres. La section les lit dans la
définition persistante et écrit la commande qui défige, sans la lancer.

Les sections Python du rapport sont désormais isolées. Le fichier
s'écrit d'un bloc à la fin : une section qui lève emportait tout ce qui
avait été relevé avant elle, laissant sans rapport au moment d'en avoir
besoin. Le double de test rendait un objet sans « returncode », ce qui
masquait le défaut.

--- EN ---

The report said what the HOST can do, never what each VM will get on its
next boot. Three values answer that together and none alone: video type,
« accel3d », and the device libvirt pinned, which outranks both. The
section reads them from the persistent definition and writes out the
unpinning command without running it.

Python sections of the report are now isolated. The file is written in
one block at the end: a section that raised took down everything
gathered before it, leaving no report exactly when one is needed. The
test double returned an object with no « returncode », hiding the fault.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
1b26c4661b [FIX] qemu 3D : voir l'ABI figée qui annule l'accélération demandée
Depuis libvirt 12.5.0, le <model> vidéo porte un attribut « device » qui
grave le device QEMU retenu, pour tenir l'ABI de l'invité stable d'un
démarrage à l'autre. Il l'emporte sur « accel3d ». Une VM démarrée une
première fois sans 3D — module GL absent, ou option non cochée — garde
donc le device sans GL, et l'activer ensuite n'écrit qu'une intention :
QEMU reçoit toujours « virtio-vga » nu.

L'état lisait « accel3d » sans lire cet attribut, si bien que le résumé
annonçait « 3D » sur une VM qui tourne sans. Il porte désormais le device
figé quand celui-ci contredit la demande.

--- EN ---

Since libvirt 12.5.0 the video <model> carries a « device » attribute
recording the chosen QEMU device, to keep the guest ABI stable across
restarts. It outranks « accel3d ». A VM first started without 3D — GL
module absent, or the box unticked — therefore keeps the non-GL device,
and enabling it afterwards writes an intent only: QEMU still gets plain
« virtio-vga ».

State read « accel3d » without that attribute, so the summary announced
« 3D » on a VM running without it. It now names the pinned device
whenever it contradicts the request.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
94090d5280 [FIX] qemu 3D : le module virtio-vga-gl manquait au relevé des pièces
Les deux devices accélérés sont des modules séparés, empaquetés à part
sur les distributions qui découpent QEMU. Le relevé ne cherchait que
« virtio-gpu-gl » ; c'est « virtio-vga-gl » que libvirt exige pour un
écran principal compatible VGA, soit le cas d'une VM graphique. Le
rapport ressortait donc tout en vert à côté de la pièce absente.

Sans ce module, QEMU n'annonce pas le device, libvirt le remplace en
silence par « virtio-vga » nu et la VM démarre sans 3D — l'egl-headless
demandé restant en place, rien dans la définition ne trahit le repli.

--- EN ---

The two accelerated devices are separate modules, packaged apart on
distributions that split QEMU. The survey looked only for
« virtio-gpu-gl »; libvirt needs « virtio-vga-gl » for a VGA-compatible
primary display, which is what a graphical VM gets. The report therefore
came back all green next to the missing piece.

Without that module QEMU does not advertise the device, libvirt quietly
falls back to plain « virtio-vga » and the VM boots with no 3D — the
requested egl-headless still in place, nothing in the definition telling.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
9370bff319 [ADD] qemu diagnostic : le device vidéo reçu par le QEMU en cours
Le relevé disait ce que la définition DEMANDE, jamais ce que la VM a
REÇU ; libvirt peut retirer l'accélération entre les deux sans le
signaler. Or « egl-headless » paraît sur la ligne de commande que la 3D
soit active ou non : le seul témoin est le suffixe « -gl » du device.
Chercher « virgl » n'aide pas — cette forme ne s'écrit plus sur les QEMU
récents, et son absence sur une VM accélérée fait conclure à un réglage
manquant qui n'existe pas.

La sonde lit /proc/<pid>/cmdline, sans privilège, et trie sur argv[0] :
sur la ligne entière elle se compterait elle-même.

--- EN ---

The report told what the definition ASKS for, never what the VM GOT;
libvirt can drop acceleration in between without saying so. And
« egl-headless » shows on the command line whether 3D is live or not:
the only witness is the device's « -gl » suffix. Grepping for « virgl »
does not help — that spelling is gone from recent QEMU, and its absence
on an accelerated VM suggests a missing setting that does not exist.

The probe reads /proc/<pid>/cmdline, unprivileged, and sorts on argv[0]:
on the whole line it would count itself.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
6f7ad39616 [IMP] script todo : icônes de menu, alignées sur leur largeur rendue
Le menu Git avait posé la convention ; dix autres restaient nus, et une
liste sans icône se lit ligne à ligne. L'espacement suit la largeur
RENDUE, lue dans Unicode et non devinée : deux espaces derrière un emoji
d'une colonne, une derrière un large — d'où le changement de signe de la
Configuration, dont le sien décalait le menu principal. Un même concept
garde son signe d'un menu à l'autre. « Quitter » garde sa traduction nue,
sa clé servant onze fois en raccourci de pied de page : son icône se pose
sur la ligne du menu.

Vérifié : menus rendus dans les deux langues, aucune clé incomplète,
aucune icône divergente entre les langues.

--- EN ---

The Git menu had set the convention; ten others stayed bare, and a list
without icons is read line by line. Spacing follows the RENDERED width,
read from Unicode rather than guessed: two spaces after a one-column
emoji, one after a wide one — hence the new sign for Configuration, whose
own offset the main menu. One concept keeps its sign from menu to menu.
« Quit » keeps its bare translation, its key serving eleven times as a
footer shortcut: its icon sits on the menu's own line.

Checked: menus rendered in both languages, no incomplete key, no icon
diverging between languages.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
625288fd4c [ADD] qemu diagnostic : un relevé à transmettre, Gérer scindé en trois
Un problème de VM se résout souvent par quelqu'un qui n'a pas accès à la
machine : vingt et une sondes en lecture écrivent un relevé unique —
hôte, hyperviseur, GPU, outils, stockage — chacune bornée dans le temps,
une commande qui pend ne devant pas retenir le rapport. Sur le pilote
NVIDIA propriétaire, trois conditions vivent hors des briques Mesa et
QEMU, dont la liste de périphériques où libvirt n'ajoute jamais les nœuds
de la carte : le relevé les nomme et propose la commande qui installe ce
qui lui manque, affichée avant la question. Gérer se scinde en trois.

--- EN ---

A VM problem is often solved by someone with no access to the machine:
twenty-one read-only probes write one report — host, hypervisor, GPU,
tools, storage — each time-bounded, since a command that hangs must not
hold the report. On the proprietary NVIDIA driver three conditions live
outside the Mesa and QEMU pieces, among them the device list where
libvirt never adds the card's own nodes: the report names them and offers
the command installing what it lacks, shown before the question is put.
The Manage menu splits in three.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
416ed42448 [ADD] qemu manage : retirer la 3D quand le démarrage échoue sur EGL
Le nœud de rendu peut exister sans qu'EGL y démarre : QEMU refuse alors
le domaine, et la 3D écrite dans la définition rend la VM inutilisable
jusqu'à ce que quelqu'un défasse le réglage. La création avait déjà son
repli, le réglage d'une VM existante n'en avait pas. Un relevé accompagne
le refus, car deux briques distinctes entrent en jeu : egl-headless ouvre
le nœud par GBM — c'est Mesa qui répond — et virglrenderer ne sert
qu'ensuite, donc l'installer ne répare pas un EGL qui refuse.

Vérifié : 11 tests, rougis par trois mutations — rattraper n'importe quel
échec, ne pas redémarrer après le retrait, éteindre l'autostart au passage.

--- EN ---

The render node can exist without EGL starting on it: QEMU then refuses
the domain, and the 3D written into the definition leaves the VM unusable
until someone undoes the setting. Creation already had its fallback,
adjusting an existing VM had none. A report comes with the refusal, since
two distinct bricks are involved: egl-headless opens the node through GBM
— Mesa answers — and virglrenderer only serves afterwards, so installing
it repairs no EGL that refuses.

Checked: 11 tests, turned red by three mutations — catching any failure,
not restarting after removal, switching autostart off along the way.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
83c0d705c1 [FIX] qemu stats : un « datetime » de trop faisait tomber le menu
Le module importe la CLASSE datetime, pas le module : « datetime.datetime »
n'existe donc pas, et l'écran des statistiques sortait sur une
AttributeError qui emportait TODO entier. La garde couvre aussi
TypeError : un horodatage absent vaut None dans un résumé sans aucune
installation, et une statistique ne doit pas décider de la survie du menu.

Vérifié : un horodatage valide rend sa date, None et une valeur hors
bornes rendent « ? » sans lever.

--- EN ---

The module imports the datetime CLASS, not the module: « datetime.datetime »
therefore does not exist, and the statistics screen exited on an
AttributeError that took the whole of TODO with it. The guard also covers
TypeError: an absent timestamp is None in a summary with no install at
all, and a statistic must not decide whether the menu survives.

Checked: a valid timestamp renders its date, None and an out-of-range
value render « ? » without raising.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
12cef44991 [FIX] qemu menu : sortir le venv du PATH des outils système
Le « bin » du venv est en tête du PATH de chaque commande lancée par le
menu, et il contient un python3. Un outil système écrit en Python et
amorcé par « env python3 » s'y amorce donc, dans un interpréteur où les
modules de la distribution n'existent pas : l'import échoue sur un module
que la machine possède pourtant. Sous sudo le piège était invisible, sudo
réinitialisant le PATH ; le retirer là où il ne servait plus l'a mis au
jour.

Vérifié : 6 tests, rougis par trois mutations. La ligne d'amorçage des
outils visés n'a pas été inspectée : le mécanisme est démontré, pas qu'il
soit la cause sur un hôte donné.

--- EN ---

The venv's « bin » leads the PATH of every command the menu launches, and
it holds a python3. A system tool written in Python and started through
« env python3 » therefore boots on that interpreter, where the
distribution's modules do not exist: the import fails on a module the
machine does have. Under sudo the trap was invisible, sudo resetting the
PATH; removing it where it was no longer needed brought it out.

Checked: 6 tests, turned red by three mutations. The shebang of the tools
concerned was not inspected: the mechanism is demonstrated, not that it
is the cause on any given host.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
f016eefce9 [ADD] qemu menu : récupérer des fichiers dans le disque d'une VM
Une VM qui ne démarre plus garde ses fichiers : libguestfs monte son
qcow2 sans elle. Toute commande porte « --ro », et c'est ce qui change la
manœuvre : ouvrir en écriture le disque d'une machine allumée corrompt
son système de fichiers, la lire ne risque rien. Un arrêt reste proposé
et non imposé — une lecture vivante voit un état peut-être incohérent,
un fichier à moitié écrit, un journal non rejoué. Le nom du paquet suit
la distribution, celui de Debian n'existant nulle part ailleurs.

Vérifié : 16 tests, rougis par quatre mutations — perdre le « --ro »,
donner le nom Debian partout, dupliquer un numéro, retirer l'entrée.

--- EN ---

A VM that no longer boots still holds its files: libguestfs mounts its
qcow2 without it. Every command carries « --ro », and that is what
changes the operation: opening a running machine's disk for writing
corrupts its filesystem, reading it risks nothing. A shutdown stays
offered, not imposed — a live read sees a possibly torn state, a
half-written file, an unreplayed journal. The package name follows the
distribution, Debian's existing nowhere else.

Checked: 16 tests, turned red by four mutations — losing « --ro », using
Debian's name everywhere, duplicating a number, dropping the entry.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
bf114ac182 [FIX] script todo : une clé i18n dupliquée écrasait un libellé du menu
La section du menu des plugins prenait « Install » pour clé, déjà portée
par l'entrée « 📦 Installation » du menu principal. Dans un littéral de
dict Python la dernière définition gagne, sans erreur ni avertissement :
le menu principal affichait donc « Installer ». La clé de la section est
renommée, la première étant en place depuis longtemps.

Vérifié : le menu principal retrouve « 📦 Installation », et un balayage
des clés ne laisse que trois doublons, tous à valeurs identiques donc
sans effet.

--- EN ---

The plugin menu's section used « Install » as its key, already held by
the main menu's « 📦 Installation » entry. In a Python dict literal the
last definition wins, with no error and no warning: the main menu
therefore showed « Installer ». The section's key is renamed, the first
one having been there far longer.

Checked: the main menu gets « 📦 Installation » back, and a sweep of the
keys leaves only three duplicates, all with identical values and so
without effect.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
3bd9f1edad [FIX] qemu menu : nommer l'URI libvirt, sinon la liste des VM est vide
Sans « --connect », un virsh non root vise qemu:///session : un
hyperviseur SÉPARÉ, où aucune VM du système n'existe. « list --all » y
rend une liste vide, sans erreur ni avertissement. L'URI par défaut de
root masquait l'omission tant que les commandes passaient par sudo ;
appartenir au groupe libvirt donne le droit d'atteindre qemu:///system
mais ne change pas l'URI. Les 40 appels locaux passent donc par un
constructeur unique — dont 19 en liste d'arguments, qui gardaient encore
sudo en dur.

Vérifié : une garde balaie les deux fichiers et échoue si un virsh est
écrit sans URI ; la réintroduire fait rougir.

--- EN ---

Without « --connect », a non-root virsh targets qemu:///session: a
SEPARATE hypervisor, where none of the system's VMs exist. « list --all »
returns an empty list there, with no error and no warning. Root's default
URI masked the omission as long as commands went through sudo; libvirt
group membership grants the right to reach qemu:///system but does not
change the URI. All 40 local calls therefore go through one builder —
19 of them argument lists that still hardcoded sudo.

Checked: a guard sweeps both files and fails if a virsh is written
without the URI; putting one back turns it red.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
f417a9ea86 [ADD] qemu deploy : case 3D à la création, même sans écran virtuel
Une VM sans console peut vouloir un virtio-gpu accéléré — rendu hors
écran, ou émulateur qui tourne dedans — et « auto » ne l'accorde jamais :
il s'abstient sans écran, pour ne pas poser un périphérique vidéo que
personne n'a demandé. La case envoie donc « --gpu on », qui l'accorde
désormais. « --graphics none » est alors écarté : il dit « aucun
affichage », et egl-headless EST un affichage. Le repli le rend quand la
3D tombe, sinon la VM repartirait sur le défaut de virt-install.

Vérifié : 8 tests neufs et 2 sur le repli, rougis par trois mutations.
Le rendu de la case dans le terminal plein écran n'est pas testé.

--- EN ---

A VM without a console may want an accelerated virtio-gpu — offscreen
rendering, or an emulator running inside — and « auto » never grants it:
it abstains without a screen, so as not to add a video device nobody
asked for. The box therefore sends « --gpu on », which now grants it.
« --graphics none » is then dropped: it means « no display », and
egl-headless IS a display. The fallback gives it back when 3D fails,
otherwise the VM would restart on virt-install's default.

Checked: 8 new tests and 2 on the fallback, turned red by three
mutations. The box's rendering in the full-screen terminal is not tested.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
c8d76f1c33 [ADD] qemu deploy : proposer d'effacer un disque orphelin qui bloque
Une création interrompue laisse son qcow2 sans VM définie, et deploy_qemu
refuse ensuite d'écraser : la création échoue APRÈS avoir fait attendre.
Le disque est donc proposé à l'effacement, taille et chemin affichés.
Proposé et non effacé d'office : le même nom peut désigner le disque
d'une VM retirée à la main, dont on voulait garder les données. Un refus
ne laisse pas filer vers l'échec, il redemande. La proposition vient
après la fermeture du formulaire plein écran, parce qu'effacer là demande
root et qu'une invite de mot de passe n'y a nulle part où s'afficher.

Vérifié : 7 tests, rougis par deux mutations — ne pas redemander après un
refus, et sauter la proposition sur le chemin plein écran.

--- EN ---

An interrupted creation leaves its qcow2 with no defined VM, and
deploy_qemu then refuses to overwrite: creation fails AFTER the wait. The
disk is therefore offered for deletion, size and path shown. Offered, not
deleted outright: the same name may designate the disk of a VM removed by
hand, whose data was meant to be kept. A refusal does not drift into
failure, it asks again. The offer comes after the full-screen form
closes, because deleting there needs root and a password prompt has
nowhere to appear inside it.

Checked: 7 tests, turned red by two mutations — not asking again after a
refusal, and skipping the offer on the full-screen path.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
9f94316ab0 [FIX] qemu deploy : recréer la VM sans 3D quand EGL ne démarre pas
Le nœud de rendu existe mais EGL ne s'y initialise pas : QEMU s'arrête
sur « eglInitialize failed » pendant la connexion au moniteur. La
détection ne voit qu'un fichier dans /dev/dri, et rien ne distingue un
GPU utilisable d'un nœud sans pile EGL avant que QEMU n'essaie. La
création réessaie donc sans la 3D, après avoir retiré le domaine de
l'essai raté — sans quoi le nom reste pris. « --gpu on » n'est pas
rétrogradé en silence, et un échec qui n'est pas celui d'EGL n'est pas
rattrapé.

Vérifié : 8 tests et trois mutations. Le chemin réel n'a pas été exécuté,
faute de /dev/dri et de virt-install sur la machine de développement.

--- EN ---

The render node exists but EGL will not initialise on it: QEMU stops on
« eglInitialize failed » while connecting to the monitor. Detection only
sees a file under /dev/dri, and nothing separates a usable GPU from a
node without an EGL stack until QEMU tries. Creation therefore retries
without 3D, after undefining the domain of the failed attempt — the name
would otherwise stay taken. « --gpu on » is not silently downgraded, and
a failure that is not EGL's is not caught.

Checked: 8 tests and three mutations. The real path was not exercised,
for lack of /dev/dri and virt-install on the development machine.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
feb6cd7b12 [FIX] qemu menu : passer par le groupe libvirt plutôt que par sudo
Appartenir au groupe libvirt suffit à joindre qemu:///system : le sudo
écrit en dur n'y ajoutait aucun droit et réclamait un mot de passe à
chaque entrée de menu. La question se tranche en ESSAYANT, jamais en
lisant /etc/group : les groupes d'un processus sont figés à l'ouverture
de session, donc la table dit le déclaré, l'essai le faisable. C'est la
distinction que porte aussi l'avertissement d'avant-installation. Un
hyperviseur distant garde sudo, ses droits ne se sondant pas d'ici.

Vérifié : 10 tests, rougis par deux mutations — lire /etc/group, et
conclure « pas de sudo » sur un sondage mort.

--- EN ---

Membership of the libvirt group is enough to reach qemu:///system: the
hardcoded sudo added no right there and asked for a password at every
menu entry. The question is settled by TRYING, never by reading
/etc/group: a process's groups are frozen at session start, so the table
states what is declared, the attempt what is doable. The pre-install
warning carries that same distinction. A remote hypervisor keeps sudo,
its rights not being probeable from here.

Checked: 10 tests, turned red by two mutations — reading /etc/group, and
concluding « no sudo » from a dead probe.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
d3e8d50953 [FIX] qemu deploy : montrer le message de l'outil quand une VM échoue
Une VM ratée n'affichait que ses quatre dernières lignes, et l'épilogue
« Échec de la commande » avec sa ligne de commande les occupe entièrement :
le message de l'outil tombait juste au-dessus de la fenêtre. La sortie
étant jetée après la boucle, passé l'écran elle n'existait plus nulle
part. Trente lignes en cas d'échec, quatre en cas de réussite, et la
sortie complète va dans un fichier dont le chemin s'affiche. Quand la
commande portait la 3D, le rapport nomme « --gpu off » : le menu ne
l'expose pas, c'est la seule issue depuis là.

Vérifié : 7 tests, dont un nom de VM hostile qui ne doit pas composer un
chemin hors du répertoire de session.

--- EN ---

A failed VM showed only its last four lines, and the « Échec de la
commande » epilogue with its command line fills them entirely: the tool's
own message fell just above the window. The output being discarded after
the loop, past the screen it existed nowhere at all. Thirty lines on
failure, four on success, and the whole output goes to a file whose path
is printed. When the command carried 3D, the report names « --gpu off »:
the menu does not expose it, and it is the only way out from there.

Checked: 7 tests, among them a hostile VM name that must not compose a
path outside the session directory.

Assisted-by: Claude Opus 5
2026-09-03 05:00:55 -04:00
29b12c8ca1 [ADD] qemu arch : yay et bash-completion sur l'invité, guide au MOTD
Une image cloud Arch est nue : ni bash-completion, ni accès à l'AUR. Les
deux arrivent avec l'amorçage, sur la seule branche pacman. yay-bin
plutôt que yay, dont le paquet source compile Go pour le même outil ; la
construction reste sous l'utilisateur de la VM, makepkg refusant root. Le
« || true » qui ferme le bloc porte : le groupe est le dernier membre de
sa liste « || », donc set -e s'y applique et un sudo en échec emporterait
l'installation entière. Le guide de connexion n'annonce yay que si une
installation a eu lieu.

Vérifié : 10 tests, dont « bash -n » sur la commande distante entière et
la survie du bloc sous set -e avec un PATH vide.

--- EN ---

An Arch cloud image is bare: no bash-completion, no AUR access. Both come
with the bootstrap, on the pacman branch alone. yay-bin rather than yay,
whose source package compiles Go for the same tool; the build stays under
the VM user, makepkg refusing root. The « || true » closing the block
carries weight: the group is the last member of its « || » list, so set -e
applies inside it and one failing sudo would take the whole install down.
The login guide announces yay only when an install ran.

Checked: 10 tests, among them « bash -n » over the whole remote command
and the block surviving set -e with an empty PATH.

Assisted-by: Claude Opus 5
2026-09-02 08:04:05 -04:00
736ab3b676 [FIX] qemu setup-host : demander avant de redémarrer l'hôte
Accepter d'installer les paquets QEMU redémarrait la machine sans autre
question : --assume-yes couvrait le gestionnaire de paquets, et la
commande y ajoutait --reboot-if-needed, qui ne demandait rien. Une seule
constante servait la VM qu'on vient de créer et le poste qui la crée.
--reboot-if-needed PROPOSE désormais, sur /dev/tty pour rester visible
quand la sortie est un tuyau, et vaut non par défaut ; un refus laisse
les paquets posés et dit quoi faire. --assume-yes-reboot est le seul
consentement muet, que seul le profil invité porte.

Vérifié : 7 tests, rougis par deux mutations — assume_yes rouvrant la
porte, le menu hôte reprenant le drapeau.

--- EN ---

Accepting the QEMU package install rebooted the machine with no further
question: --assume-yes covered the package manager, and the command
added --reboot-if-needed, which asked nothing. One constant served both
the VM just created and the workstation creating it. --reboot-if-needed
now OFFERS, on /dev/tty so it stays visible when output is a pipe, and
defaults to no; a refusal leaves the packages in place and says what to
do. --assume-yes-reboot is the only silent consent, carried by the guest
profile alone.

Checked: 7 tests, turned red by two mutations — assume_yes reopening the
door, the host menu taking the flag back.

Assisted-by: Claude Opus 5
2026-09-02 07:38:11 -04:00
2435f0f6d3 [ADD] script todo : /todo_generate_code, effort high et règles du dépôt
Les règles à appliquer avant de coder sont éparpillées entre les configs,
les hooks et les modules maison, et la documentation en contredit
plusieurs : autopep8 est recommandé alors que le script sort en 1, aucun
script oca-* n'est installé, la racine n'a ni .pre-commit-config.yaml ni
.pylintrc, et flake8 comme pylint-odoo ne vivent que dans le venv Odoo,
que rien ne lance. Le gabarit énonce ce que l'outillage impose, à effort
high, l'épinglage ultracode restant celui de l'utilisateur.

Vérifié : 146 règles relevées, 142 confirmées contre leur citation, 4
retirées. Deux gardes lient chaque entrée du menu à un gabarit présent
qui déclare le bon nom.

--- EN ---

The rules to apply before coding are scattered across the configs, the
hooks and the in-house modules, and the documentation contradicts several
of them: autopep8 is recommended although the script exits 1, no oca-*
script is installed, the root carries neither .pre-commit-config.yaml nor
.pylintrc, and flake8 as well as pylint-odoo live only in the Odoo venv,
which nothing invokes. The template states what the tooling enforces, at
high effort, the ultracode pin remaining the user's own.

Checked: 146 rules surveyed, 142 confirmed against their citation, 4
dropped. Two guards tie each menu entry to a template that exists and
declares the right name.

Assisted-by: Claude Opus 5
2026-09-02 07:16:23 -04:00
4f85a4f225 [ADD] script todo : /todo_plan_max, planifier avant d'ajouter une entrée
Ajouter une entrée commence par des décisions qu'aucun gabarit
n'imposait : quel menu parent, motif A ou B, quoi faire en cas d'échec,
ce qui est détruit, ce qui touche à une donnée client. La commande les
demande avant d'écrire, planifie avec superpowers quand le plugin est
là, et pose sa spécification dans tasks/, non versionné. Son frontmatter
fixe l'effort à max, seule valeur de l'énumération ; ultracode est un
épinglage de session que l'utilisateur tape, non un réglage de gabarit.

Vérifié : les deux gabarits déployés dans un HOME jetable, relus « à
jour » par l'écran de contexte.

--- EN ---

Adding an entry starts with decisions no template forced: which parent
menu, pattern A or B, what to do on failure, what gets destroyed, what
touches customer data. The command asks them before writing, plans with
superpowers when the plugin is there, and lays its specification in
tasks/, which is not versioned. Its frontmatter sets effort to max, the
only value in the enumeration; ultracode is a session pin the user
types, not a template setting.

Checked: both templates deployed into a throwaway HOME, read back as up
to date by the context screen.

Assisted-by: Claude Opus 5
2026-09-02 06:35:57 -04:00
6847891c1b [ADD] script todo : gérer les plugins Claude Code et leur liste ERPLibre
Rien ne posait un plugin depuis TODO ; la CLI seule le faisait, hors du
menu. L'installation passe par « -y » : la sortie de TODO est un tuyau,
pas un terminal, et la CLI refuse sans lui toute installation qui
exécute une commande déclarée par un marketplace. La liste préférée
s'affiche donc AVANT la confirmation, seule occasion de la lire ; ses
quatre plugins travaillent sur le poste, sans service tiers ni compte.
La recherche lit les manifestes sur le disque, donc hors ligne.

Vérifié : 8 tests neufs, dont la frontière de mot qui sépare deux noms
dont l'un contient l'autre — une recherche naïve en rate trois.

--- EN ---

Nothing installed a plugin from TODO; the CLI alone did, outside the
menu. Installing goes through « -y »: TODO's output is a pipe, not a
terminal, and without it the CLI refuses any install that runs a
command declared by a marketplace. The preferred list is therefore
shown BEFORE the confirmation, the only chance to read it; its four
plugins run on the workstation, with no third party and no account.
Search reads the manifests from disk, hence offline.

Checked: 8 new tests, among them the word boundary separating two names
where one contains the other — a naive search misses three of them.

Assisted-by: Claude Opus 5
2026-09-02 06:35:05 -04:00
c32e7d87c9 [ADD] script todo : déployer /git_prepare_merge depuis le menu Claude
Une fusion apporte plusieurs commits d'un coup, et rien ne guidait les
deux écrits qu'elle demande : l'entrée de changelog et le message de
merge. La commande déployée impose la source — CHANGELOG.base.md, les
fichiers générés étant perdus au prochain doc_markdown — et le corps
bilingue avec son trailer, que le garde-fou ne verra jamais, commit-msg
ignorant tout message ouvrant sur « Merge ». Elle prépare, elle ne
fusionne pas : le message part dans tasks/, non versionné.

Vérifié : 45 tests du menu, et un déploiement dans un HOME jetable que
l'écran de contexte relit « à jour » face à son gabarit.

--- EN ---

A merge lands several commits at once, and nothing guided the two pieces
of writing it needs: the changelog entry and the merge message. The
deployed command imposes the source — CHANGELOG.base.md, the generated
files being lost at the next doc_markdown — and the bilingual body with
its trailer, which the guard rail never sees, commit-msg skipping any
message opening on « Merge ». It prepares, it does not merge: the
message goes to tasks/, which is not versioned.

Checked: 45 menu tests, and a deployment into a throwaway HOME that the
context screen reads back as up to date against its template.

Assisted-by: Claude Opus 5
2026-09-02 06:15:43 -04:00
8bdbf42a31 [ADD] script todo : installer Claude Code et opencode, PATH garanti
Ces installateurs posent leur binaire dans un répertoire du HOME que le PATH
d'un shell ne porte pas toujours : sans la ligne d'export, le binaire est là
et la commande reste introuvable. Le répertoire diffère d'un outil à l'autre,
et l'un des deux écrit déjà sa propre ligne — la présence se teste donc sur
le répertoire, pas sur la graphie, et rien n'est ajouté deux fois.

Le PATH d'un processus est figé depuis son démarrage : un nouveau shell est
nécessaire, ce que la sortie dit. Troisième écrivain dans le fichier de
shell, starship compris, d'où l'écriture mise en commun. Vérifié : 31 tests.

--- EN ---

These installers put their binary in a HOME directory that a shell's PATH
does not always carry: without the export line, the binary is there and the
command stays not found. The directory differs from one tool to the other,
and one of the two already writes its own line — presence is therefore tested
on the directory, not on the spelling, and nothing is added twice.

A process PATH is frozen since its start: a new shell is needed, which the
output says. Third writer into the shell file, starship included, hence the
shared write. Checked: 31 tests.

Assisted-by: Claude Opus 5
2026-09-02 04:51:03 -04:00
371605b90a [IMP] script todo : une icône par entrée du menu Git et Shell
Sept entrées sans repère visuel se lisent une par une. L'icône entre dans la
traduction, qui porte déjà le libellé affiché.

Un glyphe à présentation texte occupe une colonne quand les autres en
occupent deux, et décale la colonne des libellés : il prend deux espaces,
comme 🖥 et ⚙ ailleurs dans le fichier. Rendu vérifié dans les deux langues.

--- EN ---

Seven entries with no visual marker are read one by one. The icon goes into
the translation, which already carries the displayed label.

A text-presentation glyph takes one column where the others take two, and
shifts the label column: it gets two spaces, like 🖥 and ⚙ elsewhere in the
file. Rendering checked in both languages.

Assisted-by: Claude Opus 5
2026-09-02 04:43:43 -04:00
b321052888 [ADD] script todo : Starship depuis le menu Git, renommé Git et Shell
Poser le binaire ne change pas le prompt : c'est la ligne d'initialisation
dans le fichier du shell qui le fait, et les deux étapes échouent séparément.
Le paquet vient de la distribution quand elle le connaît, de l'installateur
amont sinon — starship n'est pas empaqueté partout. Le fichier du shell n'est
demandé que devant plusieurs candidats, et la ligne ne s'écrit qu'une fois.

L'entrée porte sa destination dans « method » : son rang suit le nombre
d'entrées de todo.json, qu'un numéro codé en dur ignorerait. Vérifié : 18
tests.

--- EN ---

Laying down the binary does not change the prompt: the init line in the shell
file does, and the two steps fail separately. The package comes from the
distribution when it knows it, from the upstream installer otherwise —
starship is not packaged everywhere. The shell file is only asked for when
several candidates exist, and the line is written only once.

The entry carries its destination in « method »: its rank follows the number
of todo.json entries, which a hard-coded number would ignore. Checked: 18
tests.

Assisted-by: Claude Opus 5
2026-09-02 04:43:25 -04:00
d31ca9eab9 [UPD] qemu manage : nom de VM sans la version d'une publication continue
Une distribution en publication continue n'a qu'une version, latest : le
segment ne distingue aucune VM d'une autre et sort du nom. Une version
nommée qui coexiste avec d'autres au catalogue y reste, tumbleweed comme
les numérotées.

Le nom se relit dans l'autre sens pour retrouver (distro, version) et
filtrer les outils par distribution : une VM déjà déployée sous l'ancien
nom n'y est plus résolue, la renommer suffit. Vérifié : catalogue rejoué
sans collision, aller-retour du nom, 6 tests.

--- EN ---

A rolling-release distribution has a single version, latest: the segment
tells no VM apart from another and leaves the name. A named version that
coexists with others in the catalogue stays, tumbleweed as much as the
numbered ones.

The name is read back the other way to recover (distro, version) and filter
tools by distribution: a VM already deployed under the old name no longer
resolves there, renaming it is enough. Checked: catalogue replayed without
collision, name round trip, 6 tests.

Assisted-by: Claude Opus 5
2026-09-02 04:13:48 -04:00
93ffb3b7e9 [ADD] script todo : poser merge.conflictStyle=zdiff3 depuis le menu Git
zdiff3 fait figurer la base commune dans les marqueurs de conflit et sort
de la zone contestée les lignes que les deux côtés ont en commun : il reste
moins à arbitrer à la main. Le style demande git 2.35, que toutes les
plateformes supportées dépassent.

La valeur est relue après écriture, « git config » ne rendant rien à
l'écriture. L'entrée est déclarée dans TestGitMenuNumbering : le menu Git
mêle entrées codées en dur et entrées de todo.json, qu'un rang de plus
décale. Vérifié : 117 tests au vert.

--- EN ---

zdiff3 puts the merge base into the conflict markers and lifts out of the
contested area the lines both sides share: less is left to arbitrate by
hand. The style needs git 2.35, which every supported platform exceeds.

The value is read back after writing, as « git config » returns nothing on
write. The entry is declared in TestGitMenuNumbering: the Git menu mixes
hard-coded entries with todo.json ones, which one more rank shifts.
Checked: 117 tests green.

Assisted-by: Claude Opus 5
2026-09-02 04:01:10 -04:00
3dcccf40d9 [FIX] script todo : rtk hors du PATH, résultat d'installation annoncé
Un processus garde le PATH qu'il avait au démarrage : rtk installé dans
~/.local/bin pendant que TODO tourne échappe à shutil.which, et « rtk » nu
sort en 127. Le menu le cherche donc aussi à l'emplacement de
l'installateur et l'appelle par son chemin absolu, sans confondre un
binaire hors PATH avec une absence.

L'installation annonce son résultat : version et chemin, ou échec.
Vérifié : 9 tests neufs, 117 au total.

--- EN ---

A process keeps the PATH it had at startup: rtk installed into
~/.local/bin while TODO runs escapes shutil.which, and a bare « rtk »
exits 127. The menu therefore also looks at the installer's location and
calls the binary by its absolute path, without mistaking a binary outside
the PATH for a missing one.

Installation now reports its outcome: version and path, or failure.
Checked: 9 new tests, 117 in total.

Assisted-by: Claude Opus 5
2026-09-02 03:53:40 -04:00
1cc74c83b4 [FIX] qemu shrink : place mesurée avant sauvegarde, étape annoncée
La sauvegarde double la place occupée et le défaut était OUI : sur un
disque presque plein, une entrée vide lançait une copie qui s'arrête à
mi-course et laisse un .bak tronqué. Les deux tailles passent donc avant
la question, et le défaut bascule à NON quand la place manque.

Le besoin annoncé est la taille ALLOUÉE : « cp --sparse=always » ne
recopie pas les trous d'un qcow2. La sortie du gestionnaire de paquets
enchaînait par ailleurs sur une question portant sur autre chose ;
l'étape se referme d'une ligne.

Vérifié : le défaut remis à OUI sans place, comme la taille apparente au
lieu de l'allouée, font tomber les tests. 4144 verts.

--- EN ---

A backup doubles the space used and the default was YES: on a nearly
full disk, an empty answer started a copy that stops midway and leaves a
truncated .bak. Both sizes now come before the question, and the default
flips to NO when the room is short.

The need shown is the ALLOCATED size: "cp --sparse=always" does not copy
the holes of a qcow2. The package manager output also ran straight into a
question about something else; the step now closes with a line of its
own.

Checked: putting the default back to YES without room, like the apparent
size instead of the allocated one, makes the tests fail. 4144 green.

Assisted-by: Claude Opus 5
2026-08-31 07:51:19 -04:00
87ad8c4439 [ADD] script todo : installer les outils manquants, les quatre familles
La réduction sûre listait les outils manquants et s'arrêtait là. Trois
écritures séparées savaient installer, chacune un sous-ensemble
différent : openSUSE posait virt-viewer, mais ni navigateur CLI ni
lm-sensors.

Le binaire n'est presque jamais le paquet : sgdisk vit dans « gdisk »
chez Debian et Fedora, dans « gptfdisk » chez Arch et openSUSE. Deux
règles passent aussi dans la composante : l'ID de la distribution décide
avant le PATH, et la commande s'affiche avant la question.

Vérifié : commandes identiques sur les trois familles déjà couvertes,
4139 tests verts.

--- EN ---

The safe shrink listed the missing tools and stopped there. Three
separate writings knew how to install, each covering a different
subset: openSUSE could put virt-viewer down, but neither a CLI browser
nor lm-sensors.

A binary is almost never the package: sgdisk lives in "gdisk" on Debian
and Fedora, in "gptfdisk" on Arch and openSUSE. Two rules move into the
component as well: the distribution ID decides before the PATH, and the
command is shown before the question.

Checked: commands identical on the three families already covered, and
4139 tests green.

Assisted-by: Claude Opus 5
2026-08-31 07:18:13 -04:00
40a1e178f3 [FIX] qemu manage : ni adresse, ni nom de VM, ni récit en commentaire
Les commentaires suivent le dépôt en amont : une adresse ou un nom de
machine qui s'y trouve devient public. Cinq adresses et deux noms de VM
y étaient, plus cinq phrases racontant la séance où le défaut est apparu.

Le mode de défaillance reste, au présent — « une VM renommée se voit
attribuer la passerelle » — et l'incident part. Un relevé d'un seul jour
part avec lui ; la structure durable reste.

check_comment_hygiene.py passe de 5 identifiants et 5 à relire à zéro.
Aucune ligne de code n'est touchée, seulement du texte de commentaire.

--- EN ---

Comments follow the repository upstream: an address or a machine name
sitting in one becomes public. Five addresses and two VM names were
there, plus five sentences telling the session where the fault appeared.

The failure mode stays, in the present — "a renamed VM is given the
gateway" — and the incident goes. A reading taken on one day goes with
it; the durable structure stays.

check_comment_hygiene.py goes from 5 identifiers and 5 to re-read down to
zero. No line of code is touched, only comment prose.

Assisted-by: Claude Opus 5
2026-08-31 07:17:22 -04:00
0b8f25fb4b [ADD] script todo : installer les hooks git depuis le menu Git
Un commit-msg non installé, c'est le garde-fou de convention qui ne tourne
jamais, et la pose restait une commande à recopier. git saute sans rien
dire un hook privé du bit d'exécution : l'installation le remet.

exec_command_live ne passe aucun cwd et ce dépôt porte 128 dépôts
imbriqués : sans « git -C racine », un lancement depuis un addon y écrivait
core.hooksPath et laissait la racine sans garde-fou. Et comme cette
fonction retourne le code sans jamais lever, un « fatal: not in a git
directory » annonçait « hooks installés ».

Vérifié : 4114 tests verts ; le nouveau garde tombe sur un dispatch
échangé comme sur une entrée sans branche.

--- EN ---

An uninstalled commit-msg is the convention guard never running, and
installing it stayed a command to copy by hand. git skips a hook without
the execution bit and says nothing: installing restores it.

exec_command_live passes no cwd and this checkout holds 128 nested
repositories: without the "git -C root", a launch from an addon wrote
core.hooksPath there and left the root unguarded. And since that function
returns the exit code without ever raising, a "fatal: not in a git
directory" still announced "hooks installed".

Checked: 4114 tests green; the new guard fails on a swapped dispatch as
well as on an entry with no branch.

Assisted-by: Claude Opus 5
2026-08-31 06:17:48 -04:00
54f44755cd [I18N] commit-msg : le garde-fou parle la langue du dépôt
Les messages du garde-fou étaient des littéraux français : l'anglais n'était
pas supporté, quelle que soit la valeur de EL_LANG. Onze clés passent
désormais par t(), pour 6 ms de surcoût mesuré par commit. L'en-tête
annonçait « sujet » alors que le contrôle lit aussi le corps ; il dit
« message ».

Les tests ne passaient que parce que le dépôt est en français : un poste en
anglais en cassait quarante. La langue devient une précondition — épinglée en
mémoire, car set_lang() écrirait un fichier suivi — et les assertions qui
citent du texte français se sautent en nommant leur raison.

Vérifié en basculant EL_LANG : 48 tests OK en français, 2 sautés en anglais.

--- EN ---

The guard rail's messages were French literals: English was not supported at
all, whatever EL_LANG said. Eleven keys now go through t(), for a measured
6 ms of overhead per commit. The header announced "subject" while the check
reads the body too; it says "message".

The tests passed only because the repository is in French: a workstation in
English broke forty of them. The language becomes a precondition — pinned in
memory, since set_lang() would write a tracked file — and the assertions
quoting French text skip while naming their reason.

Checked by switching EL_LANG: 48 tests OK in French, 2 skipped in English.

Assisted-by: Claude Opus 5
2026-08-31 00:50:31 -04:00
fcad37c24f [FIX] suite unitaire : bit d'exécution, script/*.py balayé, clé i18n
Trois contrôles préexistants passaient au rouge sur cette branche.

Un fichier qui porte un shebang doit être exécutable : commité en 644, il
casse deux invariants, le mode stocké par git et le bit sur disque. Ses six
frères de script/analyse/ sont en 755.

Le contrôle d'outillage ne balayait pas script/*.py : un module partagé posé
là passait pour un paquet tiers non déclaré. Trois fichiers de plus entrent
dans le balayage, sans révéler d'autre dépendance. Et une clé de traduction
appelée six fois n'était pas déclarée, l'écran rendant un mot anglais.

Vérifié : 4105 tests, tout vert.

--- EN ---

Three pre-existing checks were turning red on this branch.

A file carrying a shebang must be executable: committed as 644, it breaks two
invariants, the mode git stores and the bit on disk. Its six siblings in
script/analyse/ are 755.

The tooling check never scanned script/*.py: a shared module placed there was
taken for an undeclared third-party package. Three more files enter the scan,
revealing no other dependency. And a translation key called six times was
undeclared, the screen rendering an English word.

Checked: 4105 tests, all green.

Assisted-by: Claude Opus 5
2026-08-31 00:03:50 -04:00
78e702e1e3 [ADD] todo : afficher le contexte fourni à Claude
Le contexte d'un assistant est éparpillé sur six sources — instructions,
règles, skills, commandes déployées, hooks git, mémoire — et rien ne disait
laquelle manquait ni laquelle avait dérivé.

L'écran les relève. Chaque commande déployée est comparée à son gabarit du
dépôt en ignorant les lignes d'identité git, que le déploiement substitue :
une copie périmée se voit, là où une égalité stricte les déclarerait toutes
périmées. Les termes de la liste privée sont COMPTÉS, jamais affichés.

--- EN ---

An assistant's context is scattered over six sources — instructions, rules,
skills, deployed commands, git hooks, memory — and nothing said which one was
missing nor which one had drifted.

The screen reports them. Each deployed command is compared to its repository
template, ignoring the git identity lines the deployment substitutes: a stale
copy shows up, where a strict equality would declare them all stale. The
private list's terms are COUNTED, never displayed.

Assisted-by: Claude Opus 5
2026-08-30 06:14:05 -04:00
0e5441ac39 [ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et
docstrings, jamais le code autour, et rend deux familles inégales :
l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée —
est une trouvaille ; le récit — témoignage, date, personne — un signal à relire.

Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle
bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue.
La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et
l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un.

L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six
fichiers d'essai touchés passent.

--- EN ---

Nothing reported the comments that break the convention. The tool reads
comments and docstrings, never the code around them, and returns two unequal
families: identifying data — address, e-mail, account path, private-list name
— is a finding; narrative — witness marker, date, person — a signal to re-read.

The pre-commit hook runs it on the index, always exits 0 — a blocking check at
that scale gets uninstalled — and speaks when the tool fails. The generator
rule and its doc carry the clean-up as you go, and a forbidden thing's example
is invented: a real database, address, account and host each take one.

The context screen is in place, with no menu entry. Checked: 252 tests of the
six touched fixture files pass.

Assisted-by: Claude Opus 5
2026-08-30 06:08:30 -04:00
7844387fa1 [ADD] commit-msg : contrôler le corps, sa longueur et l'identifiant
Le garde-fou ne lisait que le sujet : le corps portait des noms de bases
réelles, des adresses et des chemins de compte, et sa longueur médiane
faisait une fois et demie le plafond de la convention.

Il lit désormais le corps, trailers et diff de --cleanup=scissors exclus :
longueur par langue, adresse IP, courriel, chemin de compte, et les termes
d'une liste qui vit hors du dépôt puisque c'est elle qu'elle protège —
absente, le contrôle se tait. Le prédicat qui sépare une adresse d'une
version de manifeste est partagé ; 40 tests couvrent les deux lectures.

--- EN ---

The guard rail read the subject only, while the body carried real database
names, addresses and account paths, and its median length was one and a
half times the convention's ceiling.

It now reads the body, trailers and the --cleanup=scissors diff excluded:
length per language, IP address, e-mail, account path, and the terms of a
list living outside the repository, since the list is what it protects —
absent, that check stays silent. The predicate telling an address from a
manifest version is shared; 40 tests cover both readings.

Assisted-by: Claude Opus 5
2026-08-30 05:11:36 -04:00
17295fd7c3 [ADD] migration : mesurer la dérive du plan comptable entre paliers
Une montée de version recharge le gabarit de localisation sans rien dire :
trois des scripts l10n du noyau appellent try_loading() sans
force_create=False, et tout compte que le gabarit n'apparie pas par code
est CRÉÉ. Les groupes ajoutés reclassent alors le plan existant.

Un compte absolu ne dit rien : seul l'ÉCART entre deux paliers de la même
migration se juge — comptes sans écriture, noms homonymes, grille apparue
de rien. L'outil ne répare pas : la clé naturelle de suppression attrape
aussi des comptes réappariés par code, et des ajoutés sont accrochés en ON
DELETE SET NULL. Il dit de rejouer le palier ; 24 tests le couvrent.

--- EN ---

A version bump reloads the localisation template silently: three of the
core l10n scripts call try_loading() without force_create=False, and every
account the template cannot match by code is CREATED. The groups thus
added then reclassify the existing chart.

An absolute count says nothing: only the GAP between two steps of one
migration can be judged — accounts with no entry, homonymous names, a
grouping grid from nothing. The tool does not repair: its natural deletion
key also catches accounts re-matched by code, and added ones are pinned by
ON DELETE SET NULL. It says to replay the step; 24 tests cover it.

Assisted-by: Claude Opus 5
(cherry picked from commit e99575967e0ad5b5122a2c35df3b9eaef7a72d53)
2026-08-29 02:11:04 -04:00
d2832f5c60 [FIX] migration 17→18 : retirer la vue morte account_root
Odoo 17 créait une vue SQL pour account.root ; en 18 le modèle porte
_auto = False et _table_query = '0' : son nom n'entre plus dans aucune
requête, et le contrôle des tables manquantes ignore ces modèles.

Elle n'est pas seulement morte, elle est FAUSSE : bâtie sur la colonne code que
l'ORM 18 n'écrit plus, elle rend trop peu de racines. Elle est aussi l'unique
épingle des deux colonnes héritées où database_cleanup échoue, d'où ce retrait
avant le chargement en 18 ; aucun DROP COLUMN, OpenUpgrade les lit encore.

Sur base jetable, l'ordre est rejouable et laisse intactes une vue d'une
autre forme, une vue dont dépend un objet, une table homonyme.

--- EN ---

Odoo 17 created a SQL view for account.root; in 18 the model carries
_auto = False and _table_query = '0': its name no longer enters any query,
and the missing-table check ignores such models.

It is not merely dead, it is WRONG: built on the code column the 18 ORM no
longer writes, it returns too few roots. It is also the sole pin holding the
two legacy columns database_cleanup fails on, hence its removal before the
load into 18; no DROP COLUMN, OpenUpgrade still reads them afterwards.

On a throwaway database the statement replays cleanly and leaves alone a
view of another shape, a view something depends on, a table of that name.

Assisted-by: Claude Opus 5
(cherry picked from commit b50e2308cbdd1efdfa914497062919eab1bbd329)
2026-08-29 02:11:04 -04:00