Commit graph

352 commits

Author SHA1 Message Date
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
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
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
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
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
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
809273eb34 [FIX] verdicts de migration : distinguer trouvaille et échec d'outil
L'écran peignait en échec tout code non nul, quand la convention écrite
dans todo_upgrade.run_tool dit : 0 rien à signaler, 1 des trouvailles, 2
l'outil a échoué. database_cleanup imprime lui-même « This is a warning,
not a failure » avant de rendre 1.

Un écran qui contredit l'outil apprend à ignorer les deux : du rouge
signalait une migration en échec là où rien n'avait échoué.

L'icône rejoint la couleur dans migration_status, et le panneau écrit le
sens du chiffre à côté de lui : « statut 1 (des trouvailles) ». Testé.

--- EN ---

The screen painted every non-zero code as a failure, where the convention
written in todo_upgrade.run_tool says: 0 nothing to report, 1 findings, 2
the tool failed. database_cleanup itself prints "This is a warning, not a
failure" before returning 1.

A screen that contradicts the tool teaches you to ignore both: red marked
a migration as failed where nothing had failed.

The icon now lives beside the colour in migration_status, and the panel
spells the number out: "status 1 (findings)". Covered by tests.

Assisted-by: Claude Opus 5
(cherry picked from commit 39d122965e7a09d710255fff061a7526a4077aaa)
2026-08-29 02:11:04 -04:00
b7d68cd199 [FIX] odoo_bin : lire le config.conf du dépôt, pas le ~/.odoorc
« odoo_bin.sh db --drop » échoue par AccessDenied à chaque palier de
migration, et le clone bute ensuite sur « database already exists ».

db_restore.py lit ./config.conf, y voit « admin_passwd = admin » et
n'envoie donc aucun mot de passe ; odoo_bin.sh ne passait pas de « -c »,
donc Odoo lisait ~/.odoorc, qui porte un mot de passe haché.

ODOO_RC ferme la couture en un point plutôt qu'à vingt sites d'appel :
les versions 12 à 18 le lisent après « -c » et avant ~/.odoorc, donc un
choix explicite l'emporte. Vérifié : le drop passe, la précédence est testée.

--- EN ---

"odoo_bin.sh db --drop" fails with AccessDenied at every migration step,
and the clone then hits "database already exists".

db_restore.py reads ./config.conf, sees "admin_passwd = admin" and so
sends no master password; odoo_bin.sh passed no "-c", so Odoo read
~/.odoorc, which carries a hashed password.

ODOO_RC closes the seam in one place instead of twenty call sites:
versions 12 to 18 read it after "-c" and before ~/.odoorc, so an explicit
choice wins. Verified: the drop succeeds, and a test covers precedence.

Assisted-by: Claude Opus 5
(cherry picked from commit 9503d80982b725034513b610b527f29d49025381)
2026-08-29 02:11:04 -04:00
7ba3c6504c [UPD] invites : 15 secondes pour décider, et le fichier nomme la base
Cinq secondes ne suffisaient pas à LIRE la question. Le compte à rebours
n'existe pas pour aller vite mais pour qu'on puisse s'absenter ; trop
court, il fait l'inverse — on répond par réflexe, ou l'on subit un défaut
non lu. Et « test » ne disait rien de ce qu'on migrait : des migrations
successives portaient toutes le même nom, quand le fichier de sauvegarde
en porte déjà un parlant. Le nom est assaini — il finit dans un createdb —
et borné à 41 caractères : le pilote ajoute « _neutralize_upgrade_18 » et
PostgreSQL tronque à 63, ce qui ferait finir deux paliers sur le même nom.
Un téléchargement distant garde celui que le serveur a donné.

--- EN ---

Five seconds were not enough to READ the question. The countdown does not
exist to go fast but so you can step away; too short, it does the
opposite — you answer by reflex, or you get a default you never read. And
"test" said nothing about what was being migrated: successive migrations
all carried the same name, when the backup file already carries a telling
one. The name is sanitised — it ends up in a createdb — and capped at 41
characters: the driver appends "_neutralize_upgrade_18" and PostgreSQL
truncates at 63, which would land two steps on one name. A remote
download keeps the name the server gave.

Assisted-by: Claude Opus 5
(cherry picked from commit dad0f6b0ef4551898fe2964e177ff5c1c888b5dc)
2026-08-29 02:11:04 -04:00
f6aa2575c3 [ADD] migration : garder la sortie des tests, par pseudo-terminal
Le journal d'étape notait la commande et son code de retour, jamais ce
qu'elle avait écrit : l'écran d'analyse ne pouvait rien montrer d'un
échec. Un tube aurait capturé et changé le programme — smoke_public_url
appelle can_ask(), qui exige stdin ET stdout sur un terminal, et derrière
un tube il cesse en silence d'offrir la réparation des vues COW. Un
pseudo-terminal lève le dilemme : l'enfant voit un vrai terminal, la
réponse tapée lui parvient, le code de retour survit. Neuf exécutions y
passent, dont check_hidden_models, qui tournait sans verdict retenu ;
deux restent dehors, pty.spawn naît en 0×0 et un plein écran s'y perdrait.

--- EN ---

The step log recorded the command and its exit code, never what it
wrote: the analysis screen could show nothing of a failure. A pipe would
have captured and changed the program — smoke_public_url calls can_ask(),
which requires stdin AND stdout to be terminals, and behind a pipe it
silently stops offering the COW view repair. A pty settles it: the child
sees a real terminal, a typed answer reaches it, the exit code survives.
Nine runs go through it, including check_hidden_models, which ran with no
verdict recorded; two stay out, pty.spawn starts at 0×0 and a full-screen
app would lay out on nothing.

Assisted-by: Claude Opus 5
(cherry picked from commit 81e9227502a0d16e6409338cf958495a5f8e3827)
2026-08-29 02:11:04 -04:00
1f606fed00 [ADD] verdicts : tous les paliers, leur journal, et la bascule
L'écran de qualité ne listait que les échecs, quand la question devant
une base migrée est « qu'a-t-on vérifié » : les quatorze verdicts
s'affichent, de la 12 à la 18, seule façon de voir qu'un échec a été
rattrapé à un palier plus haut. Le panneau ne portait que la commande ;
il montre le passage du journal d'étape qui l'entoure, garde par tee ce
qu'il lance lui-même et le relit sans relancer. La sortie de l'outil,
elle, part sur le terminal : un tube ferait renoncer les pleins écrans.
Relancer un test d'un autre palier ouvrait la base avec la mauvaise
version, qui y écrit avant d'échouer ; l'écran demande avant de basculer.

--- EN ---

The quality screen listed failures only, when the question in front of a
migrated database is "what did we check": all fourteen verdicts now show,
12 through 18, the only way to see that a failure at one tier was
recovered higher up. The panel carried only the command; it shows the
step-log passage around it, keeps by tee what it runs itself and re-reads
that without rerunning. The tool output goes to the terminal: a pipe
would make full-screen tools give up. Replaying a test from another tier
opened the database with the wrong version, which writes before it
fails; the screen asks before switching the checkout.

Assisted-by: Claude Opus 5
(cherry picked from commit 2d460b7c777d39a887dabfe7cf5405864c6c3f8c)
2026-08-29 02:11:04 -04:00
8fe0bafe10 [ADD] migration : détecter le type de vue tree resté dans les sources
Odoo 18 a supprimé le type de vue tree, sans conversion : un module
resté dessus ne s'installe pas, ou casse au clic (« View types not
defined tree found in act_window »). Les autres outils de la revue
lisent la BASE, où un module jamais installé ne laisse rien.

Le mot reste un identifiant valide : sur 465 occurrences, 80 cassent — le
noyau 18 garde account.view_invoice_tree en <list>. D'où lxml et ast,
jamais de regex : seule la position du littéral décide. Trois portes
filtrent avant tout motif : version, module installable, fichier chargé.
Balayage : 80 constats sur 2654 modules, 26 touchés.

--- EN ---

Odoo 18 removed the tree view type with no shim: a module still on it
fails to install, or breaks on click ("View types not defined tree found
in act_window"). The other review tools read the DATABASE, where a
module that never installed leaves nothing.

The word is still a valid identifier: of 465 occurrences, 80 break — the
18 core keeps account.view_invoice_tree in <list>. Hence lxml and ast,
never regex: only the literal's position decides. Three gates filter
before any pattern: version, installable module, loaded file. Sweep: 80
findings over 2654 modules, 26 affected.

Assisted-by: Claude Opus 5
(cherry picked from commit 38a91039d111b3fa6de32edf315c5caae8553c11)
2026-08-29 02:11:04 -04:00
599401b6c4 [ADD] manifestes : détecter un dépôt absent d'un palier de migration
Une migration 12 → 18 traverse six paliers, et le checkout bascule de
manifeste à chaque fois. Un dépôt d'addons absent du manifeste du 17
n'existe pas sur disque pendant l'étape 17 : Odoo déclare ses modules
introuvables, le pilote propose de les effacer, on répond oui, et la
fonctionnalité part sans qu'aucun échec ne soit signalé.

Seul le trou compte — présent avant et après, absent au milieu — et la
branche en amont le confirme : sur 35 trous, 19 sont de vraies
omissions, quinze déclarées en 16 et 18 mais pas en 17 ; les 35 auraient
fait 46 % de bruit. development.git est rétabli en 13 et en 15.

--- EN ---

A 12 → 18 migration crosses six steps, and the checkout switches
manifest each time. An addons repository missing from the 17 manifest
does not exist on disk during step 17: Odoo reports its modules as
missing, the driver offers to delete them, you answer yes, and the
feature is gone without a single failure being reported.

Only the hole counts — present before and after, absent in between — and
the upstream branch confirms it: of 35 holes, 19 are real omissions,
fifteen declared in 16 and 18 but not in 17; all 35 would have been 46%
noise. development.git is restored in 13 and in 15.

Assisted-by: Claude Opus 5
(cherry picked from commit d7613ea930d45153e9fb7c08460690a95e3dca0e)
2026-08-29 02:11:04 -04:00
33cf04c7f9 [REM] résidus : retirer le contrôle res_lang.active à NULL
Le contrôle jugeait cassée une langue dont `active` est NULL : listée
nulle part, plus réactivable. Faux dans la source 18 — un domaine
('active','=',False) compile en (IS NULL OR = FALSE) (models.py:3217),
l'action du menu Langues porte active_test: False, le tri passe par
COALESCE(active, FALSE) et la lecture rend bool(value).

Odoo écrit ce NULL : active = fields.Boolean() sans défaut
(res_lang.py:64) et un res.lang.csv sans la colonne, soit un NULL par
langue ajoutée au catalogue. « Zéro avant, non nul après » ne suffit
donc pas. Un test refuse désormais une clé que plus rien ne définit.

--- EN ---

The check called a language broken when `active` is NULL: listed
nowhere, no longer re-enablable. False in the 18 source — an
('active','=',False) domain compiles to (IS NULL OR = FALSE)
(models.py:3217), the Languages menu action carries active_test: False,
sorting goes through COALESCE(active, FALSE), reading returns bool(value).

Odoo writes that NULL itself: active = fields.Boolean() with no default
(res_lang.py:64) and a res.lang.csv without the column, one NULL per
language added to the catalogue. So "zero before, nonzero after" is not
enough. A test now refuses a key that nothing defines any more.

Assisted-by: Claude Opus 5
(cherry picked from commit ee932334cfde63c1af831316549a432c3d382721)
2026-08-29 02:11:04 -04:00
c95cf2e9d8 [ADD] qualité de migration : verdicts, sources, revue
Le rapport comparait les paliers sans dire si la migration avait réussi,
alors que les verdicts dorment déjà dans lst_event du journal de
progression : des contrôles en échec y restent sans remonter nulle part.

Trois sections s'ajoutent sous les paliers : les verdicts, rattachés au
palier ODOO et non au compteur du pilote, décalé d'un rang ; où vivent les
traces, car config.conf laisse logfile= vide et la sortie d'Odoo meurt avec
le terminal ; et la revue, six étapes lançables par « r ». Le contrôle de
résidus porte la même section sans toucher son code de sortie : un verdict
vient du fichier, pas de la base.

--- EN ---

The report compared the tiers without saying whether the migration had
succeeded, while the verdicts already sit in lst_event of the progression
file: failed checks stay there and surface nowhere.

Three sections are added below the tiers: the verdicts, tied to the ODOO
tier and not to the driver counter, which is off by one; where the traces
live, since config.conf leaves logfile= empty and Odoo's output dies with
the terminal; and the review, six steps runnable with "r". The residue
check carries the same section without touching its exit code: a verdict
comes from the file, not from the database.

Assisted-by: Claude Opus 5
(cherry picked from commit b05e0333c4b94d58eb794f09a2459e41d826c655)
2026-08-29 02:11:04 -04:00
4a90f9face [FIX] tests : balayer test/test_*.py et refuser un fichier muet
Le lanceur ne prenait que sept préfixes de noms, « le reste demandant une
base de données » : les 3703 tests du répertoire passent avec PostgreSQL
injoignable, et la suite n'en exécutait que 1131. Il balaie test/test_*.py.

Deux formes rendent un fichier muet sans erreur : unittest.main() posé en
plein milieu, qui sort avant que la seconde moitié soit définie (quatre
fichiers, 87 tests), et l'absence de bloc __main__, qui compte zéro (huit
fichiers, 174 tests). Une garde refuse ces deux formes et le retour d'une
liste de préfixes.

--- EN ---

The runner took seven filename prefixes only, "the rest needing a
database": all 3703 tests in the directory pass with PostgreSQL
unreachable, and the suite ran 1131 of them. It now globs test/test_*.py.

Two shapes make a file silent without error: unittest.main() placed
mid-file, which exits before the second half is defined (four files, 87
tests), and no __main__ block at all, which counts zero (eight files, 174
tests). A guard now refuses both shapes and the return of a prefix list.

Assisted-by: Claude Opus 5
(cherry picked from commit db71b333cc08ae0fce4ddaaf037357648b10236b)
2026-08-29 02:11:04 -04:00
ccbb2a8597 [FIX] liste de prix : ne rien créer quand il en existe déjà une
`_activate_or_create_pricelists` ne compte pas une liste de prix partagée
entre sociétés (company_id vide) comme appartenant à la société : elle voit
une société « sans liste » et lui en fabrique une. Sur une base migrée qui
n'a rien perdu, cela ajoute un doublon vide à côté de la liste existante.

Elle interroge maintenant le détecteur `pricelist_missing` du même outil,
qui distingue déjà les deux cas.

Vérifié dans les deux sens sur copie jetable : liste présente, rien n'est
créé ; aucune liste, la réparation en crée une.

--- EN ---

`_activate_or_create_pricelists` does not count a pricelist shared across
companies (empty company_id) as belonging to the company: it sees a company
"without a pricelist" and makes one. On a migrated database that lost
nothing, that adds an empty duplicate beside the existing list.

It now asks the same tool's `pricelist_missing` detector, which already
tells the two cases apart.

Verified both ways on a throwaway copy: a pricelist present, nothing is
created; no pricelist, the repair creates one.

Assisted-by: Claude Opus 5
(cherry picked from commit b5bd2e4e44e89bdba4d639672611dcb964265987)
2026-08-29 02:11:04 -04:00
2534d4d022 [FIX] liste de prix : décider sur la fonctionnalité, pas sur l'exécutant
`env.user.has_group()` répond « oui » dès que l'exécutant est membre du
groupe, et la migration l'y ajoute en cours de route. Or la case des
réglages lit tout autre chose : ce que `base.group_user` IMPLIQUE
(res_config.py, « which groups are implied by the group Employee »).
Décider sur l'exécutant créait une liste de prix dans une base dont la
fonctionnalité est éteinte, et Odoo prévenait à chaque ouverture des
réglages qu'il allait l'archiver. Le contrôle « restant de migration »
posait la même mauvaise question ; les deux lisent désormais l'implication
du groupe. Vérifié sur copie jetable, dans les deux sens : fonctionnalité
éteinte, rien n'est signalé ; activée, constat et réparation reviennent.

--- EN ---

`env.user.has_group()` says yes as soon as the caller belongs to the
group, and the migration adds it along the way. But the settings checkbox
reads something else: what `base.group_user` IMPLIES (res_config.py,
"which groups are implied by the group Employee"). Deciding on the caller
created a pricelist in a database whose feature is off, and Odoo warned on
every opening of the settings that it would archive it. The
migration-residue check asked the same wrong question; both now read the
group implication. Verified on a throwaway copy, both ways: feature off,
nothing is reported; feature on, finding and repair come back.

Assisted-by: Claude Opus 5
(cherry picked from commit 38ba25e01894f45fb956bcbb08cc3d96a125e648)
2026-08-29 02:11:04 -04:00
8cc5566743 [FIX] anonymisation : tirer les nombres dans l'étendue mesurée
`resource.calendar.attendance.hour_from` est un `float` qui porte une
HEURE DE LA JOURNÉE : un tirage uniforme sur 0 à 1000 le sort de 0..23 et
l'affichage de la fiche lève « ValueError: hour must be in 0..23 ». La
borne vit dans `time(int(integral), ...)` de resource/models/utils.py, et
aucune contrainte PostgreSQL ne la déclare : le schéma ne dit pas le SENS
de la donnée. « 0 à 1000 » est donc faux pour tout nombre borné par un
usage : heures, pourcentages, taux. On respecte désormais la seule borne
que les DONNÉES déclarent, leur propre étendue — min et max mesurés par
table, et 0 à 1000 quand elle est inconnue. Vérifié : les tirages tiennent
dans l'étendue mesurée, et la fiche s'affiche sans lever.

--- EN ---

`resource.calendar.attendance.hour_from` is a `float` holding an HOUR OF
DAY: a uniform draw over 0 to 1000 takes it out of 0..23 and displaying the
record raises "ValueError: hour must be in 0..23". The bound lives in
`time(int(integral), ...)` in resource/models/utils.py, and no PostgreSQL
constraint declares it: the schema does not say what the data MEANS. So "0
to 1000" is wrong for any number bounded by usage: hours, percentages,
rates. We now respect the only bound the DATA declares, its own range —
min and max measured per table, and 0 to 1000 when it is unknown. Checked:
draws stay inside the measured range, and the record displays without
raising.

Assisted-by: Claude Opus 5
(cherry picked from commit 8e0a19823714b1d18f05613d498f8136b682c047)
2026-08-29 02:11:04 -04:00
f9edaa4e59 [ADD] git : un garde-fou commit-msg pour le sujet
Rien ne tenait la convention sur le sujet : le tag est respecté partout,
c'est la longueur qui glisse. Le hook refuse le mécanique et rien de plus
— tag absent, plus de 72 caractères, sujet ouvrant sur une citation ; dire
sur quoi porte le code reste un jugement qu'aucun hook ne rendra. Il compte
des caractères et non des octets, sans quoi un sujet français de 72
caractères tomberait sur ses accents. Le refus enseigne le repli vers des
mots-clés plutôt que la troncature, et nomme `--no-verify` : un garde-fou
qui refuse trop est désinstallé. Les tests pèsent donc autant les
acceptations, « Merge branch » ou fixup de rebase. Le lanceur balaie le
préfixe test_git_ : 32 tests jamais exécutés, le total va de 1021 à 1071.

--- EN ---

Nothing held the subject convention: the tag is respected everywhere, it
is the length that slips. The hook refuses the mechanical and nothing more
— no tag, over 72 characters, a subject opening on a quotation; whether it
says what the code is about stays a judgement no hook will make. It counts
characters, not bytes, or a 72-character French subject would fall on its
accents. The refusal teaches the fallback to keywords rather than
truncation, and names `--no-verify`: a guard rail that refuses too much
gets uninstalled. So the tests weigh the acceptances as much, a "Merge
branch" or a rebase fixup. The runner sweeps the test_git_ prefix: 32
tests never ran, and the total goes from 1021 to 1071.

Assisted-by: Claude Opus 5
(cherry picked from commit c9abf4d1b723ae8ff3762d39bfe1d6982400d12d)
2026-08-29 02:11:04 -04:00
46eebf0859 [ADD] run : précharger le registre Odoo au lancement, sans nuire
Odoo ne charge le registre d'une base qu'à la PREMIÈRE requête : sur une
base migrée, la personne qui ouvre la page attend des dizaines de secondes.
La sonde prend ce temps à sa place et s'arrête à la première réponse : un
303, un 404 ou un 500 prouvent tous que le registre est chargé.

Elle ne peut pas nuire : elle rend TOUJOURS 0, meurt avec run.sh par un trap
et abandonne après deux minutes. Le port vient de la ligne de commande, puis
de config.conf, puis du journal, exact même quand le port demandé était pris ;
l'écoute sur une interface vide ou générale est sondée par le bouclage.

`--erplibre-disable-warmup-http` la coupe, et elle seule est RETIRÉE avant
odoo_bin.sh, qu'Odoo refuse ; `--no-http` et `--stop-after-init` aussi.

--- EN ---

Odoo only loads a database's registry on the FIRST request: on a migrated
database, whoever opens the page waits tens of seconds. The probe takes that
time instead and stops at the first answer: a 303, a 404 or a 500 all prove
the registry is loaded.

It cannot get in the way: it ALWAYS returns 0, dies with run.sh through a
trap and gives up after two minutes. The port comes from the command line,
then config.conf, then the log, exact even when the requested port was taken;
an empty or wildcard listen interface is probed on the loopback.

`--erplibre-disable-warmup-http` turns it off, and it alone is STRIPPED
before odoo_bin.sh, which Odoo rejects; `--no-http` and `--stop-after-init` too.

Assisted-by: Claude Opus 5
(cherry picked from commit d0943782029ac9a23830d8a8041fccbcac98b67d)
2026-08-29 02:11:04 -04:00
2437a9b5ea [FIX] analyse: ne pas écrire de mots dans les champs qui portent des ids
Odoo déclare `parent_path` en `char` mais y range un chemin d'identifiants
— « 1/7/12/ » — reparsé aussitôt par int() : un mot écrit là casse le premier
chargement de page. Sept modèles `_parent_store` en portent un dans une base
ordinaire, et account_payment_term passe de même `days_next_month` à int().

L'anonymisation écarte les deux noms connus, puis sonde le contenu : une colonne
dont chaque valeur est un chemin reste intacte, même dans un module maison. Les
barres obliques sont exigées : un numéro tout en chiffres y échapperait.

Vérifié sur une base de production : les sept modèles chargent, les
partenaires sont anonymisés.

--- EN ---

Odoo declares `parent_path` as `char` but stores an identifier path in it —
« 1/7/12/ » — parsed right back with int(): a word written there breaks the
first page load. Seven `_parent_store` models carry one in an ordinary
database, and account_payment_term likewise passes `days_next_month` to int().

Anonymisation skips the two known names, then probes the content: a column
whose every value is a path is left alone, even in an in-house module. The
slashes are required: a number of pure digits would escape it.

Verified on a production database: the seven models load, partners are
anonymised.

Assisted-by: Claude Opus 5
(cherry picked from commit b47c94524d75172e7afce96e62f3f15b7ad6e225)
2026-08-29 02:11:04 -04:00
13a7bca972 [FIX] anonymize : passer la liste noire entière sans échec ni omission
Le SQL de la liste noire dépasse MAX_ARG_STRLEN, les 131 072 octets que Linux
impose à UN argument ; il passe par un fichier avec -f, qui garde
--single-transaction que l'entrée standard aurait perdu. Les colonnes texte à
longueur déclarée sont tronquées par left(..., n), l'identifiant en TÊTE sur
une colonne unique. Tous les identifiants sont cités : Odoo laisse nommer un
champ user ou order.

Écarter toute colonne sous CHECK laissait res_partner.name intact ; sur du
texte, seules les contraintes de FORME sont hors de portée. Vérifié sur les
quatre cas ; un UPDATE qui échoue n'écrit rien.

--- EN ---

The blacklist SQL exceeds MAX_ARG_STRLEN, the 131 072 bytes Linux allows for
ONE argument; it travels through a file with -f, which keeps
--single-transaction that stdin would have dropped. Text columns with a
declared length are truncated by left(..., n), the id FIRST on a unique column.
Every identifier is quoted: Odoo allows a field named user or order.

Skipping every column under a CHECK left res_partner.name untouched; on text,
only FORM constraints are out of reach. Checked on the four cases; an UPDATE
that fails writes nothing.

Assisted-by: Claude Opus 5
(cherry picked from commit 610f1d30414d578b49bff5649dbda82f4a2eb19f)
2026-08-29 02:11:04 -04:00
fe52ad9974 [FIX] database: de 12 à 15, neutraliser par le script plutôt que refuser
De 12 à 15 la neutralisation d'Odoo n'existe pas et la demande était refusée ;
le dépôt a pourtant sa technique de longue date, update_prod_to_dev.sh, et une
copie imparfaitement neutralisée vaut mieux qu'une copie brute. Le chemin suivi
est ANNONCÉ à l'exécution, car les deux ne posent pas les mêmes gestes : le
script ne pose pas is_neutralized, ne désactive pas les crons et laisse les
clés de paiement, là où il supprime les serveurs de courriel et pose un compte
de développement. Un seul cas reste refusé, le script introuvable, et son échec
fait échouer la copie : elle sortirait brute en s'annonçant neutralisée.

--- EN ---

From 12 to 15 Odoo's neutralisation does not exist and the request was refused;
yet the repository has long had its own technique, update_prod_to_dev.sh, and
an imperfectly neutralised copy beats a raw one. The route taken is ANNOUNCED
at run time, because the two do not make the same gestures: the script does not
set is_neutralized, does not disable crons and leaves the payment keys, where
it deletes the mail servers and sets up a development account. One case is
still refused, a missing script, and its failure fails the copy: it would come
out raw while announcing itself neutralised.

Assisted-by: Claude Opus 5
(cherry picked from commit bd7b648305a09c37adfe4b280e7479f1f178f20e)
2026-08-29 02:11:04 -04:00
abb605aad3 [ADD] database: dupliquer une base, et la neutraliser pour de bon
La duplication passe par exp_duplicate_database d'Odoo plutôt que par CREATE
DATABASE ... TEMPLATE : lui seul coupe les connexions de la source, régénère
database.uuid, copie le filestore et neutralise. Les trois modules maison du
dépôt n'obtenaient aucun des quatre.

Sous Odoo 15 et avant la neutralisation n'existe pas : la demande est REFUSÉE,
jamais ignorée — une copie qu'on croit neutralisée est pire qu'une brute.
Vérifié sur la copie d'une base migrée 12 → 18 : is_neutralized posé, crons
réduits au seul autovacuum, clé de paiement effacée, filestore copié.

--- EN ---

Duplication goes through Odoo's exp_duplicate_database rather than CREATE
DATABASE ... TEMPLATE: only it drops the source's connections, regenerates
database.uuid, copies the filestore and neutralises. The repository's three
in-house modules obtained none of the four.

Under Odoo 15 and earlier neutralisation does not exist: the request is
REFUSED, never ignored — a copy believed neutralised is worse than a raw one.
Checked on the copy of a 12 → 18 migrated database: is_neutralized set, crons
reduced to autovacuum alone, payment key cleared, filestore copied.

Assisted-by: Claude Opus 5
(cherry picked from commit af99d450c12e70a891d01d0ccf556c1d560e943b)
2026-08-29 02:11:04 -04:00
6e6e99c304 [FIX] long_test : bail attendu, rallumage à froid, marge mémoire
Trois mesures faites sur une descente à cinq étages, et une conclusion de ma
part corrigée par le contre-essai.

Le bail DHCP se fait attendre. L'étage 3 était créé, en type='kvm', et
« domifaddr » ne rendait rien : l'invité n'avait pas encore demandé son
adresse — 87 s puis 94 s selon les tours, quand deploy_qemu s'accorde 90 s et
rend 0 sans l'avoir trouvée. Lu une fois, cela ne prouvait rien.

Un redémarrage demandé à l'invité peut le laisser bloqué dans son
micrologiciel : RIP immobile 46 minutes, pas un octet lu, trois vCPU à fond.
J'ai d'abord conclu que c'était la taille de la mémoire, parce que la même
machine à 2 Go démarrait. Le contre-essai à 4 Go l'a réfuté : elle démarre
aussi, à froid. La différence est le REDÉMARRAGE, pas la mémoire — à chaud
elle reste dans l'UEFI, à froid elle charge son noyau en 60 à 90 s, à 2, 3 et
4 Go. La descente rallume donc une fois par le parent, et une seule : une
boucle de rallumage cacherait un vrai échec.

La mémoire de la pile QEMU est doublée pour une autre raison, mesurée elle
aussi : l'étage 2 avec 5 Go hébergeait un invité de 4 Go et n'avait plus que
127 Mo de libre. Ce n'est pas le plancher qui compte, c'est l'écart.

--- EN ---

Three measurements from a five-level descent, and a conclusion of mine
refuted by the counter-test.

The DHCP lease takes its time. Level 3 was created, type='kvm', and
"domifaddr" returned nothing: the guest had not yet asked for its address —
87 s then 94 s depending on the run, while deploy_qemu allows itself 90 s and
returns 0 without having found it. Read once, that proved nothing.

A reboot asked of the guest can leave it stuck in its firmware: static RIP for
46 minutes, not a byte read, three vCPU at full tilt. I first concluded it was
the memory size, because the same machine booted at 2 GB. The counter-test at
4 GB refuted it: it boots too, cold. The difference is the REBOOT, not the
memory — warm it stays in UEFI, cold it loads its kernel in 60 to 90 s, at 2,
3 and 4 GB. The descent therefore power-cycles once through the parent, and
only once: a restart loop would hide a real failure.

The QEMU stack's memory is doubled for another, also measured reason: level 2
with 5 GB hosted a 4 GB guest and had 127 MB left. It is not the floor that
matters, it is the gap.

Assisted-by: claude-opus-5
(cherry picked from commit e3f60e3ddf066eb76d434bbfe6b01f2271fb1874)
2026-08-29 01:53:03 -04:00
5af6c94c0a [FIX] deep_qemu : listes apt, sous-réseau par étage, étape muette
Trois défauts trouvés en une heure par le premier lancement réel — c'est ce
qu'un test d'intégration doit produire.

1. « --setup-host » a échoué en ZÉRO seconde sur « Unable to locate package
   qemu-system-x86 », alors que le paquet existe : la VM venait de démarrer et
   ses listes ne portaient que bookworm-security. Le message envoyait chercher
   des paquets, pas des listes. Même parade qu'install_proxmox.sh — arrêter
   apt-daily, puis réessayer.

2. Le réseau « default » de libvirt sert 192.168.122.0/24 à TOUS les étages.
   L'étage 2, dont l'adresse VENAIT de ce réseau, voyait son propre net-start
   refusé : « Network is already in use by interface enp1s0 ». Un invité qui
   vit dans un réseau ne peut pas servir le même. Chaque étage prend le sien,
   déduit de sa profondeur absolue, et le REDÉFINIT avant de le démarrer.

3. Le mien : l'extraction du moteur avait coupé preparer_systeme sur le
   « return False » de sa boucle, sans son « return True ». La fonction rendait
   None, donc l'étape échouait SANS RIEN DIRE, et les deux piles étaient
   cassées. L'essai à blanc ne pouvait pas le voir — il sort avant. Un test
   d'AST interdit désormais qu'une étape retombe sur None.

long_test/ était introuvable hors du menu : une ligne dans CLAUDE.md, trois
entrées au CHANGELOG, deux sections au README.

--- EN ---

Three defects found in one hour by the first real run — which is what an
integration test is for.

1. "--setup-host" failed in ZERO seconds on "Unable to locate package
   qemu-system-x86" though the package exists: the VM had just booted and its
   lists carried only bookworm-security. The message sent us looking for
   packages, not for lists. Same remedy as install_proxmox.sh — stop
   apt-daily, then retry.

2. libvirt's "default" network serves 192.168.122.0/24 at EVERY level. Level 2,
   whose own address CAME from that network, had its net-start refused:
   "Network is already in use by interface enp1s0". A guest living inside a
   network cannot serve the same one. Each level takes its own, derived from
   its absolute depth, and REDEFINES it before starting it.

3. Mine: extracting the engine had cut preparer_systeme at its loop's "return
   False", without the final "return True". The function returned None, so the
   step failed SAYING NOTHING, and both stacks were broken. The dry run could
   not see it — it exits earlier. An AST test now forbids a step from falling
   through to None.

long_test/ was undiscoverable outside the menu: one line in CLAUDE.md, three
CHANGELOG entries, two README sections.

Assisted-by: claude-opus-5
(cherry picked from commit e46bad408143f7511a04ffdc6a20efdb785f4b5e)
2026-08-29 01:53:03 -04:00
032556544e [ADD] long_test : partir d'un hôte existant, et le menu des deux piles
Créer une VM de tête pour héberger un hyperviseur qu'on possède déjà coûte
cinq minutes ET un étage d'imbrication — donc de la lenteur, puisque c'est
elle qu'on mesure. « --hote » part d'un hôte existant ; le menu le propose
sans le rechercher, l'hôte Proxmox déjà retenu étant lu par _pve_host(ask=False).

Trois conséquences que le code ne tirait pas :

- le plan se dimensionne sur la RACINE, lue par ssh. Le dimensionner sur la
  machine locale quand les étages vivent ailleurs annoncerait des étages qui
  ne tiennent pas ;
- les délais comptent la profondeur ABSOLUE. Un enfant de niveau 1 posé dans
  une racine déjà au troisième étage est en réalité au quatrième, et héritait
  de délais quatre fois trop courts — le défaut même que « delai » raconte
  avoir corrigé ;
- la racine n'est pas un étage atteint. L'y compter décalait de un le total et
  le code de sortie ; elle va dans une clé à part, et jamais « cree ».

« sudo » est DÉDUIT de « id -u » et non supposé, et une racine illisible fait
renoncer au lieu d'inventer une capacité.

Le menu offre les deux piles et défait chacune séparément — elles partagent le
dossier des rapports mais chacune ne connaît que les siens. Un test vérifie que
toute entrée affichée a son branchement : ils sont couplés par position, sans
garde.

80 tests, quatre garde-fous morts sous mutation.

--- EN ---

Creating a head VM to host a hypervisor you already own costs five minutes AND
one level of nesting — that is, slowness, which is the very thing being
measured. "--hote" starts from an existing host; the menu offers it without
searching, reading the already-chosen Proxmox host via _pve_host(ask=False).

Three consequences the code did not draw:

- the plan is sized on the ROOT, read over ssh. Sizing it on the local machine
  while the levels live elsewhere would announce levels that do not fit;
- delays count ABSOLUTE depth. A level-1 child placed in a root already at the
  third level is really at the fourth, and inherited delays four times too
  short — the very defect "delai" recounts having fixed;
- the root is not a level reached. Counting it shifted the total and the exit
  code by one; it goes in its own key, and never as "cree".

"sudo" is DEDUCED from "id -u" rather than assumed, and an unreadable root
makes us give up instead of inventing a capacity.

The menu offers both stacks and undoes each separately — they share the report
directory but each knows only its own. A test checks that every displayed entry
has its branch: they are coupled by position, with no guard.

80 tests, four guards die under mutation.

Assisted-by: claude-opus-5
(cherry picked from commit c2ab1a968346458925f55fc95619eb4ebd9d3efa)
2026-08-29 01:53:03 -04:00
8ff88f03d2 [ADD] long_test : deep_qemu, et la preuve que KVM est bien là
Le pendant de deep_proxmox : des QEMU dans des QEMU. Le ralentissement du
quatrième étage vient du PROCESSEUR, mais le coût par étage vient de ce qu'on
installe — un nœud Proxmox pose un noyau, corosync, ceph et une interface web
là où un hôte libvirt pose libvirtd. Les deux mesures ensemble séparent ce qui
tient au matériel de ce qui tient à la pile.

Ce test ne peut pas se contenter de descendre. deploy_qemu.py ne passe jamais
« --cpu host-passthrough » et, quand /dev/kvm manque, il n'échoue PAS : il pose
« --virt-type qemu », avertit sur une ligne et crée une VM entièrement ÉMULÉE —
sept minutes et demie de démarrage, aucun code de retour pour le dire. Sans
garde, la descente mesurerait de la TCG empilée en croyant mesurer de
l'imbrication, et rendrait un chiffre plus flatteur et faux.

Chaque étage doit donc PROUVER : /dev/kvm lisible, « nested » à Y, et le
domaine de l'enfant en type='kvm'. Ce qui n'a pas été lu vaut NON — un
/sys/module absent, c'est un module non chargé, pas une permission.

nesting_plan reçoit ses coûts : les constantes vCPU décrivent la physique de
l'imbrication et valent pour les deux piles, les six nombres qui chiffrent un
Proxmox non. Un étage QEMU demande 2 Go et 20 Go, contre 4 et 25.

26 tests, six garde-fous morts sous mutation.

--- EN ---

The counterpart to deep_proxmox: QEMU inside QEMU. The fourth level's slowdown
comes from the PROCESSOR, but the per-level cost comes from what you install —
a Proxmox node lays down a kernel, corosync, ceph and a web UI where a libvirt
host lays down libvirtd. Together the two measurements separate what is due to
the hardware from what is due to the stack.

This test cannot merely descend. deploy_qemu.py never passes "--cpu
host-passthrough" and, when /dev/kvm is missing, it does NOT fail: it sets
"--virt-type qemu", warns on one line and creates a fully EMULATED VM — seven
and a half minutes to boot, no exit code to say so. Unguarded, the descent
would measure stacked TCG while believing it measured nesting, and return a
more flattering, false number.

Every level must therefore PROVE: /dev/kvm readable, "nested" at Y, and the
child's domain type='kvm'. What was not read counts as NO — an absent
/sys/module means an unloaded module, not a permission problem.

nesting_plan takes its costs: the vCPU constants describe the physics of
nesting and hold for both stacks, the six numbers that price a Proxmox do not.
A QEMU level asks 2 GB and 20 GB against 4 and 25.

26 tests, six guards die under mutation.

Assisted-by: claude-opus-5
(cherry picked from commit 39682cb1ce9648db91261387cae88c40c2a67837)
2026-08-29 01:53:03 -04:00
b46615f3cf [REF] long_test : moteur commun, sûreté déclarée, sixième étape
deep_proxmox.py passe de 1245 à 474 lignes : tout ce qui ne connaît ni « qm »
ni pmxcfs vit désormais dans descente.py, prêt pour un second test long.

L'extraction a mis à nu ce qui protégeait un hôte qu'on n'a pas créé : rien.
a_defaire exigeait « vmid » et « parent_alias », deux clés que seule une
descente écrit — la protection tenait parce qu'aucun champ ne décrivait un
hôte emprunté. Un champ « cree », écrit à l'instant de la création, la rend
explicite et ferme trois portes : la liste de destruction, le repli par NOM de
detruire_etage1, et le retrait des entrées ~/.ssh/config de l'utilisateur.

Quatrième porte : le dossier des rapports est partagé. « deep_qemu --detruire »
aurait pris le rapport le plus récent, fût-il celui d'une descente Proxmox. Le
rapport porte son outil ; un rapport plus ancien, qui n'en a pas, est placé par
le préfixe de son nom de fichier plutôt que d'être rendu indéfaisable.

detruire_etage1 ne devine plus le nom de sa cible : il est obligatoire. Et une
sixième étape est née — « cet étage peut-il héberger le suivant ? » — parce que
le contrôle du stockage était celui du DÉBUT de l'étage suivant.

64 tests, les cinq garde-fous morts sous mutation. Au passage : la classe
LongTestMenuMixin, que mon renommage de répertoire avait rebaptisée
long_testMenuMixin sans qu'aucun test le voie.

--- EN ---

deep_proxmox.py drops from 1245 to 474 lines: everything that knows neither
"qm" nor pmxcfs now lives in descente.py, ready for a second long test.

The extraction laid bare what protected a host we did not create: nothing.
a_defaire required "vmid" and "parent_alias", two keys only a descent writes —
the protection held because no field described a borrowed host. A "cree" field,
written the instant a machine is created, makes it explicit and closes three
doors: the destroy list, detruire_etage1's fallback to the NAME, and the
removal of the user's own ~/.ssh/config entries.

Fourth door: the report directory is shared. "deep_qemu --detruire" would have
taken the most recent report, Proxmox's included. Reports now carry their tool;
an older one without it is placed by its filename prefix rather than made
undestroyable.

detruire_etage1 no longer guesses its target's name: it is mandatory. And a
sixth step is born — "can this level host the next?" — because the storage
check was the one at the START of the next level.

64 tests, all five guards die under mutation. Along the way: the class
LongTestMenuMixin, which my directory rename had turned into
long_testMenuMixin without any test noticing.

Assisted-by: claude-opus-5
(cherry picked from commit e1bc9ae3cfacd36a502bc88dc0789a4e86ce986b)
2026-08-29 01:53:03 -04:00
84ec78a61d [REF] long_test : renommer le répertoire selon la convention du dépôt
LongTest était le SEUL répertoire en CamelCase que nous ayons créé. Les deux
exceptions sous script/ — OCA_maintainer-tools, OCA_odoo-module-migrator —
sont des noms de dépôts amont tirés par Google Repo, pas les nôtres. Tout le
reste est en minuscules avec des soulignés : image_db, code_generator,
fork_github_repo, shell_script_odoo.

Le nom avait été repris tel qu'il m'avait été dicté, sans être confronté à la
convention — le contrôle même que le reste de ce travail applique partout.

50 occurrences dans 8 fichiers. Le menu TODO résout le nouveau chemin, l'essai
à blanc passe, et les 125 tests des trois fichiers touchés restent verts.

--- EN ---

LongTest was the ONLY CamelCase directory we created. The two exceptions under
script/ — OCA_maintainer-tools, OCA_odoo-module-migrator — are upstream repo
names pulled by Google Repo, not ours. Everything else is lowercase with
underscores: image_db, code_generator, fork_github_repo, shell_script_odoo.

The name had been taken as dictated, without being checked against the
convention — the very check the rest of this work applies everywhere.

50 occurrences across 8 files. The TODO menu resolves the new path, the dry run
passes, and the 125 tests in the three touched files stay green.

Assisted-by: claude-opus-5
(cherry picked from commit 170ee61e50dfeff638c84c07eadac00c62526e4d)
2026-08-29 01:53:03 -04:00
8e5f5b9097 [UPD] LongTest : trois étages par défaut, la mesure le dit
Le défaut promettait dix étages qu'aucune machine ne tient. Une descente
complète par ligne, sur 28 cœurs :

    étage 1 :   0 s d'amorçage,    200 s d'installation, 280 s en tout
    étage 2 :  37 s,               344 s,                495 s
    étage 3 :  93 s,               777 s,              1 064 s
    étage 4 : 15 608 s,         26 306 s,      n'a pas abouti

Trois étages coûtent une demi-heure. Le quatrième a coûté 4 h 20 d'amorçage et
7 h 18 d'installation sur la même machine : tout y est 15 à 30 fois plus lent,
pas une seule étape. C'est aussi là que les fabricants s'arrêtent — le
quatrième étage est le troisième hyperviseur imbriqué, AMD en documente deux.

La profondeur reste le seul paramètre, et le README dit désormais ce qu'on
achète en la montant.

--- EN ---

The default promised ten levels no machine can hold. One full descent per row,
on 28 cores:

    level 1:      0 s boot,    200 s install,   280 s total
    level 2:     37 s,         344 s,           495 s
    level 3:     93 s,         777 s,         1 064 s
    level 4: 15 608 s,      26 306 s,      did not finish

Three levels cost half an hour. The fourth cost 4 h 20 of boot and 7 h 18 of
install on the same machine: everything there is 15 to 30 times slower, not one
step. It is also where the vendors stop — level 4 is the third nested
hypervisor, and AMD documents two.

Depth remains the only parameter, and the README now says what raising it buys.

Assisted-by: claude-opus-5
(cherry picked from commit 226d6ffa3c664ba9e1a6dfdf48d52027b52eda23)
2026-08-29 01:53:03 -04:00
30399b4559 [FIX] imbrication : le coût d'un vCPU dépend de la profondeur de l'étage
Amorçage du quatrième étage, mesuré sur deux descentes complètes : 1 664 s à
2 vCPU, 15 608 s à 3. Un seul vCPU de plus, ×9,4. Aux étages 2 et 3 le même
vCPU ne coûte RIEN — ssh en 37 s et 93 s, comme à deux.

Le « gel » observé à 8 et 12 vCPU n'est probablement pas autre chose que cette
courbe poussée assez loin : 1 664 × 9,4 par vCPU dépasse vite toute patience,
et un RIP immobile à cinq minutes d'intervalle ne s'en distingue pas.

D'où un SEUIL de profondeur au lieu d'une largeur uniforme. Le troisième vCPU
reste aux étages 2 et 3, où il est gratuit et où il enlève le surengagement
qui affamait l'installation de l'étage 4 — celle-ci ne finissait pas avec un
parent à 2 vCPU, elle progresse avec un parent à 3. À partir du quatrième
étage, le strict minimum.

La combinaison ainsi obtenue — parent 3, enfant 2 — n'a jamais été mesurée :
les deux essais étaient (2, 2) et (3, 3).

--- EN ---

Fourth-level boot, measured on two full descents: 1,664 s at 2 vCPU, 15,608 s
at 3. One more vCPU, ×9.4. At levels 2 and 3 that same vCPU costs NOTHING —
ssh in 37 s and 93 s, same as at two.

The "freeze" seen at 8 and 12 vCPU is most likely nothing but this curve taken
far enough: 1,664 × 9.4 per vCPU quickly exceeds any patience, and a static
RIP five minutes apart is indistinguishable from it.

Hence a depth THRESHOLD instead of a uniform width. The third vCPU stays at
levels 2 and 3, where it is free and where it removes the overcommit that
starved level 4's install — which never finished with a 2-vCPU parent and does
progress with a 3-vCPU one. From the fourth level down, the strict minimum.

The resulting combination — parent 3, child 2 — has never been measured: the
two attempts were (2, 2) and (3, 3).

Assisted-by: claude-opus-5
(cherry picked from commit 9a5583a4b9461a087d161775764c560c82e0609e)
2026-08-29 01:53:03 -04:00
b2b3f36026 [FIX] imbrication : aucun étage imbriqué n'est large, le gel le dit
Ma propre conclusion de ce matin était fausse, et une mesure l'a réfutée.
J'avais écrit — code, README, commit — que le gel à 12 vCPU venait du
SURENGAGEMENT : cette VM avait douze vCPU sur un hôte qui en avait deux.

Descente réelle : l'étage 4 à huit vCPU, sur un parent qui en avait NEUF,
charge 1,47, aucun surengagement. Gelé pareil. 32 Mio lus en 106 minutes, même
RIP à trois relevés espacés de cinq minutes. C'est le nombre de vCPU de
l'invité imbriqué, et rien d'autre.

Le dimensionnement de bas en haut donnait 8 vCPU à l'étage 4, 7 au 5 : il
rendait larges précisément les étages qui doivent rester étroits. Trois
largeurs fixes le remplacent — métal, intermédiaire, fond. La mémoire et le
disque, eux, restent dimensionnés depuis le bas.

VCPU_INTERMEDIAIRE = 3 est une hypothèse assumée : deux démarre au quatrième
étage, huit gèle, rien n'est mesuré entre les deux.

--- EN ---

My own conclusion from this morning was wrong, and a measurement refuted it. I
had written — code, README, commit — that the 12-vCPU freeze came from
OVERCOMMIT: that VM had twelve vCPU on a host with two.

Real descent: level 4 at eight vCPU, on a parent with NINE, load 1.47, no
overcommit whatsoever. Frozen all the same. 32 MiB read in 106 minutes, same
RIP at three readings five minutes apart. It is the nested guest's vCPU count,
nothing else.

Bottom-up sizing gave level 4 eight vCPU and level 5 seven: it made wide
exactly the levels that must stay narrow. Three fixed widths replace it —
metal, intermediate, floor. Memory and disk stay sized from the bottom.

VCPU_INTERMEDIAIRE = 3 is an owned hypothesis: two boots at the fourth level,
eight freezes, nothing is measured in between.

Assisted-by: claude-opus-5
(cherry picked from commit ee45ff4f333c69e3862e277334cb34efa39bbd57)
2026-08-29 01:53:03 -04:00
006f4d271b [FIX] LongTest : l'étage 1 s'identifie par son UUID, pas par son nom
Dernières trouvailles de la relecture, et la famille la plus tenace de ce
travail : une machine liée à ce qu'elle s'appelle plutôt qu'à ce qui
l'identifie.

« virsh undefine --remove-all-storage » partait sur le nom fixe deep-pve-1,
quel que soit le domaine qui le porte — la VM d'une descente précédente qu'on
voulait garder, ou une machine sans rapport. L'UUID est noté à la création et
vérifié avant de détruire ; un rapport ancien n'en a pas, on procède alors par
le nom faute de mieux, mais on le dit.

Le nom des étages imbriqués était de même déduit du numéro d'étage à la
RELECTURE. Un rapport écrit avant un changement de nom_etage aurait désigné
des machines qui ne sont pas les siennes. Il est écrit à la création.

Les deux meurent sous mutation. 52 tests dans ce fichier.

--- EN ---

Last findings from the review, and the most persistent family in this work: a
machine bound to what it is called rather than to what identifies it.

"virsh undefine --remove-all-storage" went by the fixed name deep-pve-1,
whatever domain carries it — a previous descent's VM one meant to keep, or an
unrelated machine. The UUID is recorded at creation and checked before
destroying; an older report has none, so we fall back to the name, and say so.

The nested levels' names were likewise derived from the level number at READ
time. A report written before a change to nom_etage would have named machines
that are not its own. It is now written at creation.

Both die under mutation. 52 tests in this file.

Assisted-by: claude-opus-5
(cherry picked from commit 93292653afb91351b0efa5700fcf66f6bb82604f)
2026-08-29 01:53:03 -04:00
178785b90a [FIX] LongTest : garder le VMID, le code des lectures, le journal PVE
Trois trouvailles de la relecture adversaire, toutes de la même famille : une
information qu'on possédait et qu'on jetait.

Le VMID ne remontait qu'au RETOUR de creer_enfant, qui enchaîne six commandes
sur le parent. Un échec à la quatrième — « qm resize » sur un stockage plein —
laissait une VM allumée et un disque alloué que le rapport ne nommait nulle
part : « --detruire » ne pouvait pas la défaire.

preparer_parent jetait le code de retour de ses lectures. Un « ip link show »
qui échoue se lisait « pas de pont », et de là on POSAIT un pont et un NAT sur
une machine qui en avait déjà un. Pour une lecture, le code de retour est la
seule chose qui distingue « j'ai lu, il n'y a rien » de « je n'ai pas pu lire ».

reparer_pmxcfs jetait le journal des unités PVE, que pve_unit_cmd joint exprès
à un échec. Il ne restait qu'un « /etc/pve : ABSENT » sans cause, à chercher
sur un hyperviseur mesuré 36 fois plus lent que son hôte.

Les trois meurent sous mutation, chacune avec son contrôle négatif.

--- EN ---

Three findings from the adversarial review, all the same family: information
we already held and threw away.

The VMID only surfaced on creer_enfant's RETURN, and that function chains six
commands on the parent. A failure at the fourth — "qm resize" on a full
storage — left a running VM and an allocated disk that the report named
nowhere: "--detruire" could not undo it.

preparer_parent discarded its reads' exit codes. An "ip link show" that fails
read as "no bridge", and from there we CREATED a bridge and a NAT on a machine
that already had one. For a read, the exit code is the only thing separating
"I read it, there is nothing" from "I could not read".

reparer_pmxcfs discarded the PVE units' journal, which pve_unit_cmd attaches
to a failure on purpose. All that remained was "/etc/pve : ABSENT" with no
cause, to be hunted on a hypervisor measured 36 times slower than its host.

All three die under mutation, each with its negative control.

Assisted-by: claude-opus-5
(cherry picked from commit 4c0279665e590169c23c4629e69ad945c4ae3fe4)
2026-08-29 01:53:03 -04:00
4d26155901 [FIX] imbrication : la ressource qui borne, l'attente, le décompte
Incident sur une descente réelle : un agent de relecture, chargé de vérifier
ce que redemarrer_et_verifier PROUVE, l'a appelé sur l'étage 1 vivant. Le
reboot a éteint les étages 2, 3 et 4 d'un coup. La descente a alors attendu
son délai entier — quarante minutes — un ssh qui ne pouvait plus aboutir, puis
a conclu « jamais joignable ». L'attente surveille désormais la MAISON.

Le décompte de la destruction mentait dans l'autre sens : les étages
injoignables étaient annoncés « il reste des machines » alors que le disque de
l'étage 1, effacé, les contenait. Les entrées ~/.ssh/config sont retirées
aussi, sinon leur ProxyJump désigne un hôte disparu.

Et « arret » nommait la RAM quand le vCPU bornait : la chaîne était figée en
ram > disque > vcpu et évaluée à la profondeur demandée. Il nomme maintenant
le plus bas des trois plafonds, et les trois sont affichés.

--- EN ---

Incident on a real descent: a review agent, tasked with checking what
redemarrer_et_verifier PROVES, called it on the living level 1. The reboot
took levels 2, 3 and 4 down at once. The descent then waited its whole
timeout — forty minutes — for an ssh that could no longer land, and concluded
"never reachable". The wait now watches the HOUSE.

The destroy count lied the other way: unreachable levels were reported as "il
reste des machines" when level 1's erased disk contained them. The
~/.ssh/config entries are removed too, else their ProxyJump names a host that
is gone.

And "arret" named RAM when vCPU was the bound: the chain was frozen as
ram > disque > vcpu and evaluated at the requested depth. It now names the
lowest of the three ceilings, and all three are shown.

Assisted-by: claude-opus-5
(cherry picked from commit 2d84b62bdd9907e9a30383774015bcb1ae379de1)
2026-08-29 01:53:03 -04:00
c046e027e9 [FIX] LongTest : ne pas détruire sous une descente vivante ; blocs ssh
Relecture adversaire du commit précédent : il avait CRÉÉ un danger. Le rapport
s'écrivant maintenant VM par VM, celui de la descente EN COURS est le plus
récent, et « --detruire » l'aurait choisi — qm destroy --purge sur l'arbre que
le processus installait encore. Deux garde-fous : un PID dans le rapport, et
un refus net tant qu'un autre deep_proxmox.py tourne. Le second est nécessaire
car une descente déjà lancée a l'ancien module en mémoire.

Reconnu par ARGUMENT, pas par sous-chaîne : mon propre « pgrep -f
deep_proxmox.py » de surveillance donnait deux faux positifs sur trois.

Trois autres, mêmes preuves :
- un rapport vide plus récent masquait celui qui nommait les VM réelles ;
- detruire() ne créditait jamais l'étage 1 : le décompte était décalé de un
  dans tous les cas, donc l'avertissement sortait toujours ;
- _ssh_config_drop_hosts prenait l'indentation pour de la syntaxe. Sur un bloc
  au corps non indenté, seule la ligne Host partait et ssh rattachait
  « StrictHostKeyChecking no » au bloc précédent — un serveur de production.
  Et un bloc partagé (« Host prod-db vm-a ») partait en entier.

Les cinq correctifs meurent sous mutation.

--- EN ---

Adversarial review of the previous commit: it had CREATED a hazard. With the
report now written VM by VM, the RUNNING descent's is the most recent, and
"--detruire" would have picked it — qm destroy --purge on the tree the process
was still installing. Two guards: a PID in the report, and a flat refusal
while another deep_proxmox.py runs. The second is needed because an
already-running descent holds the old module in memory.

Matched by ARGUMENT, not substring: my own monitoring "pgrep -f
deep_proxmox.py" produced two false positives out of three.

Three more, same evidence:
- a newer empty report masked the one naming the real VMs;
- detruire() never credited level 1: the count was off by one in every case,
  so the warning always fired;
- _ssh_config_drop_hosts took indentation for syntax. On a block with an
  unindented body only the Host line went, and ssh attached
  "StrictHostKeyChecking no" to the preceding block — a production server.
  And a shared block ("Host prod-db vm-a") went entirely.

All five fixes die under mutation.

Assisted-by: claude-opus-5
(cherry picked from commit 7d348d976f96e420b4fec4d879911b658a730ffa)
2026-08-29 01:53:03 -04:00
d0a06c03b7 [FIX] LongTest : rapport écrit VM par VM, retrait ssh sans bloc nu
Descente de dix étages arrêtée pendant l'installation du quatrième : quatre
machines réelles restaient, et « --detruire » répondait « aucun rapport : rien
à défaire ». Le rapport ne s'écrivait qu'à la fin, donc le seul enregistrement
du couple (alias du parent, VMID) mourait avec le processus. Il fallait les
retrouver par leur NOM, ce que tout ce fichier s'applique à éviter.

En nettoyant à la main, second défaut : appelé sans nom à écrire — un retrait
pur, légitime, les machines n'existant plus — _write_ssh_config_entry écrivait
« Host » NU suivi d'un « HostName » vide dans le ~/.ssh/config réel, puis
mourait sur IndexError en annonçant l'ajout. Constaté, puis retiré du fichier.

Les deux correctifs meurent sous mutation : 3 tests et 2 tests.

--- EN ---

A ten-level descent stopped during the fourth level's install: four real
machines were left, and "--detruire" answered "no report: nothing to undo".
The report was only written at the end, so the sole record of the (parent
alias, VMID) pair died with the process. They had to be found by NAME, which
this whole file works to avoid.

Cleaning up by hand surfaced a second defect: called with no name to write — a
pure removal, legitimate since the machines are gone — _write_ssh_config_entry
wrote a BARE "Host" followed by an empty "HostName" into the real ~/.ssh/config,
then died on IndexError while announcing the addition. Observed, then removed.

Both fixes die under mutation: 3 tests and 2 tests.

Assisted-by: claude-opus-5
(cherry picked from commit e7c8b540c630b33c6eb1b1a2f51c01497e0b3ca0)
2026-08-29 01:53:03 -04:00
469a5fde9e [FIX] imbrication : dimensionner les étages depuis le bas, vCPU compris
Le plan cédait à l'enfant ce que le parent pouvait céder. Descente réelle à
dix étages : l'étage 4 a reçu 44 Go et 2 vCPU sur un hôte qui en avait 2 —
cent pour cent de surengagement, à chaque étage. Son installation dure 2 h 52
et n'est pas finie, contre 793 s pour l'étage 3. Extrapolé, le dixième
demandait des années.

Le plus profond reçoit désormais ce qu'un Proxmox de test demande, et chaque
parent ajoute son seul surcoût : un vCPU, 2 Gio, 10 Go. Dix étages tiennent
sur 11 vCPU et 22 Go au premier, contre 50 Go avant.

Corrige aussi l'explication du gel à 12 vCPU : cette VM avait douze vCPU sur
un hôte qui en avait deux. C'est le surengagement qui gèle, pas le douze.

--- EN ---

The plan handed the child whatever the parent could spare. Real ten-level
descent: level 4 got 44 GB and 2 vCPU on a host that had 2 — a hundred
percent overcommit, at every level. Its install has run 2h52 and is not done,
against 793 s for level 3. Extrapolated, the tenth wanted years.

The deepest level now gets what a test Proxmox asks for, and each parent adds
its own overhead only: one vCPU, 2 GiB, 10 GB. Ten levels fit in 11 vCPU and
22 GB at the first, against 50 GB before.

Also corrects the account of the 12-vCPU freeze: that VM had twelve vCPU on a
host with two. Overcommit freezes, not the twelve.

Assisted-by: claude-opus-5
(cherry picked from commit 7cda84bf391ee3ed36c5953ca4b86df44bd09e3b)
2026-08-29 01:53:03 -04:00
adf0f275d4 [FIX] proxmox : apt-daily tient le verrou au démarrage
Trois pannes trouvées en LANÇANT la descente, aucune vue en la relisant — ni
par moi, ni par l'attaque adversariale.

Le premier « apt update » d'une image cloud échoue sur un verrou qui n'est pas
celui qu'on croit. Mesuré une seconde après le premier ssh :

  E: Could not get lock /var/lib/apt/lists/lock.
     It is held by process 1026 (apt-get)

Ce n'est pas cloud-init — « status --wait » avait rendu la main. C'est
apt-daily, le minuteur de Debian, qui se déclenche au démarrage. Et le verrou
des LISTES n'est pas couvert par « DPkg::Lock::Timeout », que l'installeur
réglait pourtant déjà à 600 s : cette attente ne vaut que pour dpkg. On arrête
donc les minuteurs, puis on RÉESSAIE — arrêter une unité n'interrompt pas
l'apt-get déjà en vol. Le défaut touchait tout déploiement Proxmox, pas
seulement ce test.

Le premier étage n'avait pas d'alias ssh. « deploy_qemu.py » en ligne de
commande n'écrit pas d'entrée ~/.ssh/config — le menu le fait, la CLI non. La
descente aurait attendu son plein délai avant de conclure « jamais joignable »
sur une VM qui répondait à son adresse. Elle l'écrit maintenant elle-même,
depuis l'adresse résolue, et refuse d'avancer si la VM n'en a pas.

Et la réserve de l'hôte est proportionnelle. Quatre gigaoctets sur une machine
de soixante, c'était 6 % laissés au système : le jour où les invités touchent
vraiment leur mémoire, c'est l'hôte qui part en swap — et la mesure serait
celle du swap, pas de l'imbrication. Un huitième, avec le plancher d'avant
pour les petites machines.

Ce que la descente a établi en trois étages : 392 s, 644 s, 1120 s, soit 1,7
fois par étage. Puis la poignée de main ssh passe de 77 à 1664 secondes au
quatrième — vingt fois d'un seul cran. Le coude est là.

Et le mur que j'avais pris pour une limite d'imbrication n'en était pas une.
La VM qui gelait au quatrième étage avait douze vCPU ; celle-ci en a deux et
elle passe, en écrivant. C'était une limite de parallélisme SOUS imbrication —
exactement ce que l'algorithme borne, vérifié pour la première fois plutôt que
supposé.

--- EN ---

Three faults found by RUNNING the descent, none seen by reading it — neither
by me nor by the adversarial attack.

A cloud image's first "apt update" fails on a lock that is not the one you
expect. Measured one second after the first ssh:

  E: Could not get lock /var/lib/apt/lists/lock.
     It is held by process 1026 (apt-get)

It is not cloud-init — "status --wait" had returned. It is apt-daily, Debian's
timer, firing at boot. And the LISTS lock is not covered by
"DPkg::Lock::Timeout", which the installer already set to 600 s: that wait
only applies to dpkg. So we stop the timers, then RETRY — stopping a unit does
not interrupt the apt-get already in flight. The defect affected every Proxmox
deployment, not just this test.

The first level had no ssh alias. "deploy_qemu.py" on the command line does not
write a ~/.ssh/config entry — the menu does, the CLI does not. The descent
would have waited its full timeout before concluding "never reachable" about a
VM answering at its address. It now writes the entry itself, from the resolved
address, and refuses to proceed if the VM has none.

And the host's reserve is proportional. Four gigabytes on a sixty-gigabyte
machine left 6 % to the system: the day the guests really touch their memory,
the host swaps — and the measurement would be of swap, not of nesting. One
eighth now, keeping the old floor for small machines.

What the descent established over three levels: 392 s, 644 s, 1120 s — 1.7x per
level. Then the ssh handshake goes from 77 to 1664 seconds at the fourth:
twenty times in one step. That is the elbow.

And the wall I had taken for a nesting limit was not one. The VM that froze at
the fourth level had twelve vCPU; this one has two and it gets through,
writing. It was a limit of parallelism UNDER nesting — exactly what the
algorithm caps, verified for the first time rather than assumed.

Assisted-by: Claude Opus 5
(cherry picked from commit 7f86562cbd8f10017dcb88fe4272efc162cbccbc)
2026-08-29 01:53:03 -04:00
9bce1a8503 [FIX] LongTest : sh au lieu de bash, et --detruire trop large
Attaqué par trois lentilles sur le code écrit, avant de le lancer pour de
vrai. Deux fautes valaient à elles seules l'exercice.

Il n'aurait JAMAIS fonctionné. L'installeur était lancé par « sh », or il
porte « set -euo pipefail » et un shebang bash : sur Debian /bin/sh est dash,
qui répond « set: Illegal option -o pipefail » et sort à la PREMIÈRE ligne.
Chaque étage aurait échoué sur l'installation, à tous les coups.

Et « --detruire » pouvait emporter une machine étrangère. Il prenait toute
entrée ssh dont le nom CONTENAIT « deep-pve », puis sur son rebond détruisait
toute VM dont le nom contenait « deep-pve » — une « deep-pve-lab » de
production tombait dedans, et « --purge » emporte les disques. Son tri « du
plus profond au plus haut » comptait les « + » de l'alias, or alias_etage
remplace le « + » du parent par un « - » : chaque alias en portait exactement
UN, le tri ne triait rien, et la destruction partait du plus HAUT — le disque
du parent emportait ses enfants sans qu'on les ait nommés. Il ignorait
« --dry-run », ne lisait aucun code de retour, concluait « ✓ défait », et le
menu le lançait d'une touche.

Il ne détruit plus que ce que le RAPPORT nomme : un couple (parent, VMID) par
étage, du plus profond d'après le niveau lu, égalité stricte du nom, arrêt
CONSTATÉ avant destruction, codes de retour lus, et une confirmation par
« OUI » après la liste.

Six autres constats, tous réels. Le redémarrage se prouve par btime et non par
le seul noyau — rejoué sur un étage déjà installé, on validait un redémarrage
qui n'avait pas eu lieu, exactement le piège corrigé la semaine dernière dans
le suivi. La sonde de disponibilité ne demande plus sudo, sinon un sudo lent
se lisait « jamais joignable en ssh ». Les délais suivent la profondeur : le
script existe pour mesurer un ralentissement de 36x, et un plafond fixe
déclarait échouée une installation qui avançait. L'adresse fixe est contrôlée
AVANT de télécharger une image et de démarrer une VM. Le DNS de l'hôte suit la
spec, sinon apt meurt sans rien expliquer. Et l'essai à blanc ne prétend plus
avoir atteint quoi que ce soit — son rapport était indiscernable d'une
réussite, JSON compris.

L'algorithme aussi : profondeur 0 rendait un plan d'UN étage, donc
« --depth 0 » créait une VM ; et sur un hôte de quatre cœurs le premier étage
recevait UN vCPU quand son invité en recevait deux — un parent plus étroit que
son enfant.

Les tests mordent, prouvé par mutation : remplacer le calcul du premier étage
par la valeur imbriquée les laissait verts.

--- EN ---

Attacked by three lenses on the written code, before running it for real. Two
faults alone justified the exercise.

It would NEVER have worked. The installer was run by "sh", yet it carries "set
-euo pipefail" and a bash shebang: on Debian /bin/sh is dash, which answers
"set: Illegal option -o pipefail" and exits on the FIRST line. Every level
would have failed at install, every time.

And "--detruire" could take a stranger's machine. It took every ssh entry
whose name CONTAINED "deep-pve", then on its jump host destroyed every VM
whose name contained "deep-pve" — a production "deep-pve-lab" fell in, and
"--purge" takes the disks. Its "deepest first" sort counted the "+" in the
alias, yet alias_etage replaces the parent's "+" with a "-": every alias had
exactly ONE, the sort sorted nothing, and destruction started from the TOP —
the parent's disk took its children with it, unnamed. It ignored "--dry-run",
read no return code, concluded "✓ done", and the menu fired it on one key.

It now destroys only what the REPORT names: a (parent, VMID) pair per level,
deepest first by the recorded level, strict name equality, shutdown VERIFIED
before destruction, return codes read, and a "OUI" confirmation after the
list.

Six more findings, all real. The reboot is proven by btime, not by the kernel
alone — replayed on an already-installed level, we validated a reboot that
never happened, exactly the trap fixed last week in the monitor. The liveness
probe no longer asks for sudo, or a slow sudo read as "never reachable by
ssh". Timeouts follow the depth: the script exists to measure a 36x slowdown,
and a fixed ceiling declared failed an install that was progressing. The
static address is checked BEFORE downloading an image and starting a VM. The
host's DNS follows the spec, or apt dies explaining nothing. And the dry run
no longer claims to have reached anything — its report was indistinguishable
from a success, JSON included.

The algorithm too: depth 0 returned a ONE-level plan, so "--depth 0" created a
VM; and on a four-core host the first level got ONE vCPU while its guest got
two — a parent narrower than its child.

The tests bite, proven by mutation: replacing the first level's computation
with the nested value left them green.

Assisted-by: Claude Opus 5
(cherry picked from commit 64b8e5063bd7f420cdeb27b88f94043190b5ecd4)
2026-08-29 01:53:03 -04:00
7199a7cbb2 [ADD] LongTest : jusqu'à quel étage un Proxmox imbriqué tient-il
La profondeur d'imbrication praticable ne se déduit pas, elle se mesure. Une
mesure à la main a trouvé, au quatrième étage, un invité 36 fois plus lent que
le temps réel — 583 secondes d'horloge pour 16 secondes de temps invité,
chaque ligne d'ACPI prenant une seconde — puis un noyau gelé au MÊME octet
quelles que soient les ressources. Un chiffre obtenu une fois, sur une
machine, n'est pas un chiffre.

D'où trois choses.

L'algorithme, en fonctions pures. Deux ressources s'épuisent en descendant :
la mémoire, chaque étage gardant de quoi faire tourner ses propres démons, et
le disque, celui de l'enfant vivant DANS celui du parent. Une troisième se
dégrade, et elle borne le vCPU à deux au-delà du premier étage : douze ont
gelé le noyau invité, les mêmes deux avançaient. La mémoire n'est PAS bornée —
la même VM gelait au même octet avec 9 Go et avec 2 Go, donc la rogner ne
gagnerait rien et priverait l'étage du dessous. Le plan est annoncé avant
toute création, et jamais au-delà de ce qui tient.

Le garde-fou dans l'écran. Il lisait la capacité de l'HÔTE et l'offrait en
entier : sur un troisième étage à 14 cœurs, il a proposé 12 vCPU à une VM qui
n'a jamais démarré. Le nombre n'était pas absurde pour la machine ; il l'était
pour sa profondeur, que l'écran ignorait. Elle se compte maintenant sur la
chaîne de ProxyJump — un rebond par étage, et c'est nous qui écrivons ces
entrées.

Le test long, dans LongTest/ et non dans test/ : le lanceur unitaire doit
rester lançable en quelques secondes, partout, y compris sans virtualisation.
La descente est uniforme — créer, attendre le ssh, installer, redémarrer et
vérifier le noyau, remettre pmxcfs debout, contrôler le stockage — et s'arrête
au premier étage qui échoue en NOMMANT l'étape. Il envoie notre
install_proxmox.sh par scp plutôt que de laisser la VM cloner le dépôt : c'est
notre code qu'on éprouve, et un correctif absent du distant a fait revenir le
même défaut sur trois VM.

--- EN ---

The practicable nesting depth cannot be deduced, only measured. A manual
measurement found, at the fourth level, a guest 36 times slower than real time
— 583 seconds of wall clock for 16 seconds of guest time, each ACPI line
taking a second — then a kernel frozen at the SAME byte whatever the
resources. A number obtained once, on one machine, is not a number.

Hence three things.

The algorithm, in pure functions. Two resources run out going down: memory,
each level keeping what its own daemons need, and disk, the child's living
INSIDE the parent's. A third degrades, and it caps the vCPU at two beyond the
first level: twelve froze the guest kernel, the same two progressed. Memory is
NOT capped — the same VM froze at the same byte with 9 GB and with 2 GB, so
trimming it would gain nothing and starve the level below. The plan is
announced before anything is created, and never beyond what fits.

The guard in the screen. It read the HOST's capacity and offered all of it: on
a third level with 14 cores it proposed 12 vCPU to a VM that never booted. The
number was not absurd for the machine; it was for its depth, which the screen
did not know. It is now counted on the ProxyJump chain — one hop per level,
and we are the ones writing those entries.

The long test, in LongTest/ and not test/: the unit runner must stay runnable
in seconds, anywhere, including without virtualisation. The descent is uniform
— create, wait for ssh, install, reboot and check the kernel, bring pmxcfs
back, check the storage — and stops at the first level that fails, NAMING the
step. It sends our install_proxmox.sh over scp instead of letting the VM clone
the repository: it is our code being exercised, and a fix absent from the
remote made the same defect return on three VMs.

Assisted-by: Claude Opus 5
(cherry picked from commit 4f70c461330cac6f46783a60e0f33052a979fa23)
2026-08-29 01:53:03 -04:00
4bc2fa6097 [ADD] proxmox : l'écran remet pmxcfs debout lui-même
Le conseil « rejouer install_proxmox.sh sur l'hôte » ne pouvait PAS marcher :
la VM clone le dépôt distant, donc sa copie du script est celle du distant —
tant que le correctif n'y est pas, celle qui ne corrige rien. Trois hôtes de
suite sont tombés dessus, avec le même message inutile. L'écran répare donc :
gel de cloud-init, réécriture de /etc/hosts, relance des unités, constat du
montage — le pendant exact de l'offre de créer un pont.

Écrit, puis ATTAQUÉ par trois lentilles sur le code réel. Ce qu'elles ont
mesuré valait la peine.

/etc/hosts se réécrivait en DEUX écritures — « sed -i » puis « printf >> » —
alors que la docstring promettait l'inverse. Sed refusé et ajout réussi, la
ligne 127.0.1.1 survivait EN PREMIER et la nôtre s'ajoutait une fois par
tentative ; sed réussi et ajout refusé, l'hôte perdait l'entrée de son nom, et
chaque sudo y attendait ensuite le résolveur. C'est maintenant un fichier
complet bâti dans un temporaire, VÉRIFIÉ, puis recopié — « cat > » et non
« mv », qui remplacerait l'inode et perdrait mode et propriétaire.

Le contrôle final s'en remettait à « getent hosts », qui réussit via mDNS même
quand rien n'a été écrit — et acceptait les fe80:: que notre propre code
rejette. Il relit désormais ce qui a été écrit.

awk remplace sed pour filtrer : « print » émet un saut de ligne, donc un
/etc/hosts non terminé par un — cloud-init n'en met pas — est normalisé. Sans
ça notre ligne se collait à la précédente et le nom du nœud partait sur
l'adresse d'une autre machine.

Trois autres, du même acabit. Les dépendants de pmxcfs sont relancés eux
aussi : actifs pendant la panne, ils échouaient sur ipcc_send_rec, et les
laisser donnait une GUI en « communication failure » juste après notre ✓. Un
silence du lien n'est plus lu comme une absence de montage. Et l'adresse n'est
mise en cause que si pve-cluster a réellement démarré.

Les tests exécutent les commandes au lieu de les relire, bouchons capables
d'ÉCHOUER : écriture refusée, fichier sans saut de ligne final, tabulations,
start qui rate, montage qui disparaît pendant la reconfirmation. Prouvé par
mutation — trois HOSTS-KO changés en HOSTS-OK font rougir le test.

--- EN ---

The advice "replay install_proxmox.sh on the host" could NOT work: the VM
clones the remote, so its copy of the script is the remote's — while the fix
is not there, the one that fixes nothing. Three hosts in a row hit it with the
same useless message. So the screen repairs: freeze cloud-init, rewrite
/etc/hosts, restart the units, verify the mount — the exact counterpart of the
offer to create a bridge.

Written, then ATTACKED by three lenses on the real code. What they measured
was worth it.

/etc/hosts was rewritten in TWO writes — "sed -i" then "printf >>" — while the
docstring promised the opposite. Sed refused and append succeeded: the
127.0.1.1 line survived FIRST and ours was added once per attempt; sed
succeeded and append refused: the host lost its own name entry, and every sudo
then waited on the resolver. It is now a complete file built in a temporary,
VERIFIED, then copied over — "cat >" not "mv", which would replace the inode
and lose mode and owner.

The final check relied on "getent hosts", which succeeds via mDNS even when
nothing was written — and accepted the fe80:: our own code rejects. It now
re-reads what was written.

awk replaces sed for filtering: "print" emits a newline, so an /etc/hosts with
no final one — cloud-init omits it — gets normalised. Without that our line
glued onto the previous one and the node's name pointed at another machine's
address.

Three more of the same kind. pmxcfs's dependents are restarted too: active
throughout the outage, they failed on ipcc_send_rec, and leaving them gave a
GUI in "communication failure" right after our ✓. A silent link is no longer
read as a missing mount. And the address is only blamed if pve-cluster
actually started.

The tests execute the commands instead of reading them, with stubs able to
FAIL: refused write, file with no final newline, tabs, a start that fails, a
mount that vanishes during reconfirmation. Proven by mutation — three
HOSTS-KO turned into HOSTS-OK make the test go red.

Assisted-by: Claude Opus 5
(cherry picked from commit d4f9358c6cb562029cc2ca9eb478d80c6a0a19a4)
2026-08-29 01:53:03 -04:00
e3138bea7e [FIX] proxmox : ne pas démarrer le pare-feu depuis l'extérieur
Une révision adversariale de la réparation à distance a rendu un constat que
ses TROIS lentilles — réseau, systemd, shell — ont trouvé indépendamment :
démarrer pve-firewall peut couper le ssh qui répare. Sa configuration vit dans
/var/lib/pve-cluster/config.db, donc elle est invisible tant que /etc/pve
n'est pas monté — c'est-à-dire exactement dans l'état qu'on répare. On
appliquerait des règles qu'on ne peut pas lire, sur la seule voie d'accès à la
machine.

Il n'est pas nécessaire au but : le stockage et le suivi demandent pve-cluster
et pvestatd, l'interface web pveproxy. Il repartira au prochain démarrage,
quand /etc/pve sera monté à temps. Le retirer de la liste coûte donc rien et
supprime le seul geste qui pouvait isoler un hôte.

Deux autres constats de la même révision, également réels.

Le gel de cloud-init gardait sur l'EXISTENCE du fichier. Or « printf … > » le
TRONQUE avant d'écrire : une coupure au mauvais moment laisse zéro octet, et
la garde annonce « déjà gelé » pour toujours. cloud-init continue de remettre
127.0.1.1 à chaque démarrage et le défaut redevient invisible — celui-là même
que ce code existe pour supprimer. La garde porte maintenant sur le CONTENU.

Et les adresses de lien-local passaient pour routables. Mesuré : « hostname
--ip-address » peut ne rendre QUE des fe80::, et une APIPA en 169.254 passait
le seul test « ne commence pas par 127. ». pmxcfs n'a alors rien
d'utilisable, mais le diagnostic concluait l'inverse et renvoyait vers
journalctl au lieu de /etc/hosts.

Enfin « la sonde n'a pas répondu » n'est plus lu comme « rien n'est monté » :
un dépassement de délai rend les mêmes vides, et on affirmait une cause qu'on
n'avait pas constatée.

--- EN ---

An adversarial review of the remote repair produced one finding all THREE of
its lenses — network, systemd, shell — reached independently: starting
pve-firewall can cut the ssh doing the repair. Its configuration lives in
/var/lib/pve-cluster/config.db, so it is invisible while /etc/pve is unmounted
— exactly the state being repaired. We would apply rules we cannot read, over
the machine's only way in.

It is not needed for the goal: storage and monitoring need pve-cluster and
pvestatd, the web interface pveproxy. It will come back at the next boot, when
/etc/pve mounts in time. Removing it from the list costs nothing and removes
the one gesture that could isolate a host.

Two more findings from the same review, equally real.

The cloud-init freeze guarded on the file's EXISTENCE. But "printf … >"
TRUNCATES before writing: an ill-timed cut leaves zero bytes, and the guard
then reports "already frozen" forever. cloud-init keeps putting 127.0.1.1 back
at every boot and the defect becomes invisible again — the very one this code
exists to remove. The guard now looks at the CONTENT.

And link-local addresses counted as routable. Measured: "hostname
--ip-address" can return ONLY fe80:: entries, and an APIPA 169.254 passed the
lone "does not start with 127." test. pmxcfs then has nothing usable, yet the
diagnosis concluded the opposite and pointed at journalctl instead of
/etc/hosts.

Finally "the probe did not answer" is no longer read as "nothing is mounted": a
timeout returns the same emptiness, and we were asserting a cause we had not
measured.

Assisted-by: Claude Opus 5
(cherry picked from commit fa9fb729d82e8d1a8e4fb549cc8061c7281b5dcb)
2026-08-29 01:53:03 -04:00
93b6256dba [ADD] déploiement : la VM clone le dépôt distant, pas ce checkout
« Le problème est revenu » — alors qu'il était corrigé la veille. La VM ne
reçoit pas le checkout d'ici : elle CLONE la branche depuis le dépôt distant.
Tout ce qui tourne dedans — install_proxmox.sh, les scripts d'installation, le
Makefile — vient donc de là.

Vécu deux fois de suite. Le correctif de /etc/hosts était commité ici, absent
du distant : chaque VM déployée ensuite recevait l'ancien script, et le même
défaut revenait à l'identique. Rien ne le disait, et il a fallu comparer les
deux versions du fichier à la main pour comprendre. Soixante-et-onze commits
séparaient les deux.

L'écart est donc dit AVANT de déployer, là où l'on peut encore renoncer : le
nombre, les trois premiers sujets, et « git push ». Sur les deux voies, car
les deux clonent.

Une branche que le distant ne connaît pas n'est pas un écart — c'est une
question qui ne se pose pas. La dire quand même vaudrait un avertissement à
chaque déploiement d'une branche neuve.

--- EN ---

"The problem came back" — though it had been fixed the day before. The VM does
not receive this checkout: it CLONES the branch from the remote. Everything
that runs inside it — install_proxmox.sh, the install scripts, the Makefile —
comes from there.

Twice in a row. The /etc/hosts fix was committed here and absent from the
remote: every VM deployed afterwards got the old script, and the same defect
returned unchanged. Nothing said so, and it took comparing both versions of
the file by hand to understand. Seventy-one commits separated them.

The gap is therefore stated BEFORE deploying, where you can still back out:
the count, the first three subjects, and "git push". On both paths, since both
clone.

A branch the remote does not know is not a gap — it is a question that does
not arise. Saying it anyway would mean a warning on every deployment of a new
branch.

Assisted-by: Claude Opus 5
(cherry picked from commit de27be5e736eb6e9bd01efd292e01c3b2231f91a)
2026-08-29 01:53:02 -04:00
98c2355ca0 [FIX] suivi : relevé Proxmox squelettique, index par nom, état terminal
Une cause, deux symptômes. « /cluster/resources » est bâti par pvestatd ;
celui-ci arrêté, l'hôte rend quand même une entrée par VM, mais
SQUELETTIQUE — ni nom, ni mémoire, ni disque, et « status: unknown ». Le
relevé était indexé par NOM : l'entrée disparaissait donc, la VM passait pour
absente alors que l'hôte venait de la nommer, et trois tours plus tard 🗑.
Comme « effacée » est un état TERMINAL, la ligne comptait pour finie — d'où
« 1/1 terminées · 00:09 » sur une installation qui tournait.

Le relevé est maintenant indexé par VMID, seul identifiant unique d'un hôte
Proxmox, et la correspondance vers les noms se fait là où le manifeste est
sous les yeux. Une entrée squelettique reste donc une VM présente, avec ce que
l'hôte sait d'elle — sa taille occupée, que « du » donne par VMID.

Reste à savoir pourquoi pvestatd était mort. Son journal le dit mot pour mot :
« ipcc_send_rec failed: Connection refused » — pve-cluster absent, c'est-à-dire
la panne /etc/hosts d'hier. Tous les services de Proxmox avaient échoué
ensemble, et systemd n'y revient jamais seul. L'installation relançait le seul
pve-cluster ; elle relance désormais l'ensemble, pve-cluster d'abord puisqu'il
monte /etc/pve.

Vérifié sur l'hôte : pvestatd relancé, et les colonnes passent de « - - - » à
« 3.4G/4.0G, 2.3G/25G, 2.2G écrit ».

--- EN ---

One cause, two symptoms. "/cluster/resources" is built by pvestatd; with it
stopped the host still returns one entry per VM, but SKELETAL — no name, no
memory, no disk, and "status: unknown". Readings were indexed by NAME, so that
entry vanished, the VM looked absent although the host had just named it, and
three rounds later 🗑. Since "deleted" is a TERMINAL state the row counted as
finished — hence "1/1 done · 00:09" on a running install.

Readings are now indexed by VMID, a Proxmox host's only unique identifier, and
the mapping to names happens where the manifest is at hand. A skeletal entry
therefore stays a present VM, with whatever the host does know about it — its
used size, which "du" reports per VMID.

Why was pvestatd dead? Its journal says it verbatim: "ipcc_send_rec failed:
Connection refused" — no pve-cluster, that is yesterday's /etc/hosts fault. All
of Proxmox's services had failed together, and systemd never returns to them on
its own. The installer restarted pve-cluster alone; it now restarts the whole
set, pve-cluster first since it mounts /etc/pve.

Verified on the host: pvestatd restarted, and the columns go from "- - -" to
"3.4G/4.0G, 2.3G/25G, 2.2G written".

Assisted-by: Claude Opus 5
(cherry picked from commit 3fb85f66842d4b0e9d6ae6446691229b31ef725a)
2026-08-29 01:53:02 -04:00
855acd8e61 [FIX] proxmox : pmxcfs sans adresse routable, et le stockage vide
Rapporté sur un Proxmox imbriqué. « pvesm » ne parle qu'à travers /etc/pve,
monté par pmxcfs ; pmxcfs à terre, la commande répond « Connection refused »,
la liste est vide, et l'écran s'arrête sur « aucun stockage » — trois étages
au-dessus du défaut.

pmxcfs ne démarrait pas parce que le nom d'hôte ne résolvait que vers
127.0.1.1, et il cherche une adresse ROUTABLE. L'installation corrige bien
/etc/hosts, mais l'image cloud règle « manage_etc_hosts: True » : cloud-init
le réécrit à CHAQUE démarrage. C'est le redémarrage désormais automatique qui
l'a révélé — l'installation corrigeait, le reboot amorçait le bon noyau, et
cloud-init défaisait la correction dans le même mouvement. Un fichier de
surcharge le gèle.

Deuxième geste manquant : systemd marque pve-cluster « failed » après cinq
essais rapprochés et n'y revient JAMAIS seul. Corriger /etc/hosts ne suffisait
donc pas ; l'installation relance l'unité et CONSTATE le montage plutôt que de
le supposer.

Et l'écran nomme maintenant la cause quand il n'a pas de stockage, plutôt que
de laisser chercher.

Un détail qui aurait fait un faux diagnostic : la sonde de montage
n'interroge pas storage.cfg. Ce fichier N'EXISTE PAS sur une installation
neuve — Proxmox se contente alors de ses stockages par défaut, et « local »
répond parfaitement. Vérifié sur l'hôte : /etc/pve monté, storage.cfg absent,
« pvesm status » rendant local avec 25 Go libres. C'est « .version », fichier
virtuel de pmxcfs, qui fait foi.

--- EN ---

Reported on a nested Proxmox. "pvesm" only speaks through /etc/pve, mounted by
pmxcfs; with pmxcfs down the command answers "Connection refused", the list is
empty, and the screen stops at "no storage" — three floors above the defect.

pmxcfs would not start because the hostname resolved only to 127.0.1.1, and it
needs a ROUTABLE address. The installer does fix /etc/hosts, but the cloud
image sets "manage_etc_hosts: True": cloud-init rewrites it at EVERY boot. The
now-automatic reboot is what revealed it — the install fixed it, the reboot
booted the right kernel, and cloud-init undid the fix in the same motion. An
override file freezes it.

Second missing step: systemd marks pve-cluster "failed" after five rapid
attempts and never returns to it on its own. Fixing /etc/hosts was therefore
not enough; the installer restarts the unit and VERIFIES the mount rather than
assuming it.

And the screen now names the cause when it has no storage, instead of leaving
you to hunt.

One detail that would have made a false diagnosis: the mount probe does not
ask for storage.cfg. That file DOES NOT EXIST on a fresh install — Proxmox
then uses its default storages, and "local" answers perfectly. Verified on the
host: /etc/pve mounted, storage.cfg absent, "pvesm status" returning local with
25 GB free. It is ".version", a pmxcfs virtual file, that tells the truth.

Assisted-by: Claude Opus 5
(cherry picked from commit 6212853048ba8154f2833744e45076d70bd33c72)
2026-08-29 01:53:02 -04:00
7dc5df67c3 [FIX] proxmox : le pont interne prenait l'adresse de sa propre passerelle
Un Proxmox dans un Proxmox hérite du réseau interne de son parent : la VM
vivait en 10.10.10.152, passerelle 10.10.10.1. Le pont interne, lui, avait son
adresse CODÉE EN DUR à 10.10.10.1/24. Lui demander de la poser sur son propre
pont, c'est prendre l'adresse de sa passerelle et rendre tout le /24 local. La
machine s'isole au milieu de la commande qui la configure : « ifup » n'a jamais
rendu la main, la VM ne répondait plus ni en ssh ni en ping.

Le réseau est donc CHOISI, d'après ce que l'hôte connaît déjà — ses adresses et
ses routes, car une route sans adresse locale suffit à créer le conflit, et la
route par défaut en est l'exemple exact. Le chevauchement se calcule sur les
réseaux et non sur les trois premiers octets : « 10.0.0.0/8 » écarte alors bien
tous les candidats en 10.x. Plus aucun libre ? On le dit, plutôt que d'en
écraser un — écraser, ici, c'est couper la seule voie d'accès.

Le repli « ifreload -a » s'en va aussi. Il rechargeait TOUTES les interfaces, y
compris celle qui porte la session, et sur une image cloud l'interface
principale est décrite ailleurs — ifupdown2 la descend sans la remonter. Le
repli monte maintenant le pont à la main, sans toucher à rien d'autre ; la
strophe le rend persistant. La règle de masquerading se teste avant de
s'ajouter, donc une reprise n'empile rien.

Le test d'origine interdisait « 2>/dev/null » sur toute la ligne pour que
l'erreur d'ifup reste lisible. L'intention est gardée, portée sur l'appel à
ifup seul : le repli, lui, sonde légitimement.

--- EN ---

Proxmox inside Proxmox inherits its parent's internal network: the VM lived at
10.10.10.152, gateway 10.10.10.1. The internal bridge had its address HARDCODED
to 10.10.10.1/24. Asking it to put that on its own bridge takes its gateway's
address and makes the whole /24 local. The machine isolates itself in the
middle of the command configuring it: "ifup" never returned, the VM answered
neither ssh nor ping.

The subnet is now CHOSEN from what the host already knows — its addresses and
its routes, since a route with no local address is enough to collide, and the
default route is exactly that case. Overlap is computed on networks rather than
on the first three octets, so "10.0.0.0/8" correctly rules out every 10.x
candidate. None left? We say so rather than overwrite one — overwriting here
means cutting the only way in.

The "ifreload -a" fallback goes too. It reloaded ALL interfaces, including the
one carrying the session, and on a cloud image the main interface is described
elsewhere — ifupdown2 takes it down without bringing it back. The fallback now
raises the bridge by hand, touching nothing else; the stanza makes it
persistent. The masquerade rule is checked before being added, so a retry piles
nothing up.

The original test banned "2>/dev/null" across the whole line so ifup's error
stayed readable. That intent is kept, narrowed to the ifup call itself: the
fallback legitimately probes.

Assisted-by: Claude Opus 5
(cherry picked from commit 57991b186cf891b0db6b7228fb626c4c1af317cd)
2026-08-29 01:53:02 -04:00
7a91b010bd [ADD] suivi : le redémarrage fait partie de l'installation de Proxmox
Proxmox VE n'existe qu'après un redémarrage : tant que la VM tourne le noyau
de son image cloud, elle n'a aucun module netfilter — ni pont NAT, ni invité.
install_proxmox.sh pose le noyau puis s'arrête, à raison, car lancé par ssh un
reboot couperait sa session et ferait passer l'installation pour un échec. On
le découvrait donc des jours plus tard, en créant un pont.

Le redémarrage revient à l'enveloppe de lancement, qui tourne sur NOTRE
machine et survit à celui de la VM : installation, reboot, attente, puis
vérification du noyau. Le ✅ ne s'écrit qu'après, et il veut donc dire
« hyperviseur utilisable ».

Trois choix méritent d'être dits. On ne redémarre qu'après un SUCCÈS —
redémarrer après un échec effacerait la seule machine sur laquelle on pouvait
chercher. On n'attend pas que ssh « revienne » mais que « uname -r » porte le
motif attendu : sshd répond encore une seconde ou deux après l'ordre, et on
lirait l'ancien noyau en croyant avoir la réponse. Et l'absence du noyau
attendu est un vrai ÉCHEC, pas un avertissement.

Le shell est exécuté par les tests, ssh bouchonné, dans les quatre cas — dont
celui où les deux premières lectures rendent l'ancien noyau. Un garde qu'on ne
sait pas éprouver s'ouvre le jour où il casse.

La note du sommaire ne paraît plus que sans suivi, où rien ne redémarre :
réclamer un redémarrage déjà fait est une consigne fausse.

--- EN ---

Proxmox VE only exists after a reboot: while the VM runs its cloud image's
kernel it has no netfilter module — no NAT bridge, no guest.
install_proxmox.sh installs the kernel then stops, rightly, since run over ssh
a reboot would cut its own session and make the install look failed. So you
found out days later, when creating a bridge.

The reboot moves to the launch wrapper, which runs on OUR machine and survives
the VM's: install, reboot, wait, then verify the kernel. The ✅ is written only
after, and therefore means "usable hypervisor".

Three choices worth stating. We reboot only after SUCCESS — rebooting after a
failure would wipe the one machine you could investigate. We do not wait for
ssh to "come back" but for "uname -r" to carry the expected pattern: sshd
answers for another second or two after the order, and we would read the old
kernel believing we had the answer. And a missing expected kernel is a real
FAILURE, not a warning.

The shell is executed by the tests, ssh stubbed, in all four cases — including
the one where the first two reads return the old kernel. A guard you cannot
exercise opens the day it breaks.

The summary note now appears only without monitoring, where nothing reboots:
asking for a reboot already done is a false instruction.

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
eb5e607e1e [FIX] proxmox : le pont NAT s'écrivait avant de savoir si le NAT existe
« Table does not exist » : six lignes d'iptables et « code de retour 1 »,
après avoir déjà posé la strophe dans /etc/network/interfaces. Rien dans ce
bruit ne dit qu'il faut redémarrer.

L'hôte tournait le noyau cloud de Debian, qui est dépouillé de tout netfilter
— aucun module NAT, ni legacy ni nft. Et le cas n'a rien d'exotique : c'est
notre propre install_proxmox.sh qui le produit. Il pose le noyau Proxmox sans
redémarrer, à raison — lancé par ssh, un reboot couperait la session et ferait
passer l'installation pour un échec. Une Proxmox imbriquée fraîchement
installée est donc TOUJOURS dans cet état.

L'avertissement sur le noyau existait déjà, mais à la CONFIRMATION de l'hôte,
et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un pont,
et plus personne ne rappelle rien. Le garde va donc là où la conséquence
tombe, et AVANT toute écriture. Il interroge la table NAT elle-même et non le
NOM du noyau — « -pve » est un indice, pas une preuve — puis nomme le noyau
en cours, celui qui est posé, et la commande qui règle l'affaire.

Le sommaire de déploiement le dit désormais aussi, tant qu'on lit encore
l'écran plutôt qu'au bout d'un journal d'une heure.

--- EN ---

"Table does not exist": six lines of iptables and "exit code 1", after the
stanza had already been written into /etc/network/interfaces. Nothing in that
noise says a reboot is needed.

The host was running Debian's cloud kernel, stripped of all netfilter — no NAT
module, legacy or nft. And the case is not exotic: our own install_proxmox.sh
produces it. It installs the Proxmox kernel without rebooting, rightly — run
over ssh, a reboot would cut the session and make the install look failed. A
freshly installed nested Proxmox is therefore ALWAYS in this state.

The kernel warning already existed, but at host CONFIRMATION, and the host is
then remembered: you come back days later to create a bridge and nothing
reminds you. So the guard moves to where the consequence lands, and BEFORE any
write. It asks the NAT table itself rather than the kernel's NAME — "-pve" is
a hint, not a proof — then names the running kernel, the installed one, and
the command that settles it.

The deployment summary now says it too, while the screen is still being read
rather than at the end of an hour-long log.

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
5e6c976ecb [FIX] nettoyage : les enfants s'en vont avec leur rebond
La VM Proxmox locale effacée, le nettoyage a retiré son entrée ssh — c'était
juste — et GARDÉ les trois entrées qui rebondissaient par elle, en les
annonçant « mènent encore quelque part ». Trois culs-de-sac, désignés comme
vivants.

Deux fautes. Un ProxyJump valait preuve de vie À LUI SEUL, au motif qu'il
désigne une VM imbriquée que virsh ne connaîtra jamais : le raisonnement
oubliait que le rebond, lui, peut avoir disparu. Et chaque entrée était jugée
ISOLÉMENT, alors que retirer le parent orpheline ses enfants — qui
orphelinent les leurs. D'où un point fixe, et non une passe.

Un rebond qu'on ne gère pas — hôte personnel, adresse, nom DNS — reste
supposé vivant : on n'efface pas sur une supposition. Mais un nom de NOTRE
nommage sans entrée et sans domaine ne mène nulle part, et c'est exactement
l'état qu'un nettoyage précédent laisse derrière lui.

La liste des orphelines dit maintenant POURQUOI. « Son rebond n'existe
plus : erplibre-proxmox-9 » est la seule chose qui permet de répondre non en
connaissance de cause.

--- EN ---

With the local Proxmox VM deleted, the cleanup removed its ssh entry — rightly
— and KEPT the three entries hopping through it, announcing them as "still
lead somewhere". Three dead ends, labelled alive.

Two defects. A ProxyJump counted as proof of life ON ITS OWN, on the grounds
that it names a nested VM virsh will never know: the reasoning forgot the jump
itself can be gone. And each entry was judged IN ISOLATION, while removing a
parent orphans its children — which orphan theirs. Hence a fixed point, not a
single pass.

A jump we do not manage — personal host, address, DNS name — stays presumed
alive: we do not delete on a guess. But a name of OUR OWN convention with no
entry and no domain leads nowhere, and that is exactly the state a previous
cleanup leaves behind.

The orphan list now says WHY. "Its jump host is gone: erplibre-proxmox-9" is
the only thing that lets you answer no knowingly.

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
67a59d0522 [ADD] analyse: anonymiser une copie, sans IA et sans rien casser
Des mots pris dans une liste, des nombres tirés entre 0 et 1000, écrits
en SQL. Aucun modèle, aucun réseau — un test le vérifie sur les imports.

Le difficile n'est pas de remplacer, c'est de savoir ce qu'on n'a PAS le
droit de toucher. Mesuré sur une base 18 réelle : 505 champs `selection`
sont stockés en varchar, 2693 many2one sont des entiers, 194 textes sont
des jsonb par langue, 301 contraintes d'unicité attendent une collision.
« Tous les champs string » n'existe pas ; on croise ir_model_fields,
pg_attribute et pg_constraint, et aucune des trois ne suffit seule.

Trois pièges ont été trouvés en LANÇANT l'outil, pas en le relisant :
PostgreSQL refuse d'indexer un ARRAY[...] sans parenthèses,
res_partner.credit_limit est un jsonb qu'Odoo appelle float, et
crm_lead.probability porte un CHECK qui interdit 1000. Chaque fois
l'écriture a échoué et la base est restée intacte : une seule
transaction, tout ou rien.

Preuve sur copie jetable : empreinte du schéma identique, 848 tables,
6495 contraintes, arch_db et xmlid intacts, lang et many2one inchangés —
seules les colonnes visées ont changé.

--- EN ---

Words from a list, numbers drawn between 0 and 1000, written in SQL. No
model, no network — a test checks that on the imports.

The hard part is not replacing, it is knowing what must NOT be touched.
Measured on a real 18 database: 505 `selection` fields are stored as
varchar, 2693 many2one are integers, 194 texts are per-language jsonb,
301 unique constraints await a collision. "All string fields" does not
exist; we cross ir_model_fields, pg_attribute and pg_constraint, and none
of the three is enough alone.

Three traps were found by RUNNING it, not by rereading it: PostgreSQL
refuses to subscript a bare ARRAY[...], res_partner.credit_limit is a
jsonb Odoo calls float, and crm_lead.probability has a CHECK forbidding
1000. Each time the write failed and the database stayed intact: one
transaction, all or nothing.

Proof on a throwaway copy: identical schema fingerprint, 848 tables, 6495
constraints, arch_db and xmlids intact, lang and many2one unchanged —
only the targeted columns changed.

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
db9472ef69 [FIX] proxmox : l'ancienne entrée ssh s'en va avec la convention
Le nom chaîné devient systématique, mais les entrées écrites AVANT portent le
nom court — et rien ne les retirerait : elles ne déclarent pas le nom qu'on
écrit maintenant. Deux blocs mèneraient à la même machine, exactement ce
qu'on venait d'enlever.

Le ProxyJump tranche : un bloc qui rebondit par CET hôte est le nôtre, on le
retire. Celui d'une VM locale homonyme n'en a pas, et on n'y touche jamais ;
celui d'un autre hôte Proxmox non plus.

Le drapeau Odoo gagne son test au passage. Il tombait pour la même raison que
les colonnes vides — la sonde est le dernier maillon de la suite distante, et
un parc où une seule VM n'a pas d'Odoo, un hyperviseur imbriqué par exemple,
finit en échec. Vérifié sur les trois VM : l'hôte rend bien « ODOO » pour les
deux qui écoutent, et le navigateur répondait 303 pendant que la colonne
disait « — ».

--- EN ---

The chained name becomes systematic, but entries written BEFORE carry the
short one — and nothing would retire them: they do not declare the name we
now write. Two blocks would lead to the same machine, exactly what we had
just removed.

The ProxyJump decides: a block hopping through THIS host is ours, so it goes.
A local namesake's has none, and is never touched; another Proxmox host's
neither.

The Odoo flag gains its test along the way. It failed for the same reason as
the empty columns — the probe is the remote pipeline's last link, and a fleet
where a single VM has no Odoo, a nested hypervisor for instance, ends in
failure. Verified on all three VMs: the host does return "ODOO" for the two
that listen, and the browser answered 303 while the column said "—".

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
f31e2370a7 [FIX] suivi : un relevé Proxmox jeté, et deux lignes qui montraient une autre machine
Sur trois VM d'un même Proxmox, une seule avait ses colonnes vides — et les
deux autres montraient les chiffres d'une AUTRE machine. Deux fautes, dont
une était le miroir d'un correctif précédent.

Le code de sortie de la suite distante est celui de son DERNIER maillon, la
sonde Odoo. Tant qu'Odoo n'écoute pas — c'est-à-dire pendant TOUTE
l'installation, précisément quand on regarde — la boucle finit en échec et le
relevé, parfait, était jeté. On avait corrigé l'erreur inverse, un code 0 pris
pour une réponse ; exiger 0 était la même faute retournée. Seule une liste de
ressources analysable prouve une réponse.

Pendant ce temps, « virsh domstats » indexe par NOM, et un nom se partage :
les deux VM qui avaient un homonyme LOCAL affichaient ses chiffres. Mesuré —
1,5 Gio de RAM sur 12 et 58 Gio de disque sur 65, quand la vraie tournait avec
3 Gio et 25. Les relevés locaux d'une VM qui vit ailleurs sont donc retirés
AVANT d'ajouter ceux de l'hôte : un hôte muet laisse la colonne VIDE, ce qui
est vrai. Une colonne vide se remarque ; une colonne juste et fausse, non.

L'alias enfin. Prendre le nom court quand il se trouvait libre donnait un parc
incohérent : sur ce même déploiement, deux VM ont reçu « hôte+vm » — leurs
noms étaient pris par des domaines locaux — et la troisième son nom court. Une
convention qui dépend de ce qui traîne dans le fichier n'est pas une
convention. Le nom chaîné est systématique.

--- EN ---

Of three VMs on one Proxmox, only one had empty columns — and the other two
showed ANOTHER machine's figures. Two defects, one the mirror of an earlier
fix.

A remote pipeline's exit code is its LAST link's, the Odoo probe. While Odoo
is not listening — that is, during the WHOLE install, exactly when you are
watching — the loop ends in failure and the reading, perfectly good, was
thrown away. We had fixed the opposite error, a 0 taken for an answer;
demanding 0 was the same mistake reversed. Only a parsable resource list
proves an answer.

Meanwhile "virsh domstats" indexes by NAME, and a name is shared: the two VMs
with a LOCAL namesake displayed its figures. Measured — 1.5 GiB of RAM out of
12 and 58 GiB of disk out of 65, while the real one ran on 3 GiB and 25. Local
readings for a VM that lives elsewhere are therefore dropped BEFORE the host's
are added: a silent host leaves the column EMPTY, which is true. An empty
column gets noticed; a plausible wrong one does not.

The alias, finally. Taking the short name while it happened to be free gave an
inconsistent fleet: in that same deployment two VMs got "host+vm" — their
names were held by local domains — and the third its short name. A convention
that depends on what happens to sit in the file is not a convention. The
chained name is now systematic.

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
0551d807ac [REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.

Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.

Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.

Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.

--- EN ---

On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.

The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.

The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.

One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
8be55031ab [FIX] suivi : effacer depuis un suivi rouvert vérifie d'abord l'identité
Le tableau de bord se rouvre sur un manifeste passé — c'est fait pour, les
installations partent détachées. Mais un nom de domaine se réemploie et un
VMID libéré est RÉATTRIBUÉ : effacer « le 101 » d'un run de mars, c'est
effacer ce qui porte le 101 aujourd'hui, et « erplibre-ubuntu-2604 » de mars
n'est pas celui d'aujourd'hui. Même famille que tout le reste — on jugeait
sur le nom, avec ici la pire conséquence.

La commande porte donc son garde, et non l'écran : elle protège ainsi tous
ses appelants, et la vérification se fait SUR la machine, à l'instant
d'effacer. Sur Proxmox, le VMID doit encore porter ce nom. En local, l'UUID
du domaine — relevé au lancement, seul instant où l'on sait que ce nom
désigne bien cette machine-là. Un manifeste écrit avant ce correctif n'en a
pas : il retombe sur la protection d'avant plutôt que de bloquer.

Le garde du VMID est une fonction à part, exécutable telle quelle. Il
traverse deux « shlex.quote » avant d'atteindre un dash, et un garde qu'on ne
sait pas éprouver s'OUVRE le jour où il casse. Vérifié sur erplibre-proxmox-9
sans rien détruire : le VMID 100 refusé sous un nom périmé, accepté sous le
sien.

--- EN ---

The dashboard reopens on a past manifest — by design, since installs run
detached. But a domain name gets reused and a freed VMID is REASSIGNED:
deleting "the 101" from a March run deletes whatever holds 101 today, and
March's "erplibre-ubuntu-2604" is not today's. Same family as the rest — we
judged by name, here with the worst consequence.

The command carries its guard, not the screen: that protects every caller,
and the check happens ON the machine, at the moment of deletion. On Proxmox
the VMID must still bear that name. Locally, the domain's UUID — recorded at
launch, the only moment we know that name means that machine. A manifest
written before this fix has none: it falls back to the previous protection
rather than blocking.

The VMID guard is its own function, runnable as is. It crosses two
"shlex.quote" layers before reaching a dash, and a guard you cannot exercise
OPENS the day it breaks. Verified on erplibre-proxmox-9 without destroying
anything: VMID 100 refused under a stale name, accepted under its own.

Assisted-by: Claude Opus 5
2026-08-25 03:31:12 -04:00
dcbba5295c [FIX] proxmox : quatre écrans qui parlaient d'une machine locale
La confirmation de suppression promettait à TOUTE VM « son disque qcow2
EFFACÉ », puis nommait /var/lib/libvirt/images/<nom>.qcow2. Sur une VM
Proxmox ce fichier n'existe pas — au mieux, au pire c'est celui d'une autre
VM du même nom. C'est la peur exacte qui avait fait remonter le nettoyage.
Elle nomme désormais l'hôte, le VMID et « qm destroy ».

« Console de l'hyperviseur » lisait le port par « virsh vncdisplay ». Un
Proxmox n'a pas de libvirt : l'échec se lisait « écran fermé » et on
conseillait « sudo virsh edit » sur une machine sans ce binaire. Ce n'est pas
un écran fermé, c'est la mauvaise question — Proxmox sert le sien par un
ticket. Les deux vrais chemins sont nommés : la console série, l'interface
web par tunnel.

La colonne Odoo était un 🟢 acquis pour toujours : « Odoo ne redescend pas en
cours d'install » est faux — le service redémarre au moins une fois, et il
lui arrive de mourir. Elle est relue, gratuitement sur Proxmox, toutes les
trente secondes ailleurs. Un hôte muet reste distinct d'un Odoo tombé.

Enfin « Versions principales » (F7) manquait à l'écran Proxmox, qui affiche
pourtant le même catalogue. Les trois gestes du catalogue vivent maintenant
dans le socle du plan, où ils ne peuvent plus diverger.

--- EN ---

The delete confirmation promised EVERY VM "its qcow2 disk ERASED", then named
/var/lib/libvirt/images/<name>.qcow2. On a Proxmox VM that file does not
exist — at best; at worst it is another VM's, of the same name. That is the
very fear that surfaced the cleanup report. It now names the host, the VMID
and "qm destroy".

"Hypervisor console" read the port through "virsh vncdisplay". Proxmox has no
libvirt: the failure read as "screen closed" and we advised "sudo virsh edit"
on a machine without that binary. It is not a closed screen, it is the wrong
question — Proxmox serves its own by ticket. Both real paths are named: the
serial console, the web interface through a tunnel.

The Odoo column was a 🟢 acquired forever: "Odoo does not go back down during
the install" is false — the service restarts at least once, and it does die.
It is re-read, free on Proxmox, every thirty seconds elsewhere. A silent host
stays distinct from a dead Odoo.

Finally "Main versions" (F7) was missing from the Proxmox screen, which shows
the same catalog. The catalog's three gestures now live in the plan
foundation, where they can no longer diverge.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
5e75f3da31 [FIX] proxmox : un seul nom par entrée ~/.ssh/config, et le bon
L'entrée portait deux noms sur sa ligne « Host » — le chaîné « hôte+vm » et
le court : « Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest ». Le second est un doublon dès que le premier suffit.
ssh n'a besoin que d'un nom ; le doubler n'ajoute qu'une façon de plus
d'écrire la même adresse. Rapporté.

Un seul, donc, et choisi : le nom court quand il est LIBRE, c'est celui qu'on
tape ; le chaîné quand il désignerait une autre machine — une VM locale
homonyme, ou la VM d'un autre hôte Proxmox. Ce qui a forcé le changement est
nommé à l'écran plutôt que laissé en surprise.

« Pris » se juge sur le ProxyJump du bloc et non sur sa seule présence. Le
test l'a montré avant l'usage : notre propre entrée, réécrite à chaque
déploiement, se prenait pour une rivale et le nom basculait d'une fois sur
l'autre.

--- EN ---

The entry carried two names on its "Host" line — the chained "host+vm" and
the short one: "Host erplibre-proxmox-9+erplibre-arch-latest
erplibre-arch-latest". The second is redundant as soon as the first is
enough. ssh needs one name; doubling it only adds another way to write the
same address. Reported.

One name then, and a chosen one: the short one while it is FREE, since that
is what you type; the chained one when it would point at another machine — a
local VM of the same name, or another Proxmox host's VM. Whatever forced the
change is named on screen rather than left as a surprise.

"Taken" is judged on the block's ProxyJump, not on its mere presence. The
test showed it before use: our own entry, rewritten at every deployment, took
itself for a rival and the name flipped from one run to the next.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
37c63af7e2 [ADD] migration: brancher les deux réparations qui ne tournaient jamais
fix_duplicate_index et restore_config_defaults étaient écrits, éprouvés,
et absents du pilote. Chaque migration refabriquait donc ses index
redondants et reperdait sa liste de prix.

Mesuré sur DEUX chaînes 12 → 18 indépendantes, même base source : 68
relations orphelines, 9 langues au drapeau NULL, une liste de prix
absente, 376 paires d'index en double — les mêmes nombres des deux côtés.
Ce n'est pas un accident d'exécution, c'est le chemin lui-même.

Les index à partir du palier 17 : avant, la convention n'a pas changé.
Les réglages au DERNIER palier : l'outil charge le registre, et seul
l'état final compte. Les deux en wait_at_error=False — avec --apply, le
code 1 dit « il en reste », pas « je suis tombé », et cela n'arrête pas
six paliers.

--- EN ---

fix_duplicate_index and restore_config_defaults were written, proven, and
absent from the driver. Every migration therefore rebuilt its redundant
indexes and lost its default pricelist again.

Measured on TWO independent 12 → 18 chains from the same source: 68
orphan relations, 9 languages with a NULL flag, one missing pricelist,
376 duplicate index pairs — the same numbers on both. Not a fluke of one
run: the path itself.

Indexes from step 17 onward: before that the convention had not changed.
Config defaults at the LAST step: the tool loads the registry, and only
the final state matters. Both with wait_at_error=False — with --apply,
exit 1 means "some remain", not "I crashed", and that must not halt six
steps.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
6b985e57ab [REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.

La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.

Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.

Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.

Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.

--- EN ---

VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.

Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.

Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.

Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.

The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
9a7b8cb36f [ADD] analyse: l'état d'une instance, lu pour l'usage qu'on en fait
Le même chiffre veut dire deux choses opposées. Zéro cron actif est le
succès attendu d'une copie et une panne totale sur une production. Un
rapport qui ignore cela crie au loup sur ce qu'on vient de demander, et
l'on cesse de le lire. L'attente est donc déclarée, copy ou live, et
chaque contrôle dit ce qu'il juge sous l'une et sous l'autre.

Deux contrôles ont été ÉCARTÉS sous copy après mesure : sur la base 12
d'origine, jamais démarrée, 11 crons étaient déjà en retard et db_backup
déjà vide — notre propre update_prod_to_dev les efface. Les afficher en
rouge aurait été du bruit ; en vert, un mensonge. Ils sont montrés non
jugés, avec la raison.

Le code Python en base a été mesuré et abandonné : 132 actions serveur,
zéro citant un modèle inexistant, et les 3 « modèles sans table » sont
ir.autovacuum et deux autres modèles abstraits d'Odoo.

--- EN ---

The same number means two opposite things. Zero active cron is the
expected success of a copy and a total outage on production. A report
that ignores this cries wolf over what was just requested, and stops
being read. The expectation is therefore declared, copy or live, and each
check states what it judges under either.

Two checks were DROPPED under copy after measuring: on the untouched 12
source database, 11 crons were already late and db_backup already empty —
our own update_prod_to_dev deletes them. Red would have been noise; green
a lie. They are shown unjudged, with the reason.

In-database Python was measured and dropped: 132 server actions, none
naming a missing model, and the 3 "models without a table" are
ir.autovacuum and two other Odoo abstract models.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
bf8f975ef0 [FIX] proxmox : six défauts trouvés par un audit, pas à l'usage
Trois autres chemins menaient au 🗑 sur un seul incident, et « effacée » gèle
la ligne pour de bon. Un « virsh list » en échec condamnait TOUT le parc
local. Un statut Proxmox hors des trois attendus — prelaunch, suspended,
internal-error — passait pour une disparition. Et le code de sortie de la
suite distante est celui de son DERNIER maillon : un pvesh en panne se lisait
« l'hôte a répondu sans elle ». Ce qui prouve une réponse, c'est désormais une
liste de ressources analysable.

Le plan annonçait « 25G » quand « qm resize » recevait 20 : la marge
d'ERPLibre se perdait en route, la VM naissait trop petite. « Changer l'état »
choisissait par NOM, or seul le VMID est unique sur un hôte — cocher une VM
en éteignait deux homonymes. L'entrée 13 volait son alias à une VM locale du
même nom. Enfin le déploiement par QUESTIONS avait vieilli seul : il partage
maintenant l'épilogue de l'écran, donc le guide, l'alias protégé, les colonnes
vivantes et le sommaire.

--- EN ---

Three more paths led to 🗑 on a single incident, and "deleted" freezes the row
for good. One failing "virsh list" condemned the WHOLE local fleet. A Proxmox
status outside the three expected ones — prelaunch, suspended, internal-error
— passed for a disappearance. And a remote pipeline's exit code is its LAST
link's: a broken pvesh read as "the host answered without it". Proof of an
answer is now a parsable resource list.

The plan announced "25G" while "qm resize" got 20: ERPLibre's margin was lost
on the way and the VM was born too small. "Change state" selected by NAME,
yet only the VMID is unique on a host — ticking one VM shut down two
namesakes. Menu entry 13 stole its alias from a local VM of the same name.
Finally the QUESTION-driven deployment had aged alone: it now shares the
screen's epilogue — guide, protected alias, live columns and summary.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
9b5d7db2a7 [ADD] proxmox : le guide de connexion, qui manquait à toute VM distante
Rapporté sur Arch : « pas l'écran de connexion, avec le guide qui dit de
prendre pacman, comme sur ubuntu ». Ce n'était pas Arch — c'était Proxmox.
La voie libvirt livre /etc/motd par le « write_files » de cloud-init, et
« qm set » n'offre pas cela : AUCUNE VM déployée sur un hôte Proxmox n'avait
de guide, quelle que soit sa distribution.

Une troisième voie de livraison, par ssh une fois la VM debout, avec la MÊME
source — « guide_files », qui connaissait déjà pacman. Le guide n'annonce le
dépôt que s'il y sera : un guide qui promet ce qui n'existe pas est pire que
pas de guide. Écrit sur la VM Arch de l'utilisateur pour le vérifier.

--- EN ---

Reported on Arch: "no login screen, with the guide telling you to use pacman,
like on ubuntu". It was not Arch — it was Proxmox. The libvirt path delivers
/etc/motd through cloud-init's "write_files", and "qm set" offers no such
thing: NO VM deployed on a Proxmox host had a guide, whatever its distro.

A third delivery path, over ssh once the VM is up, from the SAME source —
"guide_files", which already knew pacman. The guide only announces the
repository if it will be there: a guide promising what does not exist is worse
than none. Written onto the user's Arch VM to check it.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
2011bfecfe [FIX] suivi : pas de poubelle avant d'en être sûr, et mise sur Proxmox
Rapporté : une VM Arch à peine déployée sur Proxmox s'affichait 🗑 dès le
premier tour. « Effacée » est un état TERMINAL — la ligne gèle et ne revient
jamais — et il se déduisait d'UN relevé manquant. Or l'hôte peut être occupé,
la VM en train de naître, le relevé en cache d'avant sa création. On distingue
désormais « l'hôte n'a pas répondu » (on ne sait rien) de « l'hôte a répondu
sans elle » (on compte, trois fois), et la case part de « - » plutôt que d'un
sablier qui affirmerait qu'on attend quelque chose.

L'écran Proxmox n'offrait pas le choix de l'interpréteur Python : il envoyait
donc toujours « automatique », et comme mise n'est jamais installé d'office,
c'était pyenv — qui COMPILE Python depuis le tar.xz. Le choix existe
maintenant des deux côtés, avec le même garde-fou : rien n'est imposé quand
aucune architecture retenue n'est servie par mise.

--- EN ---

Reported: an Arch VM barely deployed on Proxmox showed 🗑 on the very first
pass. "Deleted" is a TERMINAL state — the row freezes and never comes back —
and it was inferred from ONE missing reading. Yet the host may be busy, the VM
may be starting, the reading may be cached from before it existed. We now tell
"the host did not answer" (we know nothing) from "the host answered without
it" (count, three times), and the cell starts at "-" rather than an hourglass
claiming we await something.

The Proxmox screen offered no Python interpreter choice: it therefore always
sent "automatic", and since mise is never installed by default, that meant
pyenv — which COMPILES Python from the tar.xz. The choice now exists on both
sides, with the same guard: nothing is imposed when no selected architecture
is served by mise.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
2521267896 [ADD] analyse: ausculter une base qui n'est pas ici
Les analyses existaient ; le chemin d'AVANT manquait. La base d'un client
est dans un zip, derrière une URL, ou vivante sur un serveur.

« Restant de migration » a dû être écrit : check_migration_quality compare
les bases de PALIER et exige le journal de progression — devant une
sauvegarde isolée, ni l'un ni l'autre n'existe.

Ses compteurs évidents ont été écartés après mesure. Comparés à la base
d'ORIGINE : champs sans colonne 25 → 72, modèles sans table 90 → 158.
Vingt-cinq et quatre-vingt-dix AVANT toute migration : du bruit. Ne
restent que les constats faux en eux-mêmes, 0 avant, non nuls après —
9 langues au drapeau NULL, 68 tables m2m absentes, 414 index doublés.

Le passe-plat RPC n'accepte que la lecture. psql l'obtient du serveur ;
une session RPC n'a rien d'équivalent, et la liste blanche est donc
appliquée dans le passe-plat, pas chez l'appelant.

--- EN ---

The analyses existed; the path BEFORE them did not. A customer database
sits in a zip, behind a URL, or live on a server.

« Migration leftovers » had to be written: check_migration_quality
compares STEP databases and needs the progression log — facing a lone
backup, neither exists.

Its obvious counters were dropped after measuring. Against the ORIGINAL
database: fields with no column 25 → 72, models with no table 90 → 158.
Twenty-five and ninety BEFORE any migration: noise. Only what is wrong in
itself remains, 0 before and non-zero after — 9 languages with a NULL
flag, 68 missing m2m tables, 414 duplicated indexes.

The RPC proxy only reads. psql gets that from the server; an RPC session
has no equivalent, so the allowlist lives in the proxy, not the caller.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
f8bd1c30df [ADD] proxmox : la colonne Odoo, le web et la suppression par l'hôte
La colonne Odoo testait le port 8069 depuis le poste : une VM sur pont
interne n'y répond jamais, elle restait « — » quel que soit l'état d'Odoo.
Le test part maintenant DE L'HÔTE, glissé dans l'appel des statistiques déjà
payé — aucun aller-retour de plus. Éprouvé sur une VM d'essai servant sur
8069 : 🟢, et « — » sur la voisine qui n'a rien.

Deux dernières actions visaient encore la mauvaise machine. La touche « w »
ouvrait une page morte : l'adresse d'un pont interne n'est pas routable
d'ici, elle passe donc par un tunnel local le temps de la visite — tenu par
son PID, car « pkill -f <motif> » tuait le shell qui l'avait lancé, le motif
figurant dans sa propre ligne de commande. Et la SUPPRESSION appelait
« virsh undefine <nom> », qui aurait effacé le domaine local homonyme : elle
passe par « qm destroy <vmid> » sur l'hôte.

--- EN ---

The Odoo column probed port 8069 from the workstation: a VM on an internal
bridge never answers there, so it stayed "—" whatever Odoo was doing. The
probe now runs FROM THE HOST, folded into the stats call already paid for —
no extra round trip. Proven on a test VM serving on 8069: 🟢, and "—" on the
neighbour that serves nothing.

Two last actions still aimed at the wrong machine. Key "w" opened a dead
page: an internal-bridge address is not routable from here, so it now goes
through a local tunnel for the length of the visit — held by its PID, since
"pkill -f <pattern>" killed the very shell that had launched it, the pattern
being in its own command line. And DELETION called "virsh undefine <name>",
which would have erased the homonymous local domain: it now goes through
"qm destroy <vmid>" on the host.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
4c2adb7c56 [FIX] proxmox : viser la bonne machine, et dire la vérité sur le disque
« s » ouvrait encore la VM locale homonyme : la vue de progression n'avait
que le NOM de la VM, et l'entrée ~/.ssh/config n'existe pas encore à ce
moment. Le déploiement lui passe maintenant « ssh -J <hôte> user@<ip> », qui
ne dépend de rien. Deux voisines du même défaut, jamais rapportées mais aussi
graves : la console ouvrait « virsh console <nom> » — celle de la VM LOCALE —
et la pause suspendait la locale. Les deux passent par le VMID sur l'hôte.

La colonne Disque annonçait « 6.0G/6.0G » sur une VM qui n'avait écrit que
1,2 Go : « du -sb » rend la taille APPARENTE, et un disque raw creux la donne
entière. « du -sB1 » compte les blocs. Enfin l'écran de déploiement dit ce qui
l'attend : « Quitter (q) pour lancer l'installation d'ERPLibre » — on
attendait devant une fenêtre terminée sans le savoir.

--- EN ---

"s" still opened the homonymous local VM: the progress view only had the VM's
NAME, and the ~/.ssh/config entry does not exist yet at that point. The
deployment now hands it "ssh -J <host> user@<ip>", which depends on nothing.
Two neighbours of the same defect, never reported but just as serious: the
console opened "virsh console <name>" — the LOCAL VM's — and pause suspended
the local one. Both now go through the VMID on the host.

The Disk column claimed "6.0G/6.0G" on a VM that had written 1.2 GB: "du -sb"
returns the APPARENT size, and a sparse raw disk gives it in full. "du -sB1"
counts blocks. Finally the deployment screen says what awaits it: "Quit (q)
to start the ERPLibre install" — one waited before a finished window without
knowing.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
07a9f66626 [FIX] proxmox : l'installation partait sur la mauvaise machine
Rapporté, et c'est le plus grave de la série. Une VM déployée sur Proxmox
sous le nom « erplibre-ubuntu-2604 » — nom déjà porté par un domaine LOCAL —
a vu son installation d'ERPLibre + Odoo partir sur la VM locale. Deux causes
enchaînées : l'entrée ~/.ssh/config volait l'alias de la locale, et le
lanceur détaché ré-résout l'adresse par virsh à chaque tour, qui a répondu
avec le domaine homonyme. Le journal l'écrivait — « → 192.168.123.118 » —
sans que rien n'alerte.

Une VM distante n'est plus ré-résolue : son alias est la seule vérité,
puisqu'il porte le rebond. Et son alias suit la convention des VM imbriquées,
« hôte+vm », le nom court n'étant ajouté que s'il est libre — l'écran le dit.
S'y ajoute le sommaire final qui manquait, à l'image de QEMU/KVM : ce qui
existe, son adresse, sa commande ssh, son journal.

--- EN ---

Reported, and the worst of the series. A VM deployed on Proxmox under the
name "erplibre-ubuntu-2604" — a name already held by a LOCAL domain — had its
ERPLibre + Odoo install land on the local VM. Two chained causes: the
~/.ssh/config entry stole the local one's alias, and the detached launcher
re-resolves the address through virsh on every pass, which answered with the
homonymous domain. The log said so — "→ 192.168.123.118" — with nothing to
raise an alarm.

A remote VM is no longer re-resolved: its alias is the only truth, since it
carries the jump. And its alias follows the nested-VM convention, "host+vm",
the short name being added only when free — the screen says so. Plus the
final summary that was missing, mirroring QEMU/KVM: what exists, its address,
its ssh command, its log.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
9af1e2b9d2 [FIX] analyse: dire CE QUI est parti avec une pièce jointe
« 409 pièces jointes perdues » au palier 17 → 18 n'en recouvrait presque
aucune. Sur les 516 lignes parties, 452 avaient perdu leur CHAMP PORTEUR
aux paliers 13 et 14 : elles étaient déjà illisibles — Odoo lève un
KeyError en les contrôlant. Ce ne sont pas des données, ce sont des
débris, et la 18 les ramasse.

On ne DÉCLARE pas cela dans SEMANTIC_MAP. Cette carte nomme une TABLE, et
la cause n'est pas la table, ce sont ces lignes-là ; déclarée, elle
rangerait toute perte future de ir_attachment sous « changement d'Odoo »,
cinq mille factures comprises. On la DÉDUIT, ligne par ligne, dans le
même vocabulaire.

Reste 64 lignes en rouge au palier 18 : leur champ est toujours là. Elles
demandent un contrôle d'existence d'enregistrement que je n'ai pas
mesuré, donc je ne l'affirme pas.

--- EN ---

« 409 attachments lost » at the 17 → 18 step covered almost none. Of the
516 rows that went, 452 had lost their CARRYING FIELD at steps 13 and 14:
they were already unreadable — Odoo raises a KeyError checking them. Not
data, debris, and 18 sweeps them up.

We do NOT declare this in SEMANTIC_MAP. That map names a TABLE, and the
cause is not the table but those rows; declared, it would file every
future ir_attachment loss under « an Odoo change », five thousand
invoices included. We DEDUCE it, row by row, in the same vocabulary.

64 rows stay red at step 18: their field is still there. They need a
record-existence check I have not measured, so I do not claim it.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
2ba64132ca [FIX] migration: la sonde de visibilité déclare ce qu'elle n'a pas prouvé
Elle éprouvait 45 modèles sur 125 portant une règle globale, contre
quatre utilisateurs internes — et les quatre étaient administrateurs. Un
modèle que seuls les administrateurs peuvent lire passait donc au vert :
c'est exactement la forme du bug DMS qui a motivé cet outil, vue d'un
autre angle.

Elle ne peut pas créer un témoin ordinaire — ce serait une écriture.
Elle peut dire qu'elle n'en avait pas, annoncer sa couverture au lieu du
seul nombre éprouvé, et déclarer qu'un masquage codé en Python lui
échappe puisqu'elle part d'ir_rule. Un vert qui prouve moins qu'il n'en
a l'air est pire qu'un rouge.

--- EN ---

It tested 45 models out of the 125 carrying a global rule, against four
internal users — and all four were administrators. A model only admins
can read therefore passed green: exactly the shape of the DMS bug that
motivated this tool, seen from another angle.

It cannot create an ordinary witness — that would be a write. It can say
it had none, announce its coverage rather than only the number checked,
and declare that masking coded in Python escapes it since it starts from
ir_rule. A green that proves less than it looks is worse than a red.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
65a8a351a9 [FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.

It now accepts both layouts, so whichever half lands next, it holds.

Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.

The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.

--- FR ---

Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.

Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.

Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.

Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.

Assisted-by: Claude Opus 5
2026-08-25 03:28:39 -04:00
9dd4b3e37a [FIX] qemu : un alias ssh et une adresse ne se jugent pas sur le nom
Suite du nettoyage. Le tri des entrées ~/.ssh/config comparait lui aussi le
NOM aux noms de domaines : après le renommage de la VM, son alias
« erplibre-ubuntu-2404 » ne correspondait plus à rien et a été effacé, alors
qu'il menait à une machine en marche. Une entrée est conservée si son adresse
est celle d'un domaine vivant, si elle porte un rebond — donc écrite pour une
VM que virsh ne verra jamais — ou si son nom est une VM de l'hôte Proxmox. Et
l'écran nomme ce qu'il garde, comme pour les fichiers.

Même racine pour l'adresse affichée : « virsh domifaddr --source arp »
remonte les passerelles des ponts, et le repli « la dernière candidate » y
tombait dès que le bail portait l'ancien nom d'hôte. La VM renommée était
annoncée en 192.168.122.1 au lieu de 192.168.123.170. On préfère désormais le
bail, puis l'agent, puis l'ARP, et les adresses de l'hôte sont écartées.

--- EN ---

More of the same cleanup. Sorting ~/.ssh/config entries also compared the
NAME against domain names: after the VM was renamed, its alias
"erplibre-ubuntu-2404" matched nothing and was deleted, though it led to a
running machine. An entry is kept if its address belongs to a live domain, if
it carries a jump — hence written for a VM virsh will never see — or if its
name is a VM of the Proxmox host. And the screen names what it keeps, as it
does for files.

Same root for the displayed address: "virsh domifaddr --source arp" returns
the bridges' gateways, and falling back to "the last candidate" landed there
as soon as the lease carried the old hostname. The renamed VM was announced
as 192.168.122.1 instead of 192.168.123.170. The lease now wins over the
agent, which wins over ARP, and the host's own addresses are excluded.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
81135f0963 [FIX] analyse: écarter les champs qui n'ont jamais porté de donnée
Le seau « NON déclarés par OpenUpgrade » comptait 565 champs au palier
16 → 17, dont 397 `__last_update` — un champ magique qu'Odoo 17 cesse
d'inscrire et qui n'a jamais eu de colonne. Un chiffre de tête qui fait
peur pour rien fait ignorer le rapport entier.

`store=false` le dit sans ambiguïté, et `id` s'y ajoute : il porte une
donnée, mais celle de la ligne, pas la sienne. Ils vont dans une
catégorie à part, en teinte calme, et NOMMÉE — « __last_update × 101
modèle(s) » explique à lui seul le gros chiffre.

Mesuré sur la chaîne 12 → 18 : le total passe de 1176 à 491, le pire
palier de 565 à 57. Les quatre vrais champs techniques perdus restent
visibles.

--- EN ---

The « NOT declared by OpenUpgrade » bucket held 565 fields at the 16 → 17
step, 397 of them `__last_update` — a magic field Odoo 17 stops
recording, which never had a column. A headline number that frightens
for nothing gets the whole report ignored.

`store=false` says so unambiguously, and `id` joins it: it carries data,
but the row's, not its own. They go to a separate bucket, in a quiet
tint, and NAMED — « __last_update × 101 model(s) » explains the big
number on its own.

Measured on the 12 → 18 chain: the total drops from 1176 to 491, the
worst step from 565 to 57. The four genuinely lost technical fields stay
visible.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
3d8e750af2 [FIX] deploy : la branche du dépôt, et la sortie pendant qu'elle arrive
Trois défauts rapportés sur un déploiement Proxmox. La branche proposée
était « dependabot/pip/aiobotocore-3.1.3 » : « git ls-remote » rend la liste
par ordre alphabétique, et l'écran en prenait la première. Il propose
maintenant la branche du DÉPÔT — celle qu'on a sous les yeux — puis develop,
puis master ; la liste met ces trois-là en tête et relègue les branches de
robot à la fin. Les deux écrans partagent la règle.

La vue de progression n'affichait la sortie qu'à la FIN du travail :
« subprocess.run » ne rend rien avant. Sur Proxmox, une VM demande le
téléchargement de 325 Mio puis l'import du disque — plusieurs minutes de bloc
vide. Elle lit maintenant ligne à ligne. Et « s » y ouvre un ssh sur la VM
créée, par son nom, donc par l'entrée ~/.ssh/config qui porte le rebond.

--- EN ---

Three defects reported on a Proxmox deployment. The branch offered was
"dependabot/pip/aiobotocore-3.1.3": "git ls-remote" returns the list in
alphabetical order and the screen took its first entry. It now offers the
CHECKOUT's branch — the one in front of you — then develop, then master; the
list puts those three first and pushes robot branches to the end. Both
screens share the rule.

The progress view only showed output at the END of a job: "subprocess.run"
returns nothing before that. On Proxmox a VM needs a 325 MiB download then a
disk import — minutes of an empty block. It now reads line by line. And "s"
opens an ssh to the created VM, by name, hence through the ~/.ssh/config
entry that carries the jump.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
e8ab56a0df [FIX] migration: un module fautif n'emporte plus tout le lot
« --uninstall » prend une liste virgulée et Odoo annule la transaction
entière au premier échec : soit tout part, soit rien. Mesuré sur une
chaîne 12 → 18 — crm_phone échoue sur une colonne absente de res_users
et fait tomber les 22 autres avec lui, dont huit modules maison sans
code en 13. Ceux-là sont alors montés d'un palier « installed » sans
rien pour les charger, et ne sont partis que trois paliers plus loin,
par accident.

Le lot part toujours d'abord — un démarrage d'Odoo par nom se paierait
cher pour rien. Mais s'il échoue, on reprend un par un : ce qui peut
partir part, et l'on nomme ce qui résiste.

S'ajoute la liste 12 → 13, qui n'existait pas : sans elle l'étape
« Uninstall module » n'avait rien à faire.

--- EN ---

« --uninstall » takes a comma list and Odoo rolls the whole transaction
back on the first failure: all or nothing. Measured on a 12 → 18 chain —
crm_phone fails on a column missing from res_users and drags the other
22 down with it, including eight in-house modules with no code in 13.
Those rode a step up « installed » with nothing to load them, and only
left three steps later, by accident.

The batch still goes first — one Odoo start per name would cost dearly
for nothing. But if it fails, we retry one by one: what can leave
leaves, and we name what resists.

Plus the 12 → 13 list, which did not exist: without it the « Uninstall
module » step had nothing to do.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
07df33b3ff [ADD] proxmox : suivre une VM distante, changer son état, étendre le menu
Les trois manques restants, demandés. Les colonnes vivantes du suivi — écrit
par seconde, RAM, disque — venaient de virsh, qui ne connaît pas les VM d'un
hôte Proxmox : elles restaient vides, et le relevé d'état les déclarait même
« effacées », ce qui éteignait le reste. L'hôte sait tout cela en un appel
(« pvesh get /cluster/resources »), et le relevé prend la forme de celui de
virsh pour que rien en aval ne distingue la source. Un appel par hôte, mis en
cache cinq secondes : une poignée de main ssh coûte 1 s, virsh 0,03 s.

« Lister les VM » propose maintenant d'en changer l'état, comme QEMU/KVM —
éteindre proprement avant de couper le courant, l'ordre le dit. Et le menu
accepte les commandes ajoutées par todo.json. Enfin « models » manquait aux
traductions : le rapport de migration sortait « 812 models » en français.

--- EN ---

The three remaining gaps, as asked. The dashboard's live columns — written per
second, RAM, disk — came from virsh, which knows nothing of a Proxmox host's
VMs: they stayed empty, and the state probe even declared them "gone", which
switched off the rest. The host knows all of it in one call ("pvesh get
/cluster/resources"), and the reading takes virsh's own shape so nothing
downstream tells the sources apart. One call per host, cached five seconds: an
ssh handshake costs 1 s, virsh 0.03 s.

"List VMs" now offers to change their state, like QEMU/KVM — clean shutdown
before pulling the plug, the order says so. And the menu accepts the commands
todo.json adds. Finally "models" was missing from the translations: the
migration report printed "812 models" in French.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
f2b118a110 [ADD] proxmox : créer le pont manquant depuis l'écran
Sans pont, « qm create » est impossible — et l'écran refusait de déployer
« aucun pont sur l'hôte » sans offrir le moindre moyen d'en avoir un. Une
Proxmox installée SUR Debian n'en a jamais : l'ISO en crée un, pas la
procédure sur Debian.

Deux moments, donc. Avant l'écran, la question se pose dans le terminal, où
l'on peut expliquer les deux voies et montrer ce qui s'exécute. Dans l'écran,
le sélecteur porte « ➕ créer un pont interne vmbr0 (10.10.10.1/24) + NAT » :
la création part dans un fil, l'affichage reste vivant, et le pont créé se
sélectionne tout seul. Elle ne demande rien parce qu'un pont interne ne
touche à aucune interface physique ; un pont sur le LAN déplace l'adresse de
l'hôte et coupe la session, donc il reste manuel.

--- EN ---

With no bridge, "qm create" is impossible — and the screen refused to deploy
"no bridge on the host" without offering any way to get one. A Proxmox
installed ON Debian never has one: the ISO creates it, the Debian procedure
does not.

Two moments, then. Before the screen, the question is asked in the terminal,
where both ways can be explained and the commands shown. In the screen, the
selector carries "➕ create an internal vmbr0 (10.10.10.1/24) + NAT": creation
runs in a thread, the display stays alive, and the new bridge selects itself.
It asks nothing because an internal bridge touches no physical NIC; a bridge
on the LAN moves the host's address and cuts the session, so it stays manual.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
eca61cb2a7 [FIX] db_restore: la sonde du mot de passe maître ne validait rien
Elle interrogeait `db --list`, qui ne LIT jamais le mot de passe :
mesuré, `MASTER_PWD="ceci_est_faux" odoo-bin db --list` sort en 0. La
boucle des dix essais acceptait donc le premier mot saisi, juste ou
faux, et le refus n'arrivait qu'au `--restore` — une fois la base déjà
supprimée. C'était exactement ce que cette boucle devait éviter.

Seule l'action `drop` consulte le secret, et elle le fait AVANT de
regarder la base : `check_super` d'abord, `db_exists` ensuite. Sur un
nom tiré d'un uuid4, elle répond sans rien toucher. Mesuré : mauvais mot
de passe → code 1 et AccessDenied ; bon → code 0 ; huit bases avant,
huit après.

--- EN ---

It probed `db --list`, which never READS the master password: measured,
`MASTER_PWD="ceci_est_faux" odoo-bin db --list` exits 0. The ten-attempt
loop therefore accepted the first password typed, right or wrong, and
the refusal only came at `--restore` — once the database was already
dropped. Precisely what that loop existed to avoid.

Only the `drop` action reads the secret, and it does so BEFORE looking
at the database: `check_super` first, `db_exists` after. On a uuid4 name
it answers without touching anything. Measured: wrong password → exit 1
and AccessDenied; right → exit 0; eight databases before, eight after.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
721e59dba9 [ADD] migration: recréer les réglages qu'aucun événement ne remet
Deux enregistrements ont cessé d'être LIVRÉS en données pour devenir le
produit d'un geste : créer une société, cocher une case, charger un plan
comptable. Une migration n'en fait aucun. Le nettoyage des orphelins les
emporte et rien ne les remet.

`product.list0` est déclaré jusqu'en 16 : 1 liste en 16, 0 en 17, alors
que le groupe compte six membres — tout devis s'ouvre alors sans liste
de prix. Le modèle de rapprochement est déclaré en 12 seulement : 0 dès
la 13, pour trois journaux de trésorerie.

On n'invente rien : `_activate_or_create_pricelists` et `_load_data` du
plan comptable, les méthodes qu'Odoo aurait appelées. Éprouvé sur copie :
0 → 1 liste, 0 → 4 modèles, un devis reprend « Par défaut (CAD) », un
second passage ne double rien.

--- EN ---

Two records stopped being SHIPPED as data and became the product of a
gesture: creating a company, ticking a box, loading a chart of accounts.
A migration does none of them. The orphan cleanup takes them away and
nothing brings them back.

`product.list0` is declared up to 16: 1 pricelist in 16, 0 in 17, while
the group has six members — every quotation then opens without a
pricelist. The reconciliation model is declared in 12 only: 0 from 13 on,
for three cash journals.

Nothing is invented: `_activate_or_create_pricelists` and the chart
template's `_load_data`, the methods Odoo itself would have called.
Proven on a copy: 0 → 1 pricelist, 0 → 4 models, a quotation picks up
« Par défaut (CAD) », a second pass duplicates nothing.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
93d26f50cb [FIX] qemu : ne jamais proposer d'effacer un fichier en usage
Rapporté, et c'est le pire défaut possible ici. Après avoir renommé une VM
en « erplibre-ubuntu-2404-MIGRATION », le nettoyage offrait au « rm -f » son
disque de 63 Go, son seed et son nvram — les trois attachés à une VM EN
MARCHE. L'orphelinat se jugeait sur le NOM du fichier comparé aux noms de
domaines ; un renommage suffisait donc à condamner une VM de production.

L'autorité est désormais libvirt : ce qu'un domaine référence n'est pas
orphelin, quel que soit son nom, et l'écran nomme ce qu'il conserve et pour
qui. Un second contrôle, indépendant, épargne aussi ce qu'un processus tient
ouvert. L'effacement d'une VM demande de même ses fichiers à libvirt : par le
nom, il laissait les 63 Go derrière lui.

--- EN ---

Reported, and it is the worst possible defect here. After renaming a VM to
"erplibre-ubuntu-2404-MIGRATION", cleanup offered its 63 GB disk, its seed
and its nvram to "rm -f" — all three attached to a RUNNING VM. Orphanhood was
judged on the file NAME against domain names; a rename was therefore enough
to condemn a production VM.

Libvirt is now the authority: what a domain references is not an orphan,
whatever its name, and the screen names what it keeps and for whom. A second,
independent check also spares whatever a process holds open. Deleting a VM
likewise asks libvirt for its files: by name, it left the 63 GB behind.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
215b0aac3f [FIX] proxmox : le rebond est le seul chemin vers la VM
Une VM créée sur l'hôte Proxmox vit derrière lui, sur un pont interne : son
adresse n'est pas routable d'ici, et seule l'entrée ~/.ssh/config avec son
ProxyJump y mène. Décocher « ajouter une entrée » tout en demandant une
installation laissait donc le suivi frapper à une porte qui n'existe pas.
L'entrée est maintenant écrite quand une installation ou le suivi la
réclame, et on le dit. Sans rien à faire dans la VM, le choix est respecté.

Trouvé par l'audit du découpage, pas par l'exécution : c'est un cas que
personne n'avait joué.

--- EN ---

A VM created on the Proxmox host lives behind it, on an internal bridge: its
address is not routable from here, and only the ~/.ssh/config entry with its
ProxyJump leads there. Unticking "add an entry" while asking for an install
therefore left the monitor knocking at a door that does not exist. The entry
is now written whenever an install or the dashboard needs it, and we say so.
With nothing to do inside the VM, the choice stands.

Found by the split's audit, not by running it: nobody had played that case.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
ad72f43daa [FIX] test : ne pas dépendre de la langue de l'interface
Quatre tests comparaient des mots français à une sortie traduite : ils
échouaient dès que EL_LANG passait à « en », pour une raison étrangère à ce
qu'ils vérifient. Ils comparent maintenant à t(clé). La suite passe dans les
deux langues, ce qui est la seule façon de le savoir.

Et le transfert mobile s'IGNORE quand le paquet est à moitié construit
(manifeste plein, index absents) au lieu de remonter une erreur : un état
incomplet n'est pas une régression du transfert.

--- EN ---

Four tests compared French words against translated output: they failed as
soon as EL_LANG turned to "en", for a reason foreign to what they check. They
now compare against t(key). The suite passes in both languages, which is the
only way to know it.

And the mobile transfer now SKIPS when the bundle is half-built (manifest
full, indexes missing) instead of raising an error: an incomplete state is
not a regression of the transfer.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
0cfb6f6a00 [FIX] todo : rendre au découpage ses colonnes de télémétrie
« Il manque plein d'informations qu'il y avait avant » : l'écran de
télémétrie construit son arbre en LISANT le code — un fichier, sa première
classe. Depuis que les menus QEMU/KVM et Proxmox vivent dans des mixins,
leurs colonnes avaient disparu de cet écran ; les commandes s'exécutaient
toujours, mais on ne pouvait plus les lancer de là. L'arbre lit maintenant
aussi les mixins, trouvés dans les imports de todo.py — un mixin ajouté
demain apparaîtra sans qu'on y pense.

Le menu Proxmox n'avait pas d'étiquette : son fil d'Ariane s'arrêtait deux
niveaux plus haut, sur « Deploy ». Vérifié : 16 commandes QEMU/KVM et 18
Proxmox dans l'arbre, contre zéro et zéro.

--- EN ---

"A lot of information that used to be there is missing": the telemetry screen
builds its tree by READING the code — one file, its first class. Since the
QEMU/KVM and Proxmox menus moved into mixins, their columns had vanished from
that screen; the commands still ran, but could no longer be launched from
there. The tree now reads the mixins too, found in todo.py's own imports — a
mixin added tomorrow shows up without anyone thinking about it.

The Proxmox menu had no label: its breadcrumb stopped two levels up, at
"Deploy". Verified: 16 QEMU/KVM commands and 18 Proxmox ones in the tree,
against zero and zero.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
1a8f1525b1 [FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.

Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.

--- EN ---

Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.

Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
651cfd4583 [ADD] migration: retirer les index qu'Odoo 17 a créés en double
`make_index_name` rend `{table}__{colonne}_index` depuis la 17 — deux
soulignés. Le nouvel index est créé, l'ancien reste. Mesuré sur une
chaîne 12 → 18 : 1 paire en 12, 3 en 16, 370 en 17, 365 en 18. Inerte à
la lecture, coûteux à l'écriture — deux arbres B entretenus à chaque
INSERT — et c'est le seul défaut de la famille qui empire tout seul.

L'outil s'abstient plus qu'il ne supprime : contrainte, clé primaire,
index partiel ou calculé, méthode ou classe d'opérateurs différentes,
convention non reconnaissable. Sur la base réelle, 367 sûrs sur 376.

Éprouvé sur une copie : 364 retirés, 10 Mo libérés, les 6301 contraintes
identiques au nom près, Odoo charge sans une ligne de journal, et un
« -u all » complet n'en recrée aucun.

--- EN ---

`make_index_name` returns `{table}__{column}_index` since 17 — two
underscores. The new index is created, the old one stays. Measured on a
12 → 18 chain: 1 pair in 12, 3 in 16, 370 in 17, 365 in 18. Inert on
read, costly on write — two B-trees maintained per INSERT — and the only
defect of this family that worsens on its own.

The tool abstains more than it drops: constraint, primary key, partial or
computed index, differing access method or operator class, unrecognisable
convention. On the real database, 367 safe out of 376.

Proven on a copy: 364 dropped, 10 MB freed, the 6301 constraints
identical name for name, Odoo loads without a single log line, and a full
« -u all » recreates none of them.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
32f62fda04 [ADD] migration: prédire la copie de site qui rendra 500 après le palier
`check_cow_views` ne voyait que la rupture qui ARRÊTE la migration : la
forme que l'arch doit avoir change. Deux autres la laissent finir sans un
mot — un ancrage qu'une vue héritière de la cible réclame, un t-call vers
un gabarit que la cible ne livre plus — et ne se voient qu'à l'ouverture
de la page, c'est-à-dire jamais avant la fin.

Rejouée sur la chaîne 12 → 18, la prédiction nomme le défaut de /contact
dès le palier 13 → 14, en 0,8 s. Il avait attendu six paliers.

Ces copies ne sont PAS neutralisées : chacune porte une page écrite par
quelqu'un. Le pilote appelle la réparation après OpenUpgrade, là où la
vue module porte enfin l'arch de la cible.

--- EN ---

`check_cow_views` only saw the break that STOPS the migration: the shape
the arch must have changes. Two others let it finish without a word — an
anchor a target child view requires, a t-call to a template the target no
longer ships — and only show when the page is opened, which is to say
never before the end.

Replayed on the 12 → 18 chain, the prediction names /contact's defect as
early as the 13 → 14 step, in 0.8 s. It had waited six steps.

Those copies are NOT neutralized: each holds a page someone wrote. The
driver calls the repair after OpenUpgrade, where the module view finally
carries the target's arch.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
291fd67841 [ADD] migration: réparer les copies de site qui ne savent plus se rendre
`check_cow_views` cherche la rupture qui ARRÊTE la migration : la forme
que l'arch doit avoir change. Deux autres la laissent finir sans un mot.

Un ancrage manquant : une vue héritière fait un xpath sur `//t[@t-set=…]`
que le module a gagné en chemin ; la copie, page d'utilisateur, n'est
jamais réécrite. Un t-call pendant : le gabarit appelé n'existe plus dans
la cible. Mesuré sur une 12 → 18 réelle : /contact rendait 500 depuis le
palier 14 → 15, quatre paliers de silence, et le test de fumée le voyait
sans savoir le nommer.

Le premier se répare depuis la vue module, en gardant la page telle que
son auteur l'a écrite. Le second se retire, et l'humain décide.

--- EN ---

`check_cow_views` looks for the break that STOPS the migration: the shape
the arch must have changes. Two others let it finish without a word.

A missing anchor: a child view xpaths on `//t[@t-set=…]` that the module
gained along the way; the copy, a user page, is never rewritten. A
dangling t-call: the template called no longer exists in the target.
Measured on a real 12 → 18: /contact returned 500 since the 14 → 15 step,
four steps of silence, and the smoke test saw it without naming it.

The first is repaired from the module view, keeping the page as its
author wrote it. The second is removed, and the human decides.

Assisted-by: Claude Opus 5
2026-08-25 03:19:18 -04:00
677b0e6529 [UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.

Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.

--- EN ---

"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.

What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.

Assisted-by: Claude Opus 5
2026-08-25 03:17:13 -04:00
009ee6e2f6 [FIX] proxmox : distinguer « injoignable » de « pas Proxmox »
Rapporté : « je n'arrive pas à me connecter, pourtant il est accessible ».
La machine répondait bel et bien — c'est Proxmox VE qui n'y était pas. Un
seul message couvrait les deux pannes, et la seule ligne montrée en preuve
était l'avertissement de ssh sur la clé d'hôte, qui envoyait chercher un
problème de réseau inexistant.

On demande donc à ssh s'il passe avant de conclure, et le bruit de la clé
d'hôte ne sort plus comme diagnostic. Machine joignable sans Proxmox : la
commande qui l'installe est affichée telle quelle. Vérifié sur les deux VM
du parc — l'une répond sans pveversion, l'autre ne répond plus.

--- EN ---

Reported: "I cannot connect, yet it is reachable". The machine did answer —
Proxmox VE simply was not on it. One message covered both failures, and the
only line shown as evidence was ssh's host-key warning, which sent the
reader looking for a network problem that did not exist.

So we now ask ssh whether it gets through before concluding, and the
host-key noise no longer comes out as a diagnosis. Reachable without
Proxmox: the command that installs it is printed as is. Checked against both
VMs here — one answers without pveversion, the other no longer answers.

Assisted-by: Claude Opus 5
2026-08-25 03:17:13 -04:00
c0d6b114af [FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.

Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.

--- EN ---

Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.

Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.

Assisted-by: Claude Opus 5
2026-08-25 03:17:13 -04:00
562ad1c873 [ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.

Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.

--- EN ---

The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.

Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.

Assisted-by: Claude Opus 5
2026-08-25 03:17:13 -04:00
9767ad540f [ADD] proxmox : un écran pour déployer sur un hôte distant
Le déploiement Proxmox se faisait par vingt questions, l'une après
l'autre, sans jamais voir le plan entier. Il a maintenant le même écran
que QEMU/KVM — catalogue à gauche, plan à droite, totaux dessous — et une
touche pour revenir aux questions. Il n'en redit rien : 690 lignes contre
1 340, tout le reste vient du socle commun.

Ce que Proxmox a en propre est montré AVANT de lancer : le VMID, parce
que l'hôte ne dit « déjà pris » qu'après avoir téléchargé l'image, et
l'adresse qui s'en déduit sur un pont interne. Le stockage et le pont sont
lus sur l'hôte, jamais devinés. L'icône 🗄 distingue l'entrée du menu de
celle qui déploie ici même.

--- EN ---

Deploying on Proxmox meant twenty questions, one after another, never
seeing the whole plan. It now has the same screen as QEMU/KVM — catalogue
left, plan right, totals below — and a key to fall back to the questions.
It repeats none of it: 690 lines against 1,340, everything else comes
from the shared foundation.

What belongs to Proxmox is shown BEFORE launching: the VMID, because the
host only says "already taken" after downloading the image, and the
address derived from it on an internal bridge. Storage and bridge are read
from the host, never guessed. The 🗄 icon tells the menu entry apart from
the one that deploys right here.

Assisted-by: Claude Opus 5
2026-08-25 03:17:13 -04:00
95e70150c3 [REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.

Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.

--- EN ---

todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.

Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.

Assisted-by: Claude Opus 5
2026-08-25 03:16:07 -04:00
292a03be8d [ADD] run: choisir la base au démarrage, sans jamais bloquer
« ./run.sh » sans argument partait sans base et l'on choisissait dans le
gestionnaire web. Il choisit maintenant : une seule base, il démarre
dessus ; plusieurs, il affiche un menu numéroté.

Ce qui décide n'est pas le menu mais l'endroit où il ne doit PAS s'ouvrir.
systemd écrit « ExecStart=/bin/bash …/run.sh » sans argument, avec
Restart=always : une question posée là serait une boucle de redémarrage.
Le défaut implicite exige donc un terminal sur stdin ET sur stderr — pas
stdout, qui est toujours un tube dans la substitution qui nous lit.
Mesuré : sans terminal, la ligne d'arguments est identique à l'octet et
la sonde, qui coûte 0,8 s, n'est jamais appelée.

--- EN ---

« ./run.sh » with no argument started without a database and one picked
in the web manager. It now picks: a single database, it starts on it;
several, it shows a numbered menu.

What decides is not the menu but where it must NOT open. systemd writes
« ExecStart=/bin/bash …/run.sh » with no argument and Restart=always: a
question asked there would be a restart loop. The implicit default
therefore requires a terminal on stdin AND on stderr — not stdout, which
is always a pipe inside the substitution that reads us. Measured: with no
terminal the argument line is byte-for-byte identical and the 0.8 s probe
is never called.

Assisted-by: Claude Opus 5
2026-08-23 17:26:41 -04:00
01b77dfa80 [ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.

Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.

--- EN ---

A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.

Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.

Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
9cb954749c [FIX] migration: un clone refait rejoue la liste de son palier
web_responsive avait bien été retiré, et il est revenu. Deux endroits
bâtissent la base intermédiaire : l'étape « Uninstall module », et
« Choose delete missing module » qui la jette et la refait depuis la
version précédente. Le second ne rejouait que les modules choisis là.

Relevé sur test_neutralize_upgrade_18 : retrait au rang 218, clone refait
au rang 230, et la 18 a refusé de charger sur l'exclusion de
muk_web_theme. Les deux chemins passent maintenant par
`uninstall_list_for`, donc ils ne peuvent plus diverger ; un test le
tient par l'AST, pour tout chemin à venir.

--- EN ---

web_responsive had indeed been removed, and it came back. Two places
build the intermediate database: the « Uninstall module » step, and
« Choose delete missing module » which throws it away and rebuilds it
from the previous version. The second replayed only the modules chosen
there.

Traced on test_neutralize_upgrade_18: removal at rank 218, clone rebuilt
at rank 230, and 18 refused to load on muk_web_theme's exclusion. Both
paths now go through `uninstall_list_for`, so they cannot diverge again;
a test holds that through the AST, for any future path.

Assisted-by: Claude Opus 5
2026-08-23 04:35:58 -04:00
bcd7f9ae88 [ADD] analyse: i, u et t — installés, en cours, applications
Trois filtres sous la main plutôt qu'un cycle à parcourir. « u » est le
seul qui montrait quelque chose d'invisible jusqu'ici : les états de
passage — to install, to upgrade, to remove — qu'Odoo traverse et ne
devrait pas garder. Mesuré sur test_neutralize_upgrade_13 en pleine
migration : 22 modules figés en « to remove ».

L'en-tête les annonce, le rapport texte les nomme avec leur état, et
« non installés » ne les ramasse plus : un module en « to remove » EST
encore installé, il s'en va. La même touche fait l'aller et le retour.

--- EN ---

Three filters under the hand rather than a cycle to walk. « u » is the
only one showing something invisible until now: the transient states —
to install, to upgrade, to remove — that Odoo passes through and should
not keep. Measured on test_neutralize_upgrade_13 mid-migration:
22 modules frozen in « to remove ».

The header announces them, the text report names them with their state,
and « not installed » no longer collects them: a module in « to remove »
IS still installed, it is on its way out. The same key goes and returns.

Assisted-by: Claude Opus 5
2026-08-23 04:03:18 -04:00
3c10ca2eb2 [FIX] déploiement : suivre une VM même sans installation ERPLibre
Décocher l'installation d'ERPLibre faisait disparaître le tableau de bord. La
case « suivi » vivait DANS le groupe de l'installation, build_spec ne la
recopiait même pas dans la spec, et l'épilogue était gardé par « if install or
desktop » : sans rien à installer, il ne se passait rien.

Le suivi devient un choix du DÉPLOIEMENT. Et sans rien à installer, la
commande distante ne vaut plus « true » — journal vide, ✅ instantané : elle
regarde la VM ARRIVER, attend cloud-init, puis relève système, noyau, adresse,
disque et mémoire. Le journal cesse aussi d'annoncer une installation ERPLibre
qui n'a pas lieu.

--- EN ---

Unchecking the ERPLibre install made the dashboard vanish. The "monitoring"
checkbox lived INSIDE the install group, build_spec did not even copy it into
the spec, and the deploy epilogue was gated by "if install or desktop": with
nothing to install, nothing happened.

Monitoring is now a DEPLOYMENT-level choice. And with nothing to install, the
remote command is no longer "true" — empty log, instant ✅: it watches the VM
ARRIVE, waits for cloud-init, then reports system, kernel, address, disk and
memory. The log also stops announcing an ERPLibre install that never happens.

Assisted-by: Claude Opus 5
2026-08-23 03:50:57 -04:00
7923e37e4f [ADD] analyse: qui dépend de qui, à l'écran
Le menu Analyse disait quels modules manquent, jamais qui dépend de qui.
« Puis-je retirer celui-ci » se réglait donc à la main : au palier
17 → 18, il a fallu écrire la requête pour savoir si web_responsive
pouvait partir.

L'écran liste les modules et « d » parcourt quatre relations : ce dont il
dépend, ce qui en dépend, tout ce qu'il entraîne, tout ce qui tombe avec
lui. « f » filtre, « / » cherche — une base en porte trois mille. La
réponse qui compte est écrite en toutes lettres : deux dépendants
déclarés dont zéro installé, c'est un retrait sans danger.

--- EN ---

The Analyse menu told which modules were missing, never which depends on
which. « Can I remove this one » was therefore answered by hand: at the
17 → 18 step we had to write the query to learn whether web_responsive
could go.

The screen lists the modules and « d » walks four relations: what it
needs, what needs it, everything it pulls in, everything that falls with
it. « f » filters, « / » searches — a database holds three thousand of
them. The answer that matters is spelled out: two declared dependents of
which zero installed means removal is safe.

Assisted-by: Claude Opus 5
2026-08-23 03:46:08 -04:00
f12e79c3a9 [ADD] qemu : déployer Proxmox VE (amd64, arm64)
Proxmox ne publie aucune image cloud : son ISO est un installateur qui formate
le disque. On prend donc la voie que l'amont documente lui-même — Proxmox VE
sur Debian — depuis l'image cloud trixie, partagée avec un déploiement
Debian 13 au lieu d'être téléchargée deux fois.

s390x n'y est pas et n'y sera pas par cette voie : le dépôt n'a aucun index
binary-s390x. arm64 y est, officiel depuis PVE 9. Le catalogue le dit AVANT le
déploiement, au lieu d'échouer au premier apt.

Vérifié sur une VM réelle : pve-manager 9.2.11, noyau 7.0.14-12-pve, quatre
services actifs, interface web en HTTP 200.

--- EN ---

Proxmox publishes no cloud image: its ISO is an installer that formats the
disk. So we take the path upstream documents itself — Proxmox VE on Debian —
from the trixie cloud image, shared with a Debian 13 deployment instead of
being downloaded twice.

s390x is not there and will not be by this route: the repository has no
binary-s390x index. arm64 is, official since PVE 9. The catalog says so BEFORE
the deployment rather than failing at the first apt.

Verified on a real VM: pve-manager 9.2.11, kernel 7.0.14-12-pve, four services
active, web UI answering HTTP 200.

Assisted-by: Claude Opus 5
2026-08-23 03:28:03 -04:00
770de6b0ca [FIX] script: différer les annotations, la 12 tourne en Python 3.7
La migration lance ses outils avec le venv de la version Odoo courante.
Au premier palier c'est celui de la 12, en 3.7, où « dict | None » (3.10)
et « tuple[str, str] » (3.9) sont ÉVALUÉS au chargement du module. La
restauration du zip mourait donc sur un TypeError avant d'avoir rien
fait, dans execute.py — importé par db_restore.py.

`from __future__ import annotations` existe depuis 3.7 et le dépôt s'en
sert déjà dans treize fichiers. Un test le vérifie maintenant sur toute
la fermeture d'imports des outils que le pilote lance, points d'entrée
lus dans le pilote pour que le script ajouté demain soit couvert.

--- EN ---

The migration runs its tools with the venv of the current Odoo version.
At the first step that is 12's, on 3.7, where « dict | None » (3.10) and
« tuple[str, str] » (3.9) are EVALUATED when the module loads. Restoring
the zip therefore died on a TypeError before doing anything at all, in
execute.py — imported by db_restore.py.

`from __future__ import annotations` has existed since 3.7 and the repo
already uses it in thirteen files. A test now checks the whole import
closure of the tools the driver launches, with the entry points read
from the driver so tomorrow's script is covered too.

Assisted-by: Claude Opus 5
2026-08-23 03:13:58 -04:00
e3b1fd48be [FIX] migration: rebâtir le clone annule aussi sa préparation
Quand OpenUpgrade échoue, le pilote remet le drapeau de clonage à zéro
pour que la base intermédiaire soit refaite depuis la version d'avant.
Mais les étapes qui avaient préparé CE clone gardaient le leur : le SQL
de pré-migration, les désinstallations, les installations. La base neuve
repartait sans sa préparation, et OpenUpgrade retombait sur le problème
même que ce SQL existe pour écarter.

Vu sur test_neutralize_upgrade_18 : clone à refaire, et pourtant
fix_migration_odoo170_to_odoo180.sql déjà consigné comme appliqué.
Les trois drapeaux tombent maintenant avec le clone.

--- EN ---

When OpenUpgrade fails, the driver clears the clone flag so the
intermediate database is rebuilt from the previous version. But the
steps that had prepared THAT clone kept theirs: the pre-migration SQL,
the uninstalls, the installs. The fresh database started without its
preparation, and OpenUpgrade met the very problem that SQL exists to
prevent.

Seen on test_neutralize_upgrade_18: clone pending, yet
fix_migration_odoo170_to_odoo180.sql already recorded as applied.
The three flags now fall with the clone.

Assisted-by: Claude Opus 5
2026-08-23 02:20:34 -04:00
24f38c79ad [FIX] migration: un OpenUpgrade raté ne doit pas passer pour fait
`lst_upgrade_odoo` n'est pas une copie : `dct_progression.get()` rend
l'objet stocké. La commande y était inscrite AVANT de tourner, donc le
premier `write_config()` la gravait — y compris celui du chemin d'échec,
qui remet pourtant le drapeau de clonage à zéro pour forcer un nouvel
essai. La reprise sautait alors OpenUpgrade.

Mesuré sur test_neutralize_upgrade_18, arrêté sur l'erreur des thèmes :
base = 17.0.1.3, clone à refaire, et sa commande de migration 18 déjà
consignée. Relancer aurait laissé une base 17 sous le code 18.
On l'inscrit après la réussite, là où le commentaire la situait déjà.

--- EN ---

`lst_upgrade_odoo` is not a copy: `dct_progression.get()` returns the
stored object. The command was recorded BEFORE it ran, so the first
`write_config()` persisted it — including the one on the failure path,
which resets the clone flag precisely to force a fresh attempt. A resume
then skipped OpenUpgrade.

Measured on test_neutralize_upgrade_18, halted on the theme error:
base = 17.0.1.3, clone pending, and its 18 migration command already
recorded. Resuming would have left a 17 database under 18 code.
We record it after success, where the comment already placed it.

Assisted-by: Claude Opus 5
2026-08-23 02:20:30 -04:00
c7ab378bd8 [FIX] migration: retirer web_responsive avant de monter en 18
Monter en 18 mourait sur « MuK Backend Theme et Web Responsive sont
incompatibles ». muk_web_theme n'excluait que web_enterprise en 16 et
en 17 ; la 18 y ajoute web_responsive. Les deux cohabitaient donc
légalement depuis la 12. On retire web_responsive au palier 17 → 18,
pendant que l'état est encore légal ; rien n'en dépend.

Deux défauts trouvés en le câblant. Odoo sort en 0 quand « --uninstall »
ne retire rien : muk_web_theme a traversé quatre paliers en étant réputé
parti. On lit maintenant l'état en base. Et les étapes désinstaller et
installer rangeaient leur drapeau sous la clé de l'étape migration —
à la reprise, OpenUpgrade était sauté pour ces paliers.

--- EN ---

Upgrading to 18 died on « MuK Backend Theme and Web Responsive are
incompatible ». muk_web_theme excluded only web_enterprise in 16 and 17;
18 adds web_responsive. The pair had been legal since 12. We drop
web_responsive at the 17 → 18 step, while the state is still legal;
nothing depends on it.

Wiring it up surfaced two defects. Odoo exits 0 when « --uninstall »
removes nothing: muk_web_theme crossed four steps while believed gone.
We now read the state back from the database. And the uninstall and
install steps stored their flag under the migrate step's key — on
resume, OpenUpgrade was skipped for those steps.

Assisted-by: Claude Opus 5
2026-08-23 02:20:27 -04:00
803f1ea8ce [ADD] migration: decode percent-encoded page anchors before the 13 bump
OpenUpgrade's website post-migration gathers the href of every page
anchor and glues them into a CSS selector:

    "selector": ", ".join([link.attrib["href"] for link in links])

An href is not a selector. A French anchor written #principes-mn%C3%A9-
moniques puts a % in it, which no CSS identifier may hold, and the whole
migration dies on SelectorSyntaxError.

Proven by replaying that exact code on the database: three views failed
before, one of them with the % at position 53 -- the very position in
the log. After the fix, three views processed, none failed. Decoding is
enough: the parser accepts #principes-mnémoniques and refuses the
encoded form.

Only page views, only anchor hrefs, only %XX in hex. A % elsewhere in a
URL is legitimate and stays. The decoder lives in pg_temp and leaves
nothing behind.

--- FR ---

La post-migration website d'OpenUpgrade ramasse les href des ancres
d'une page et les recolle en sélecteur CSS. Or un href n'est pas un
sélecteur : une ancre française encodée y met un %, interdit dans un
identifiant CSS, et la migration meurt.

Prouvé en rejouant ce code exact sur la base : trois vues échouaient,
dont une avec le % en position 53 — celle du journal. Après correctif,
trois vues traitées, zéro échec. Décoder suffit : le parseur accepte
#principes-mnémoniques et refuse la forme encodée.

Seulement les vues de page, seulement les href d'ancre, seulement les
%XX hexadécimaux. Un % ailleurs dans une URL est légitime et reste. Le
décodeur vit dans pg_temp et ne laisse rien derrière lui.

Assisted-by: Claude Opus 5
2026-08-23 02:20:19 -04:00
5da0ebaed3 [FIX] sshfs : n'annoncer un montage que s'il a eu lieu
Après un code 1, le menu affichait « Monté sur … », la commande pour démonter
et celle pour ouvrir le répertoire : le montage n'avait pas eu lieu, et on
cherchait des fichiers dans un répertoire vide. Le succès ne s'affiche plus
qu'en cas de succès, et le point de montage inutilisé est retiré.

L'échec, lui, avait une cause : sshfs lit « a+b » comme un chaînage d'hôtes et
ne consulte jamais ~/.ssh/config pour l'alias entier. Or c'est todo.py qui
nomme les VM « rebond+domaine », et cette seconde moitié est un domaine
libvirt, pas un alias SSH du rebond. On résout donc l'alias par « ssh -G » et
on rend à sshfs une cible qu'il ne peut plus mal lire, ProxyJump comprise.

--- EN ---

After exit code 1, the menu still printed "Mounted on …", the unmount command
and the file-manager command: nothing had been mounted, and one went looking
for files in an empty directory. The success block now only prints on success,
and the unused mount point is removed.

The failure itself had a cause: sshfs reads "a+b" as host chaining and never
consults ~/.ssh/config for the whole alias. But todo.py is what names VMs
"jump+domain", and that second half is a libvirt domain, not an SSH alias on
the jump host. So we resolve the alias with "ssh -G" and hand sshfs a target
it can no longer misread, ProxyJump included.

Assisted-by: Claude Opus 5
2026-08-23 02:20:15 -04:00
6a24687fe0 [ADD] suivi : les statistiques de chaque VM (écriture, RAM, disque)
Le tableau disait la durée et la taille du disque. Il ne disait pas si une VM
TRAVAILLAIT : une installation figée et une qui compile s'y ressemblaient.

Trois chiffres par VM, d'un seul appel « virsh domstats » pour tout le parc
(0,03 s) : ce qu'elle écrit, sa RAM occupée/totale, son disque occupé/total.
Le débit est une moyenne sur DIX secondes — le disque d'une installation
travaille par rafales, et l'instantané n'y montrait que des 0 et des pics.

Le ballon mémoire est réarmé au tour lent : sans période de collecte, libvirt
rend le dernier rapport du pilote, vieux d'une demi-heure. Et cinq caractères
récupérés sur trois colonnes trop larges font tenir la ligne en 150 colonnes.

--- EN ---

The table showed elapsed time and disk size. It did not show whether a VM was
WORKING: a stalled install and a compiling one looked alike.

Three numbers per VM, from a single "virsh domstats" call for the whole fleet
(0.03 s): bytes written, RAM used/total, disk used/total. The write figure is
a TEN-second average — an install's disk works in bursts, and the snapshot
showed only zeros and spikes.

The memory balloon is re-armed on the slow tick: with no collection period,
libvirt hands back the driver's last report, half an hour old. And five
characters reclaimed from three oversized columns keep the row inside 150.

Assisted-by: Claude Opus 5
2026-08-23 02:20:10 -04:00
60df5ac630 [FIX] test: the retry loop pinned the password ON the command line
Running the suite for real turned up seven errors I had just caused. The
fake probe took one argument, and probe_master_password now takes two.

Worse than the signature: two assertions checked that the probed command
CONTAINED "--master_password=bon". They pinned exactly the exposure the
previous commit removed -- a test can hold a defect in place as firmly as
it holds a guarantee.

They now assert the opposite, which is the property worth keeping: the
password reaches the probe beside the command, and the command carries
neither the value nor the option.

--- FR ---

Exécuter la suite pour de vrai a fait apparaître sept erreurs que je
venais de causer. La fausse sonde prenait un argument, et
probe_master_password en prend désormais deux.

Pire que la signature : deux assertions vérifiaient que la commande sondée
CONTENAIT « --master_password=bon ». Elles verrouillaient exactement
l'exposition que le commit précédent a retirée — un test tient un défaut
en place aussi fermement qu'une garantie.

Elles affirment maintenant l'inverse, qui est la propriété à conserver :
le mot de passe parvient à la sonde à CÔTÉ de la commande, et la commande
ne porte ni la valeur ni l'option.

Assisted-by: Claude Opus 5
2026-08-23 02:11:50 -04:00
3f97b0a49c [FIX] security: the KeePass password leaves the command line too
Same exposure as the master password, same fix. kdbx_manager put the Odoo
password straight into the web_login command; /proc/<pid>/cmdline is
readable by every user on the machine, and no downstream filter reaches
that.

The command now carries the NAME of an environment variable, never the
value. One name per entry, because several credentials go out in a single
"parallel" call and a single variable could not tell them apart.
get_extra_command_user therefore returns (fragments, variables), and the
two call sites hand the variables to exec_command_live, which already
merged an environment.

Two things found on the way. web_login re-sent config.default_password_auth
when it retried after dismissing a modal, ignoring whatever the caller had
passed -- the retry silently fell back to "admin". And install_forgejo
printed the admin password back to the terminal, hence into the install log
and any CI capture; its own header already documents the default.

A test pins the guarantee: the fragment must not contain the password.

--- FR ---

Même exposition que pour le mot de passe maître, même correctif.
kdbx_manager mettait le mot de passe Odoo directement dans la commande
web_login ; /proc/<pid>/cmdline est lisible par tout utilisateur de la
machine, et aucun filtre en aval ne l'atteint.

La commande porte désormais le NOM d'une variable d'environnement, jamais
la valeur. Un nom par entrée, car plusieurs identifiants partent dans un
seul appel « parallel » et une variable unique ne saurait les distinguer.
get_extra_command_user rend donc (fragments, variables), et les deux
appelants confient les variables à exec_command_live, qui fusionnait déjà
un environnement.

Deux trouvailles en chemin. web_login renvoyait config.default_password_auth
à la reprise après une modale, ignorant ce que l'appelant avait fourni — la
reprise retombait en silence sur « admin ». Et install_forgejo réaffichait
le mot de passe administrateur, donc dans le journal d'installation et
toute capture de CI ; son propre en-tête documente déjà le défaut.

Un test verrouille la garantie : le fragment ne doit pas porter le secret.

Assisted-by: Claude Opus 5
2026-08-23 02:11:50 -04:00
60d049dfec [FIX] install : compiler pykcs11 avec SWIG 4.3 et au-delà
Toute installation d'Odoo 14, 15 ou 17 échoue depuis que SWIG 4.5 est paru
sur PyPI : « ‘PyInt_FromLong’ was not declared in this scope », 55 fois.
pykcs11 — tiré par endesive — ne livre aucun wrapper pré-généré, et son
« requires = ["swig"] » n'est pas borné : c'est la DERNIÈRE version publiée
qui tourne, pas celle du système. Or SWIG 4.3 a retiré les alias Python 2
qu'il écrivait lui-même, et le typemap CK_RV de pykcs11 en utilise un.

On rend l'alias au préprocesseur, identique token pour token à celui de
SWIG 4.2. CPPFLAGS et non CFLAGS : un .cpp passe par compiler_so_cxx.
Vérifié sur la VM et ici : poetry install rend 0, import PyKCS11 passe.

--- EN ---

Every Odoo 14, 15 and 17 install has been failing since SWIG 4.5 landed on
PyPI: "'PyInt_FromLong' was not declared in this scope", 55 times. pykcs11
— pulled in by endesive — ships no pre-generated wrapper, and its
`requires = ["swig"]` is unbounded: the LATEST published version runs, not
the system one. SWIG 4.3 dropped the Python 2 aliases it used to emit
itself, and pykcs11's CK_RV typemap uses one of them.

We hand the alias back to the preprocessor, token for token identical to
SWIG 4.2's. CPPFLAGS, not CFLAGS: a .cpp goes through compiler_so_cxx.
Verified on the VM and here: poetry install returns 0, import PyKCS11 works.

Assisted-by: Claude Opus 5
2026-08-23 02:11:50 -04:00
5b748e347e [ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.

La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.

Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.

--- EN ---

These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.

The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.

Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.

Assisted-by: Claude Opus 5
2026-08-23 02:11:50 -04:00
0ebdc0c710 [FIX] db_restore: ask the master password again instead of dying on a typo
It was asked once. Wrong, and Odoo raises AccessDenied, check_output
raises CalledProcessError, nothing catches it, and the migration dies on
a traceback. After an hour of version bumps that is a steep price for
one letter. Ten attempts now.

Only a refused PASSWORD is asked again. Any other failure stops and is
shown: asking ten times in front of an unreachable database would hide
the real fault behind a prompt, and one would hunt for a password.
AccessDenied is matched on the class, never on its message, which is
translated.

The attempt is probed with --list, which changes nothing. Validating
here avoids failing half-way, once the database has already been
dropped.

--- FR ---

Il était demandé une fois. Faux, et Odoo lève AccessDenied,
check_output lève CalledProcessError, rien ne l'attrape, la migration
meurt sur une trace. Après une heure de paliers, c'est cher payé pour
une lettre. Dix essais désormais.

Seul un MOT DE PASSE refusé fait reposer la question. Tout autre échec
arrête et s'affiche : dix invites devant une base injoignable
cacheraient la panne, et l'on chercherait un mot de passe. AccessDenied
se reconnaît à la CLASSE, jamais au message, qui est traduit.

L'essai est éprouvé sur --list, qui ne modifie rien. Valider là évite
d'échouer à mi-parcours, une fois la base déjà supprimée.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
6339282668 [ADD] filestore: purge once at the end, tidy at the restore, see the 30 MB
Not between bumps, and the measurement says why: two to eleven fields
vanish at one step and COME BACK at the next -- hr.employee.phone,
account.move.statement_id. "The field is gone" is a transient state
while a migration runs. And there would be nothing to gain: 1881 dead
rows appear at the 13 bump and the count never moves again, so one
final pass takes them all.

The nesting is born once, at the restore, and the clone copies it
identically into every step -- the six databases carried the same 1168
files. It is offered where it is born, never on a closed stdin.

Widened too: the tool was named after missing files and so looked only
at those. 1860 rows in 18 hold a live file for a field that is gone --
31 MB of res.partner.image and thumbnails from before Odoo 13 computed
them. Odoo's collector will never touch them while the row exists.

--- FR ---

Pas entre les paliers, et la mesure dit pourquoi : deux à onze champs
disparaissent à une étape et REVIENNENT à la suivante --
hr.employee.phone, account.move.statement_id. « Le champ n'existe plus »
est transitoire tant que la migration court. Et il n'y aurait rien à y
gagner : 1881 lignes mortes naissent au palier 13 et le compte ne bouge
plus, donc une passe finale les prend toutes.

Le nichage naît une fois, à la restauration, et le clone le recopie
partout — les six bases portaient les mêmes 1168 fichiers. Il se répare
là où il naît, jamais sur un stdin fermé.

Élargi aussi : l'outil portait le nom des fichiers absents et ne
regardait donc qu'eux. 1860 lignes en 18 retiennent un fichier bien
présent pour un champ disparu — 31 Mo d'images res.partner et de
vignettes d'avant qu'Odoo 13 ne les calcule.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
4e0596721c [FIX] filestore: five defects a real repair session brought out
Deduplicating by file was right for COUNTING and wrong for DELETING:
twenty-two rows shared two files, so the purge only ever offered one at
a time and had to be replayed. It now gathers every row whose own field
is dead, and never a live row sharing the same file.

"root" held the root of all filestores instead of this database's
directory, so tidying looked for a nested folder at
<data_dir>/filestore/filestore and answered "nothing to tidy" in front
of 1168 stranded files. It now finds 112 to move up and 1056 duplicates.

limit=0 means "no cap" everywhere else, but the slice [:0] is empty:
"Show every entry" printed "… 3 more" and showed none of them.

The report was captured once, so after a purge it replayed the state
from before -- one then purged rows already gone, believing the work
unfinished. A repair now says whether it did something, and the report
is re-read when it did.

"DELETE 0" was announced as a success. The count now comes from what
PostgreSQL said, and saying nothing is not the same as deleting nothing.

--- FR ---

Dédupliquer par fichier était juste pour COMPTER et faux pour EFFACER :
vingt-deux lignes partageaient deux fichiers, la purge n'en offrait
qu'une à la fois. Elle prend maintenant toutes les lignes dont le champ
est mort, et jamais une ligne vivante partageant le même fichier.

« root » portait la racine de tous les filestores au lieu du dossier de
la base : le rangement cherchait à <data_dir>/filestore/filestore et
répondait « rien à ranger » devant 1168 fichiers échoués. Il en trouve
112 à remonter et 1056 doublons.

limit=0 veut dire « tout » partout ailleurs, mais [:0] est vide : « Tout
afficher » annonçait « … 3 de plus » sans en montrer un seul.

Le rapport n'était lu qu'une fois : après une purge il rejouait l'état
d'avant, et l'on repurgeait des lignes déjà effacées. Une réparation dit
maintenant si elle a fait quelque chose, et le rapport est relu alors.

« DELETE 0 » passait pour un succès. Le compte vient de ce que
PostgreSQL a annoncé, et ne rien dire n'est pas ne rien supprimer.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
ff987eeae0 [UPD] analyse: give every "go further" entry an icon, and guard it
"Show every entry" was the only bare label in menus where every other
line carried one, so it read as an oversight -- and it was. It takes
the 📜 that "Show every table" already uses for the same gesture, and
"List the known packages" gets one too rather than being left as the
last bare line.

The guard reads the labels straight out of the _analyse_follow_up calls
and checks both languages, so the next entry added without an icon
fails here instead of being noticed six months later. A second test
bounds the extraction: were it to stop finding anything, the first
would pass while checking nothing.

--- FR ---

« Tout afficher » était le seul libellé nu dans des menus où toutes les
autres lignes en portaient une : ça se lisait comme un oubli, et c'en
était un. Il prend le 📜 qu'« Afficher toutes les tables » utilise déjà
pour le même geste, et « Lister les packages connus » en reçoit une
plutôt que de rester la dernière ligne nue.

Le garde lit les libellés directement dans les appels à
_analyse_follow_up et vérifie les deux langues : la prochaine entrée
sans icône tombera là, pas dans l'œil de quelqu'un six mois plus tard.
Un second test borne l'extraction — si elle ne trouvait plus rien, le
premier passerait sans rien vérifier.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
1bc2b50ca8 [ADD] filestore: say if the record still exists, and offer the cleanups
A lost image on a deleted task is not a loss -- nobody will ever look
for it. On a LIVING task it is one, and it is the only one worth
regretting. The report now says which: project.task #15 and
calendar.event #1 both still exist, so those two really are gone.

Three states, not two: "could not check" must not read as "it is gone",
or a real loss gets filed as a false alarm.

Two repairs sit in the follow-up menu. Purging rows whose field no
longer exists deletes by ID, never by a rebuilt domain -- replaying the
reasoning in SQL would open the door to deleting more than was shown.
Tidying the nested filestore moves up what is missing and deletes pure
duplicates, never overwriting a file already in place.

--- FR ---

Une image perdue sur une tâche supprimée n'est pas une perte : personne
ne la cherchera. Sur une tâche VIVANTE, c'en est une, et la seule à
regretter. Le rapport le dit : project.task #15 et calendar.event #1
existent encore, ces deux-là sont bien perdues.

Trois états, pas deux : « pas pu vérifier » ne doit pas se lire « a
disparu », sans quoi une vraie perte passe pour une fausse alerte.

Deux réparations dans le menu de suite. La purge efface par IDENTIFIANT,
jamais par un domaine reconstruit -- rejouer le raisonnement en SQL
ouvrirait la porte à effacer plus que ce qui a été montré. Le rangement
remonte ce qui manque et supprime les doublons purs, sans jamais
écraser un fichier déjà en place.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
d6a088e926 [ADD] db_restore: check the filestore landed, and open the tool from the menu
Odoo's shutil.move renames when the destination is absent and NESTS when
it exists, so a leftover filestore/<db>/ sends a whole backup into
filestore/<db>/filestore/, where Odoo never looks. That happened once
here and the clone copied it into all seven databases of the chain --
1168 files, 133 MB each, and nothing said a word.

The check runs after a real restore only. A clone copies its source as
it stands, faults included: checking the mirror would say the same thing
twice, and in the wrong place. It warns and names the fix rather than
aborting -- the database is restored and usable, it is the layout that
is wrong.

--- FR ---

Le shutil.move d'Odoo renomme quand la destination est absente et
IMBRIQUE quand elle existe : un filestore/<base>/ resté là envoie toute
une sauvegarde dans filestore/<base>/filestore/, où Odoo ne regarde
jamais. C'est arrivé une fois ici et le clone l'a recopié dans les sept
bases de la chaîne -- 1168 fichiers, 133 Mo chacune, sans un mot.

Le contrôle ne suit qu'une vraie restauration. Un clone recopie sa
source telle quelle, défauts compris : contrôler le miroir dirait deux
fois la même chose, au mauvais endroit. Il avertit et nomme la
correction plutôt que d'interrompre -- la base est restaurée et
utilisable, c'est la disposition qui cloche.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
96330a7679 [ADD] analyse: say which missing attachment files are truly unrecoverable
"254 attachment files missing" leaves nothing to decide. The useful
split is how many are gone and how many are lying around. Measured on
the migrated 18: three are lost, one waits in a backup zip, and 250
point at a field that no longer exists -- res.country.image is
image_url, computed, since 13, so nothing reads them and there is
nothing to recover.

The tool searches the other databases' filestores, the nested ones Odoo
never reads, and the backup zips' central directory. Only the truly lost
are listed one by one; naming the rest would bury them.

--- FR ---

« 254 fichiers absents » ne laisse rien à décider. Le partage utile est
entre ce qui est perdu et ce qui traîne quelque part. Mesuré sur la 18
migrée : trois sont perdus, un attend dans une sauvegarde, et 250
pointent vers un champ disparu -- res.country.image est image_url,
calculé, depuis la 13 : rien ne les lit, rien à récupérer.

L'outil cherche dans les filestores des autres bases, dans les
filestores imbriqués qu'Odoo ne lit jamais, et dans le répertoire
central des sauvegardes. Seuls les vrais perdus sont listés un à un ;
nommer les autres les enterrerait.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
4b092c04d2 [ADD] analyse: offer to install the suggested modules that are ready
After the report, only the "available" ones are offered — an unknown
module is not in the addons path and an uninstallable one has a broken
dependency, so listing them would buy three failures. How many were
left out is stated, else the count would look like a bug.

This is the only write in the Analyse menu, so its header no longer
claims otherwise. Three guards: the checkout must match the database
version (an Odoo 18 run against a 12 rewrites it before failing), both
questions default to no, and a rejected token is always shown.

--- FR ---

Après le rapport, seuls les « available » sont proposés — un module
inconnu n'est pas dans le chemin des addons, un cassé a une dépendance
morte : les lister achèterait trois échecs. Le nombre d'écartés est dit,
sinon l'écart de comptage passerait pour un bogue.

C'est la seule écriture du menu Analyse, dont l'en-tête ne prétend donc
plus le contraire. Trois garde-fous : le checkout doit être sur la
version de la base (un Odoo 18 lancé sur une 12 la réécrit avant
d'échouer), les deux questions valent non par défaut, et un jeton
refusé est toujours montré.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
50366673c2 [FIX] analyse: make the module checker executable, and guard it
It shipped without the execute bit while carrying a shebang and a main,
so it only ran when prefixed with python3 — and nothing said so before
the attempt. The new test ties the two together in BOTH directions: a
library module marked executable invites a run it cannot serve.

--- FR ---

Livré sans le bit d'exécution alors qu'il porte un shebang et un main :
il ne se lançait qu'en le préfixant de python3, et rien ne le signalait
avant l'essai. Le test neuf lie les deux dans LES DEUX SENS — un module
de bibliothèque marqué exécutable invite à un lancement impossible.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
97d2ebec65 [ADD] analyse: modules a database lacks vs the default package
image_db.py --check_addons_exist asks whether a module is on DISK.
Nothing asked whether a given DATABASE has it. After six migration
steps, that is the question: the 18 instance grown from 12 has only 4
of the 15 modules odoo18.0_base ships.

Five verdicts, not one "missing": an available module installs, an
unknown one needs the addons path repaired first, an uninstallable one
has a broken dependency no install will work around.

shortdesc is varchar up to 15 and jsonb after, and step databases of
both shapes appear in one session — so the column type is read, never
assumed.

--- FR ---

image_db.py --check_addons_exist demande si un module est sur le
DISQUE. Personne ne demandait si une BASE donnée l'a. Après six paliers,
c'est pourtant la question : l'instance 18 issue de la 12 n'a que 4 des
15 modules d'odoo18.0_base.

Cinq verdicts, pas un « manquant » : un module disponible s'installe, un
inconnu exige d'abord de réparer le chemin des addons, un cassé a une
dépendance qu'aucune installation ne contournera.

shortdesc est varchar jusqu'en 15 et jsonb ensuite, et les deux formes
se présentent dans la même session — le type est lu, jamais supposé.

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
a2cedfa64b [ADD] analyse: diff each website copy against the view it shadows
The analysis counted 62 website copies and pointed at other tools to judge
them. But the comparison that matters for a copy needs no registry at all:
both arches are in the database, paired by key — the very pairing Odoo makes.
So it works where the module-source comparison is refused, on a database whose
version differs from the checkout, and on a backup zip.

On the 12.0 database at hand, where the other comparison cannot run: 47 of the
62 copies have a twin, all 47 compared in under a second, 12 differ. The other
35 are byte-for-byte identical to their module view — they carry no
customization at all, which is what decides whether neutralizing costs
anything.

The fields are named as the module comparison names them, so the full-screen
browser and the text report work without knowing which comparison produced the
data. A copy without a twin is left alone: it is a page made in the editor,
with nothing to compare against.

Checked on that database and on a real backup; 8 tests, including that
re-indentation alone is not a difference and that a missing arch yields no
verdict rather than « identical ».

--- FR ---

L'analyse comptait 62 copies de site web et renvoyait vers d'autres outils
pour les juger. Or la comparaison qui compte pour une copie n'a besoin d'aucun
registre : les deux arch sont dans la base, appariées par la clé — exactement
l'appariement que fait Odoo. Elle marche donc là où celle avec la source du
module est refusée : une base dont la version diffère du checkout, et une
sauvegarde zip.

Sur la base 12.0 en cours, où l'autre comparaison ne peut pas tourner : 47 des
62 copies ont une jumelle, les 47 comparées en moins d'une seconde, 12
diffèrent. Les 35 autres sont identiques octet pour octet à leur vue de module
— elles ne portent aucune personnalisation, ce qui décide si les neutraliser
coûte quelque chose.

Les champs portent les noms de la comparaison avec le module, donc l'écran de
navigation et le rapport texte marchent sans savoir laquelle des deux a
produit la donnée. Une copie sans jumelle est laissée telle quelle : c'est une
page faite dans l'éditeur, il n'y a rien à quoi la comparer.

Vérifié sur cette base et sur une vraie sauvegarde ; 8 tests, dont qu'une
ré-indentation seule n'est pas un écart et qu'une arch absente ne rend aucun
verdict plutôt que « identique ».

Assisted-by: Claude Opus 5
2026-08-23 02:09:59 -04:00
c54bfb7974 [ADD] qemu : régler le mode CPU, les écrans et le réseau d'une VM
Le formulaire matériel ne réglait que vCPU, RAM, 3D et démarrage. Trois
manques : le mode CPU, qui décide si on peut virtualiser DANS la VM, les
écrans du virtio-gpu, et le réseau — dont le passage au pont, seul moyen
d'exposer la VM sur le LAN.

La vram n'est pas offerte : domxml-to-native prouve que QEMU ne la reçoit
jamais sur un virtio-gpu, seul max_outputs y arrive.

Vérifié sur un domaine jetable et un pont d'essai : max_outputs=2, -cpu
host, vnet sur le pont, MAC et emplacement PCI conservés. 486 tests.

--- EN ---

The hardware form only set vCPU, RAM, 3D and autostart. Three gaps: the CPU
mode, which decides whether one can virtualize INSIDE the VM, the virtio-GPU
screens, and the network — including the switch to a bridge, the only way to
put the VM on the LAN.

vram is not offered: domxml-to-native proves QEMU never receives it on a
virtio-GPU, only max_outputs gets through.

Verified on a throwaway domain and a test bridge: max_outputs=2, -cpu host,
vnet on the bridge, MAC and PCI slot preserved. 486 tests.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
a55b03e199 [ADD] qemu : prendre le GPU de l'hôte, et régler le matériel des VM
Une VM graphique sans accélération rend tout par le processeur : le bureau,
et l'émulateur Android qui tourne dedans — 32 % d'images en retard, mesuré.
Le déploiement prend donc le GPU de l'hôte dès qu'un nœud de rendu existe.

Une VM déjà installée n'avait aucune voie : ni vCPU, ni RAM, ni 3D. Le menu
d'état les règle maintenant, pendant qu'elle est éteinte — le seul moment où
libvirt les lit.

Trois pièges du terrain : « --memory N » ne touche que le ballon, l'ajout
d'egl-headless n'est pas idempotent, et son retrait sans cible emporte la
console VNC. Vérifié sur un domaine jetable, 459 tests verts.

--- EN ---

A graphical VM without acceleration renders everything on the CPU: the
desktop, and the Android emulator inside it — 32 % janky frames, measured.
The deployment now takes the host GPU as soon as a render node exists.

An installed VM had no path at all: no vCPU, no RAM, no 3D. The state menu
now sets them while the VM is shut off — the only moment libvirt reads them.

Three field traps: "--memory N" only moves the balloon, adding egl-headless
is not idempotent, and removing it untargeted takes the VNC console with it.
Verified on a throwaway domain, 459 tests green.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
a98d6ec9cd [FIX] script todo: régler l'écran de l'émulateur au lancement, densité comprise
Écrire hw.lcd.* dans le config.ini de l'AVD ne servait à rien : l'émulateur
réécrit ce fichier depuis le profil du téléphone au premier démarrage, et l'AVD
repartait en 1080x2400 densité 420 — quatre fois les pixels voulus. Constaté sur
la VM, où le réglage était censé s'appliquer depuis des semaines.

La taille passe donc au lancement, et la DENSITÉ avec elle. C'est ce point qui
surprend, et il est mesuré sur une charge identique : 540x1140 en densité 420 est
PIRE que le plein écran — 81 ms de médiane contre 40, 57 % d'images en retard
contre 37, tout étant rendu énorme. En densité 240 : 38 ms, 32 %, et le 99e
centile tombe de 950 ms à 250.

« -no-snapshot-save » vient avec : ce menu propose de tuer l'émulateur par
pkill, et le lancement suivant mourait alors sur « A snapshot operation is
pending ». Vérifié après un pkill : plus aucun FATAL.

--- EN ---

Writing hw.lcd.* into the AVD's config.ini did nothing: the emulator rewrites
that file from the phone profile on first boot, and the AVD came back at
1080x2400 density 420 — four times the intended pixels. Found on the VM, where
the setting was supposed to have applied for weeks.

The size therefore moves to launch time, and the DENSITY with it. That is the
surprising part, measured on an identical workload: 540x1140 at density 420 is
WORSE than the full screen — 81 ms median against 40, 57 % janky frames against
37, everything rendered huge. At density 240: 38 ms, 32 %, and the 99th
percentile drops from 950 ms to 250.

"-no-snapshot-save" comes along: this menu offers to kill the emulator with
pkill, and the next start then died on "A snapshot operation is pending".
Checked after a pkill: no FATAL left.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
2c181c60d6 [IMP] script todo: RAM utilisée et uptime dans les infos avancées d'une VM
« RAM » disait l'allocation. Elle dit maintenant l'usage, dans la même colonne :
« 4.7G/32G ». Sur un hyperviseur, savoir qu'une VM de 32 Go n'en occupe que 4,7
décide s'il reste de la place pour la suivante — l'allocation seule ne le dit
pas. La formule est « available - usable » de virsh dommemstat, calibrée contre
le « free » de deux VM : 1186 contre 1216 Mo, et 4831 contre 4838. Et la période
de collecte est posée d'abord, sans quoi le ballon ne rafraîchit rien — une VM
qui occupait 4,8 Go en annonçait 490 Mo.

L'uptime vient de l'âge du processus QEMU : libvirt ne l'expose ni dans dominfo,
ni dans domstats, ni par l'agent, mais ce processus est né avec le domaine. Les
colonnes ont été resserrées pour que la ligne tienne en 80 caractères avec le
nom entier de la VM.

--- EN ---

"RAM" showed the allocation. It now shows the use, in the same column:
"4.7G/32G". On a hypervisor, knowing that a 32 GB VM only occupies 4.7 decides
whether there is room for the next one — the allocation alone does not say. The
formula is virsh dommemstat's "available - usable", calibrated against the "free"
of two VMs: 1186 against 1216 MB, and 4831 against 4838. And the collection
period is set first, without which the balloon refreshes nothing — a VM using
4.8 GB reported 490 MB.

Uptime comes from the age of the QEMU process: libvirt exposes it neither in
dominfo, nor domstats, nor through the agent, but that process was born with the
domain. Columns were tightened so the line fits in 80 characters with the VM's
full name.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
24531f85ff [FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.

Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.

--- EN ---

The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.

Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
7452c581db [ADD] script todo: ouvrir l'écran d'une VM avec virt-viewer
C'est la voie la plus courte : virt-viewer parle à libvirt par « qemu+ssh », lit
le port de l'écran par libvirt et monte SON tunnel — aucun « ssh -L » à tenir
ouvert, rien à deviner. Le menu tunnel le propose donc en cinquième choix.

La seule question qui compte est celle de l'affichage, et c'est l'environnement
qui tranche, pas une question de plus. Un affichage présent (poste, ou ssh -X) :
virt-viewer est installé s'il manque — apt, dnf, pacman ou zypper — puis lancé
détaché. Aucun affichage : la commande est donnée pour le poste, et rien n'est
installé sur une machine sans écran.

--- EN ---

The shortest path: virt-viewer talks to libvirt over "qemu+ssh", reads the
display port from libvirt and builds its OWN tunnel — no "ssh -L" to keep open,
nothing to guess. The tunnel menu offers it as a fifth choice.

The only question that matters is the display, and the environment answers it
rather than one more prompt. A display present (workstation, or ssh -X):
virt-viewer gets installed if missing — apt, dnf, pacman or zypper — then
launched detached. No display: the command is handed over for the workstation,
and nothing is installed on a screenless machine.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
75dbeb3258 [ADD] qemu guide: dire comment démarrer le bureau, sur les VM qui en ont un
Une VM graphique peut arriver sur une console texte — graphical.target est
atteinte avant que le paquet du bureau soit là, et « systemctl enable gdm » rend
0 sans rien faire sur Debian et Ubuntu, où l'unité n'a pas de WantedBy. La
commande qui répare tient sur une ligne ; encore faut-il la lire quelque part.

Le guide de connexion porte donc un bloc « Bureau », avec l'état et le
« enable --now ». Il n'apparaît que si la VM a été déployée avec un bureau :
sur un serveur, ces commandes ne mèneraient à aucune unité. Le drapeau existait
déjà côté déploiement, il n'y avait qu'à le passer.

--- EN ---

A graphical VM can land on a text console — graphical.target is reached before
the desktop package exists, and "systemctl enable gdm" returns 0 doing nothing
on Debian and Ubuntu, where the unit has no WantedBy. The command that repairs
it is one line; it still has to be readable somewhere.

The login guide therefore carries a "Desktop" block, with the state and the
"enable --now". It only shows when the VM was deployed with a desktop: on a
server those commands would point at no unit. The flag already existed on the
deploy side; it only had to be passed along.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
49bc354d25 [FIX] script todo: démarrer le bureau, pas seulement l'activer
Une VM graphique restait sur une console texte jusqu'au premier redémarrage.
GNOME installé, gdm3 installé, graphical.target par défaut, lien
display-manager.service posé par le paquet — et rien à l'écran. Deux causes
superposées, mesurées sur erplibre-ubuntu-2604-gnome :

graphical.target était DÉJÀ atteinte quand le paquet est arrivé, et une cible
active ne rattrape pas un service ajouté après coup. Et « systemctl enable gdm »
rend 0 sans rien faire sur Debian et Ubuntu : l'unité n'a pas de « WantedBy »,
seulement l'alias que le paquet pose lui-même.

Le bureau est donc démarré, avec repli sur le service de la saveur, et l'échec
se dit au lieu de se taire. Vérifié : bureau arrêté puis fragment rejoué ->
gnome-shell revient, écran de connexion GDM à l'image.

--- EN ---

A graphical VM stayed on a text console until its first reboot. GNOME
installed, gdm3 installed, graphical.target the default, the
display-manager.service alias in place by the package — and nothing on screen.
Two causes stacked, measured on erplibre-ubuntu-2604-gnome:

graphical.target had ALREADY been reached when the package arrived, and an
active target does not pick up a service added afterwards. And "systemctl enable
gdm" returns 0 doing nothing on Debian and Ubuntu: the unit has no "WantedBy",
only the alias the package installs itself.

The desktop is therefore started, with a fallback to the flavour's service, and
a failure says so instead of staying quiet. Verified: desktop stopped then the
fragment replayed -> gnome-shell back, GDM greeter on screen.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
3ec7eb36f1 [FIX] script forgejo: redémarrer le service quand la configuration change
Tout push finissait sur « Forgejo: Internal Server Error Decoding Failed », et
le message ne désigne pas la cause. Le journal, lui, la donne : 403 sur
/api/internal/hook/pre-receive, refusé par le contrôle du jeton interne. Le
serveur comparait l'INTERNAL_TOKEN qu'il tenait EN MÉMOIRE à celui que le hook
venait de lire sur le disque — deux valeurs différentes — et répondait 403 à son
propre hook, que celui-ci ne sait pas décoder.

La cause est ici : « systemctl enable --now » ne touche pas un service déjà
actif. Le script redémarre donc quand le binaire, la configuration ou l'unité
ont changé, et se tait sinon. Vérifié sur la VM : configuration régénérée
service actif -> redémarrage -> push accepté ; relance sur forge saine ->
aucun redémarrage, push toujours accepté.

--- EN ---

Every push ended on "Forgejo: Internal Server Error Decoding Failed", and the
message does not name the cause. The log does: 403 on
/api/internal/hook/pre-receive, refused by the internal token check. The server
was comparing the INTERNAL_TOKEN it held IN MEMORY with the one the hook had
just read from disk — two different values — and answered 403 to its own hook,
which cannot decode a 403.

The cause is here: "systemctl enable --now" does not touch an already active
service. The script now restarts when the binary, the configuration or the unit
changed, and stays quiet otherwise. Verified on the VM: config regenerated with
the service active -> restart -> push accepted; replay on a healthy forge -> no
restart, push still accepted.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
1785a2019d [FIX] script todo: nommer les outils qu'une VM sans ERPLibre ne peut pas poser
Les outils de la phase « après » vivent DANS le dépôt : la compilation mobile,
l'AVD, et maintenant le script Forgejo. Sur une VM « bureau seul », sans clone,
ils n'existent pas — et ils étaient écartés en silence. Une case cochée passait
donc pour honorée.

La commande le dit désormais, en nommant les outils concernés. Trois lignes qui
évitent de chercher pourquoi la forge demandée n'est nulle part.

--- EN ---

The "after" phase tools live IN the repository: the mobile build, the AVD, and
now the Forgejo script. On a desktop-only VM, with no clone, they do not exist —
and they were dropped in silence. A ticked checkbox therefore passed for
honoured.

The command now says so, naming the tools concerned. Three lines that save
hunting for why the requested forge is nowhere to be found.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
3a82ddbf45 [IMP] script forgejo: deviner l'adresse sans dépendre de net-tools
« hostname -I » est un drapeau de net-tools. L'inetutils d'Arch ne le connaît
pas et peut rendre le NOM de la machine — une ROOT_URL bâtie sur un nom non
résolvable est pire qu'un repli, et l'option est censée marcher sur toutes les
plateformes ERPLibre.

Trois candidats désormais, chacun validé comme adresse IPv4 avant d'être
retenu : hostname -I, puis « ip route get », puis la première adresse globale.
localhost ferme la marche — une forge joignable en local vaut mieux qu'un
script qui s'arrête. Les trois voies sont couvertes par des tests qui
bouchonnent hostname et ip.

--- EN ---

"hostname -I" is a net-tools flag. Arch's inetutils does not know it and may
return the machine NAME — a ROOT_URL built on an unresolvable name is worse than
a fallback, and this option is meant to work on every ERPLibre platform.

Three candidates now, each validated as an IPv4 address before being kept:
hostname -I, then "ip route get", then the first global address. localhost
closes the march — a forge reachable locally beats a script that stops. All
three paths are covered by tests that stub hostname and ip.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
952c92b52a [ADD] script forgejo: installer une forge git en option cochable
Une case au déploiement, et une forge git auto-hébergée répond sur le port 3000,
git par SSH sur 2222. Le travail vit dans un script dédié, appelable seul sur
une machine existante : une seule autorité pour les deux usages.

Le binaire officiel est statique, donc le même fichier sert apt, dnf, pacman et
zypper — c'est ce qui rend l'option portable sans une branche par distribution.
Les architectures suivent l'amont (amd64, arm64, arm-6) ; la case se grise sur
s390x plutôt que de poser un binaire inexécutable. Les quatre secrets sont
écrits par le script : sans oauth2.JWT_SECRET, Forgejo tente de les persister
lui-même et boucle sur un app.ini qu'il n'a pas le droit d'écrire.

Vérifié sur une VM : somme de contrôle validée, service actif, API qui répond,
dépôt créé puis cloné par git, et relance en 1,5 s sans rien réécrire.

--- EN ---

One checkbox at deploy time, and a self-hosted git forge answers on port 3000,
git over SSH on 2222. The work lives in a dedicated script, callable on its own
for an existing machine: one authority for both uses.

The official binary is static, so the same file serves apt, dnf, pacman and
zypper — that is what makes the option portable without a branch per
distribution. Architectures follow upstream (amd64, arm64, arm-6); the checkbox
greys out on s390x rather than dropping a binary that cannot run. The script
writes all four secrets itself: without oauth2.JWT_SECRET, Forgejo tries to
persist them and loops on an app.ini it is not allowed to write.

Verified on a VM: checksum validated, service active, API answering, a repo
created then cloned over git, and a replay in 1.5 s rewriting nothing.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
69431e17c9 [FIX] script todo: configurer le projet PyCharm avec le venv, et réessayer
Deux défauts empêchaient le .idea d'exister. La configuration d'abord :
« make pycharm_configure » lance le script avec le python SYSTÈME, qui n'a pas
xmltodict — « ModuleNotFoundError », mesuré. update_env_version.pycharm_update()
l'appelle depuis .venv.erplibre ; la cible make et l'étape font désormais pareil.

L'ouverture ensuite : la première tentative sur un dépôt neuf peut n'écrire
aucun .idea, son configurateur d'interpréteur plantant sur « homeDir is null »,
là où la suivante l'écrit en 25 s — constaté sur deux VM. L'étape retente donc
une fois, en gardant les deux journaux. Vérifié sur erplibre-ubuntu-2604-gnome,
caches effacés : erplibre.iml, misc.xml, modules.xml, vcs.xml, et 0 processus
survivant.

--- EN ---

Two defects kept .idea from existing. The configuration first: "make
pycharm_configure" runs the script with the SYSTEM python, which lacks
xmltodict — "ModuleNotFoundError", measured. update_env_version.pycharm_update()
calls it from .venv.erplibre; the make target and the step now do the same.

The open next: the first attempt on a fresh repo can write no .idea at all, its
interpreter configurator dying on "homeDir is null", where the next one writes
it in 25 s — seen on two VMs. The step therefore retries once, keeping both
logs. Verified on erplibre-ubuntu-2604-gnome with caches wiped: erplibre.iml,
misc.xml, modules.xml, vcs.xml, and 0 surviving processes.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
c4d8536509 [FIX] script todo: ouvrir PyCharm après l'installation, pas avant
« ⚠ pas de .idea » à chaque installation : PyCharm ouvrait un dépôt cloné mais
pas installé, son configurateur d'interpréteur Python échouait faute de venv, et
il renonçait avant d'écrire quoi que ce soit. Le même appel sur un dépôt
installé écrit erplibre.iml, misc.xml, modules.xml et vcs.xml en cinq minutes —
mesuré sur la VM.

L'ouverture passe donc après le make, et « make pycharm_configure » la suit :
pycharm_update() s'était déjà exécuté pendant l'installation, quand il n'y avait
rien à configurer. Le groupe rend toujours 0 — un bonus ne rougit pas une VM —
et la phase mobile, qui porte le verdict, reste après lui.

--- EN ---

"⚠ no .idea" on every install: PyCharm was opening a repo that was cloned but
not installed, its Python interpreter configurator failed for lack of a venv,
and it gave up before writing anything. The same call on an installed repo
writes erplibre.iml, misc.xml, modules.xml and vcs.xml in five minutes —
measured on the VM.

The open therefore moves after the make, with "make pycharm_configure" behind
it: pycharm_update() had already run during the install, when there was nothing
to configure. The group always returns 0 — a bonus does not redden a VM — and
the mobile phase, which carries the verdict, still comes after it.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
72b19568af [IMP] script todo: ne pas retélécharger un IDE déjà installé
Rejouer une installation est le cas normal — une qui est morte, un outil ajouté
après coup — et le téléchargement en est la partie longue : environ cinq
minutes pour PyCharm, autant pour Android Studio, à chaque fois. Les deux
étapes vérifient donc /opt avant de sortir curl.

Mesuré sur erplibre-ubuntu-2604-gnome, IDE déjà posés : les deux étapes passent
de dix minutes à 0,094 s au total. Le reste rejoue quand même — lanceur, alias,
raccourci de bureau — il est idempotent et bon marché. Un test vérifie que la
garde n'avale pas l'échec du cas où il faut bel et bien télécharger.

--- EN ---

Replaying an install is the normal case — one that died, a tool added later —
and the download is the long part: about five minutes for PyCharm, as much for
Android Studio, every time. Both steps now check /opt before reaching for curl.

Measured on erplibre-ubuntu-2604-gnome with both IDEs already in place: the two
steps drop from ten minutes to 0.094 s total. The rest still replays — launcher,
alias, desktop entry — it is idempotent and cheap. A test checks the guard does
not swallow the failure of the case where downloading is actually needed.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
b4310665fc [ADD] script todo: dire depuis quand un journal d'installation est muet
Le sablier ne distingue pas une installation qui travaille d'une qui est morte :
le marqueur de sortie manque dans les deux cas. Une session ssh emportée, et le
tableau de bord a affiché « ⏳ » pendant 54 minutes sans que rien ne cloche à
l'œil.

La colonne d'état porte maintenant le silence du journal — « ⏳ silence 48min ».
C'est un chiffre, pas un verdict. Le seuil de dix minutes vient d'une mesure :
le téléchargement d'Android Studio tient ~5 min sans une ligne, et l'étape
« APK debug » davantage, son détail partant dans le journal de la VM. Plus bas,
chaque installation deviendrait une alerte, et l'alerte cesserait d'être lue.

--- EN ---

The hourglass does not tell a working install from a dead one: the exit marker
is missing in both cases. An ssh session was reaped, and the dashboard showed
"⏳" for 54 minutes with nothing looking wrong.

The state column now carries the log's silence — "⏳ silent 48min". It is a
figure, not a verdict. The ten-minute threshold comes from a measurement: the
Android Studio download holds ~5 min without a line, and the "debug APK" step
longer, its detail going to the VM's own log. Any lower and every install would
become an alert, and the alert would stop being read.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00