Commit graph

869 commits

Author SHA1 Message Date
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
92a7634e4f [UPD] imbrication : le plan QEMU vise 24 Go au premier étage
Cible de 6 Go et pas de 2 Go : le couple fait tomber le premier étage d'une
descente à dix sur exactement 24 Go, ce qu'une machine à 36 Go porte.

Descendre plus bas au fond ne servirait à rien. Ce qui compte n'est pas le
plancher mais l'ÉCART entre un parent et son enfant : mesuré à 127 Mo de libre
quand il n'était que d'un gibioctet, un parent qui ne respire pas sert mal la
machine qu'il héberge.

--- EN ---

A 6 GB target and a 2 GB step: the pair makes the first level of a ten-level
descent land on exactly 24 GB, which a 36 GB machine can carry.

Going lower at the bottom would gain nothing. What matters is not the floor
but the GAP between a parent and its child: measured at 127 MB free when it
was only one gibibyte, a parent with no room to breathe serves badly the
machine it hosts.

Assisted-by: claude-opus-5
(cherry picked from commit 01aab4d06b10334df9036f5404c6ed9c4ba3356c)
2026-08-29 01:53:03 -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
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
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
667842b832 [FIX] imbrication : la descente a réfuté ce qu'on croyait mesuré
La doc et l'en-tête de l'algorithme affirmaient qu'au quatrième étage le noyau
invité gelait « au même octet quelles que soient les ressources ». Lancer la
descente l'a réfuté : son propre quatrième étage, à 2 vCPU, a démarré, s'est
installé, et a écrit des gigaoctets. La VM examinée à la main en avait douze.

Ce n'était donc pas un plafond d'imbrication mais un plafond de PARALLÉLISME
sous imbrication — précisément ce que l'algorithme borne, et qui cesse ainsi
d'être une supposition.

Le « même octet », par ailleurs, ne voulait rien dire de ce qu'on lui faisait
dire : 33 682 432 octets, c'est 32 Mio, la taille des fichiers d'amorçage.
Retirer de la mémoire ne le déplaçait pas parce qu'il ne dépendait pas de la
mémoire, pas parce qu'un mur absolu s'y trouvait. La conclusion — ne pas
borner la RAM — reste juste ; sa justification était fausse.

Une affirmation fausse dans la documentation est pire que pas de
documentation : elle décide à la place du lecteur. Les deux passages disent
maintenant ce qui a été mesuré, sur quoi, et ce que la descente a montré
ensuite.

--- EN ---

The documentation and the algorithm's header claimed that at the fourth level
the guest kernel froze "at the same byte whatever the resources". Running the
descent refuted it: its own fourth level, at 2 vCPU, booted, installed, and
wrote gigabytes. The VM examined by hand had twelve.

So it was not a nesting ceiling but a PARALLELISM ceiling under nesting —
exactly what the algorithm caps, which thereby stops being a guess.

The "same byte", moreover, did not mean what it was made to mean: 33,682,432
bytes is 32 MiB, the size of the boot files. Removing memory did not move it
because it did not depend on memory, not because an absolute wall sat there.
The conclusion — do not cap RAM — still holds; its justification was wrong.

A false claim in documentation is worse than no documentation: it decides in
the reader's place. Both passages now say what was measured, on what, and what
the descent showed afterwards.

Assisted-by: Claude Opus 5
(cherry picked from commit b8c53eaf104f6891703e71b540c76ffd5994a2cb)
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
30ffb262b6 [ADD] script addons prod to dev support disable_payment_provider 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
c96f679858 [FIX] analyse: chercher la liste de prix, pas son identifiant externe
Deuxième constat faux du même outil, et la même cause : un indicateur
jamais confronté à la vraie condition.

Le contrôle cherchait l'xmlid `product.list0`. Or la réparation laisse
Odoo créer « Par défaut » SANS le poser. Mesuré sur la migration qui
vient de tourner : une liste de prix bien présente, quatre modèles de
rapprochement recréés, et mon rapport annonçait toujours « absente ». Il
aurait signalé de même la base d'un client ayant créé la sienne à la
main.

On cherche donc une LIGNE dans product_pricelist, protégée par
to_regclass pour le cas où le module n'est pas installé.

--- EN ---

Second false finding from the same tool, and the same cause: a proxy
never checked against the real condition.

It looked for the xmlid `product.list0`. But the repair lets Odoo create
« Par défaut » WITHOUT setting it. Measured on the migration that just
ran: a pricelist plainly there, four reconciliation models recreated —
and my report still said "missing". It would have flagged a customer
database whose pricelist was made by hand just the same.

So we look for a ROW in product_pricelist, guarded by to_regclass in case
the module is not installed.

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
2462798f4e [FIX] analyse: retirer un constat qui faisait peur pour rien
« ir_model_relation nomme une table absente » : 0 avant la migration, 68
après. Le profil idéal — et aucune conséquence.

Son unique consommateur, _module_data_uninstall dans
base/models/ir_model.py, teste sql.table_exists() AVANT de supprimer : la
ligne périmée est ignorée, puis effacée. J'écrivais « la prochaine mise à
jour de module tente de la modifier et échoue » : c'est faux.

Les 68 appartiennent de plus à des modules INSTALLÉS, et database_cleanup
ne touche que les désinstallés — la réparation désignée n'en aurait
réparé aucune.

Un constat sans conséquence et sans geste possible est du bruit, quelle
que soit la netteté du signal. Restent trois constats, tous réels.

--- EN ---

« ir_model_relation names a missing table »: 0 before the migration, 68
after. The ideal profile — and no consequence whatsoever.

Its only consumer, _module_data_uninstall in base/models/ir_model.py,
tests sql.table_exists() BEFORE dropping: the stale row is skipped, then
unlinked. I wrote "the next module update tries to alter it and fails":
that is false.

The 68 also belong to INSTALLED modules, and database_cleanup only
touches uninstalled ones — the repair I named would have fixed none.

A finding with no consequence and no possible action is noise, however
clean the signal. Three findings remain, all real.

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
27a61152cb [FIX] i18n: les deux nouvelles analyses portaient leur nom sans icône
Dans l'écran de choix, « Vues personnalisées » et « Champs x_ » arrivent
avec la leur, héritée du menu Analyse ; « Restant de migration » et
« État de l'instance » sortaient nues. Quatre lignes dont deux commencent
en retrait se lisent comme deux listes.

--- EN ---

In the chooser, « Customised views » and « x_ fields » carry theirs,
inherited from the Analyse menu; « Migration leftovers » and « State of
the instance » came out bare. Four lines where two start indented read as
two lists.

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
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
ccedba3411 [ADD] proxmox : récapituler dans le terminal avant de créer
L'écran montre le plan, mais la création part sur une machine qui n'est pas
la nôtre, et l'hôte va demander sudo juste après. La ligne dit donc où,
quoi et combien — hôte, stockage, pont, puis une ligne par VM avec son VMID
et son adresse — et attend un oui. Le déploiement QEMU/KVM le fait déjà ;
c'était la seule étape qui manquait ici.

--- EN ---

The screen shows the plan, but creation goes to a machine that is not ours,
and the host will ask for sudo right after. So the recap says where, what
and how many — host, storage, bridge, then one line per VM with its VMID
and address — and waits for a yes. The QEMU/KVM deployment already does
this; it was the only step missing here.

Assisted-by: Claude Opus 5
2026-08-25 03:17:13 -04:00
0910e3ea06 [FIX] todo : nettoyer ce que le découpage a laissé derrière
Deux imports de todo.py n'avaient plus d'usager : « grp » est parti avec le
code qui posait les droits d'un groupe, et « getpass » est refait localement
par la seule fonction qui s'en sert. Trois fichiers neufs avaient leurs
imports dans le désordre, que le prochain « make format » aurait reformatés
en salissant un diff sans rapport.

--- EN ---

Two of todo.py's imports had no user left: « grp » went with the code that
set a group's rights, and « getpass » is re-imported locally by the only
function that uses it. Three new files had their imports out of order, which
the next « make format » would have reshuffled, dirtying an unrelated diff.

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
422094ca07 [UPD] format todo 2026-08-23 04:17:34 -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
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
6cdbd528f3 [FIX] security: keep the master password out of the command line
Redacting what we print treats the symptom. The password was still an
argument, and /proc/<pid>/cmdline is readable by EVERY user on the
machine for as long as the command runs -- an exposure no filter reaches.

It now travels in MASTER_PWD. The probe sets it on the child it spawns;
once accepted it is placed in this process's environment, so every later
call inherits it without argv ever carrying it. /proc/<pid>/environ is
readable only by its owner.

Worth knowing for anyone reading the old code: the option was appended to
every invocation, but odoo's db command reads it in the drop branch
alone. list, restore and clone were carrying a secret they never used.

The redaction stays. It is the last line, not the first, and other
options still put secrets on command lines.

--- FR ---

Caviarder ce qu'on affiche traite le symptôme. Le mot de passe restait un
argument, et /proc/<pid>/cmdline est lisible par TOUT utilisateur de la
machine tant que la commande tourne — une exposition qu'aucun filtre
n'atteint.

Il voyage désormais dans MASTER_PWD. La sonde le pose sur l'enfant
qu'elle lance ; une fois accepté, il est placé dans l'environnement de ce
processus, si bien que tous les appels suivants en héritent sans qu'argv
le porte jamais. /proc/<pid>/environ n'est lisible que par son
propriétaire.

À savoir pour qui relit l'ancien code : l'option était ajoutée à chaque
invocation, alors que la commande db d'odoo ne la lit que dans la branche
drop. list, restore et clone portaient un secret dont ils ne faisaient
rien.

Le caviardage reste. Il est le dernier rempart, pas le premier, et
d'autres options mettent encore des secrets sur des lignes de commande.

Assisted-by: Claude Opus 5
2026-08-23 02:11:50 -04:00
22d30a1504 [FIX] security: redact the master password from every output
CodeQL raised seven high-severity alerts on this branch. Three were real,
and the same secret was behind all of them: the Odoo master password,
which db_restore appends to its command line as soon as the database
declares one.

The command itself was printed raw -- "print(arg)" -- so the password
reached stdout, and any terminal capture with it. The probe output was
logged raw too, and a refused attempt echoes the command it tried.

Wider than that: the runner filtered the command it was about to run, but
not what came back. A tool that reprints its own arguments -- "set -x", a
traceback, odoo_bin.sh -- put the secret straight back into the terminal
AND into the log file the sink writes. Every subprocess line now goes
through the same filter as the command.

The four remaining alerts sit on expressions already wrapped in
redact_secrets(). CodeQL does not cross re.sub, so it cannot see the
barrier; the mitigation is real and they are false positives.

--- FR ---

CodeQL a levé sept alertes de sévérité haute sur cette branche. Trois
étaient réelles, et le même secret était derrière : le mot de passe maître
d'Odoo, que db_restore ajoute à sa ligne de commande dès que la base en
exige un.

La commande elle-même était imprimée telle quelle — « print(arg) » — donc
le mot de passe atteignait la sortie standard, et toute capture de
terminal avec elle. La sortie de la sonde était journalisée brute
également, et un essai refusé réaffiche la commande tentée.

Plus large : le lanceur filtrait la commande qu'il allait exécuter, mais
pas ce qui en revenait. Un outil qui réaffiche ses propres arguments —
« set -x », une trace, odoo_bin.sh — remettait le secret dans le terminal
ET dans le fichier de journal. Chaque ligne du sous-processus passe
désormais par le même filtre que la commande.

Les quatre alertes restantes portent sur des expressions déjà entourées de
redact_secrets(). CodeQL ne franchit pas re.sub et ne voit donc pas la
barrière ; la mitigation est réelle, ce sont des faux positifs.

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
deae7d16c7 [ADD] install s390x : batir PROJ quand la distribution est en retard
Debian 13 installe ERPLibre de bout en bout ; Debian 12 s'arrete sur
pyproj :

  ERROR: Minimum supported PROJ version is 9.4.0, installed version is
  9.1.1

bookworm livre 9.1.1, trixie 9.6 — d'ou l'ecart entre les deux. La
portee est etroite : sur amd64 et arm64, pyproj pose une roue manylinux
qui EMBARQUE sa propre PROJ, et la version du systeme n'entre pas en
jeu. s390x n'a pas de roue et compile contre celle du systeme.

lib_proj.sh est le calque de lib_qpdf.sh, meme motif et memes
garde-fous : seuil, comparaison qui complete les composantes manquantes
— « sort -V » classe 9.4 avant 9.4.0 — installation dans /usr/local,
declaration a ld.so, et jamais de code non nul pour ne pas masquer ce
que pyproj dira lui-meme. L'appel reste sous la garde s390x, verifie.

--- EN ---

Debian 13 installs ERPLibre end to end; Debian 12 stops on pyproj:

  ERROR: Minimum supported PROJ version is 9.4.0, installed version is
  9.1.1

bookworm ships 9.1.1, trixie 9.6 — hence the gap between the two. The
scope is narrow: on amd64 and arm64 pyproj lays down a manylinux wheel
that BUNDLES its own PROJ, and the system version never comes into play.
s390x has no wheel and builds against the system one.

lib_proj.sh mirrors lib_qpdf.sh, same pattern and same guards: a
threshold, a comparison that pads missing components — "sort -V" ranks
9.4 before 9.4.0 — installation into /usr/local, an ld.so declaration,
and never a non-zero exit so as not to mask what pyproj itself will say.
The call stays under the s390x guard, verified.

Assisted-by: Claude Opus 5
(cherry picked from commit f779702b61ff6405bdd7efabaa1f5bac09e1166b)
2026-08-23 02:11:50 -04:00
95d71df204 [FIX] systemd : lancer run.sh par bash, contre les echecs 203/EXEC
erplibre.service mourait en 3 ms sur openSUSE s390x, « status=203/EXEC »,
sans jamais entrer dans le script. Ce code ne dit pas que run.sh a
echoue : il dit que systemd n'a pas pu l'EXECUTER.

Quatre causes le produisent — bit x absent, shebang qui ne resout pas,
/home monte noexec, SELinux refusant l'execve (Leap 16 est passe a
SELinux). Les distinguer demande un acces a la machine ; les traiter
ensemble ne le demande pas.

Passe a « /bin/bash run.sh », le fichier n'est plus qu'une donnee lue :
noexec et SELinux ne portent que sur l'execve, et le bit x devient sans
objet. Mesure : un script en 644 refuse en direct, execute par bash.

Les trois generateurs ecrivaient la meme ligne, les trois sont corriges.
Le venv n'y est pour rien — odoo_bin.sh l'active deja, et c'est celui
d'Odoo, pas celui des outils.

--- EN ---

erplibre.service died in 3 ms on openSUSE s390x, "status=203/EXEC",
never entering the script. That code does not say run.sh failed: it says
systemd could not EXECUTE it.

Four causes produce it — missing x bit, unresolvable shebang, /home
mounted noexec, SELinux denying execve (Leap 16 switched to SELinux).
Telling them apart needs access to the machine; handling them together
does not.

Run as "/bin/bash run.sh", the file is merely data being read: noexec
and SELinux only cover execve, and the x bit becomes moot. Measured: a
644 script refused directly, executed fine through bash.

All three generators wrote the same line; all three are fixed. The venv
is not involved — odoo_bin.sh already activates it, and it is Odoo's,
not the tooling one.

Assisted-by: Claude Opus 5
(cherry picked from commit 6f8cee84b72fd016fa188d204f280b011594627c)
2026-08-23 02:11:50 -04:00
2b27ad73c8 [FIX] install s390x : pyproj exige le binaire proj, pas ses en-tetes
openSUSE eclate PROJ en trois paquets — libproj25 la bibliotheque,
proj-devel les en-tetes, proj les outils. Seul proj-devel etait pose, et
il ne tire PAS le troisieme.

pyproj n'a pas de roue s390x : il compile, et sa configuration execute
« proj » pour localiser l'installation. D'ou l'arret sur « proj
executable not found. Please set the PROJ_DIR variable », en plein
milieu d'un poetry install, sans que rien n'ait manque plus tot.

apt nommait deja proj-bin. dnf s'en remettait a une arete transitive :
elle tient sur RHEL, elle manque sur openSUSE. On la nomme donc partout
plutot que d'en dependre.

--- EN ---

openSUSE splits PROJ into three packages — libproj25 the library,
proj-devel the headers, proj the tools. Only proj-devel was installed,
and it does NOT pull the third.

pyproj has no s390x wheel: it builds, and its configuration runs "proj"
to locate the installation. Hence the stop on "proj executable not
found. Please set the PROJ_DIR variable", mid poetry install, with
nothing missing earlier.

apt already named proj-bin. dnf relied on a transitive edge: it holds on
RHEL, it is absent on openSUSE. So we name it everywhere rather than
depend on it.

Assisted-by: Claude Opus 5
(cherry picked from commit c02805a42eebf23380762ff6fc56db0aec1680b5)
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
91a3057b4f [UPD] qemu doc: chiffrer le transfert tel qu'il est maintenant
Les images sont revenues dans les packs, les catalogues gettext en sont sortis :
80 841 fichiers en 233 tranches au lieu de 116 156 en 391, et un APK de 354 Mo
à 2 844 entrées. La doc portait les chiffres d'avant.

Elle dit aussi ce qui n'allait pas de soi : l'APK ne suit pas la charge. Le
texte se compresse, le PNG non — 857 Mo de .po coûtaient 152 Mo d'APK, quand
218 Mo d'images en coûtent 218. D'où les deux leviers, nommés.

--- EN ---

Images came back into the packs and gettext catalogues left: 80,841 files in 233
slices instead of 116,156 in 391, and a 354 MB APK with 2,844 entries. The doc
still carried the earlier figures.

It also states what was not obvious: the APK does not follow the payload. Text
compresses, PNG does not — 857 MB of .po cost 152 MB of APK, where 218 MB of
images cost 218. Hence the two knobs, named.

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
0bd7c2fb1d [I18N] script todo: traduire le tunnel vers la console de l'hyperviseur
Dix lignes de ce chemin s'affichaient en anglais dans une session française,
dont celle qui compte : « then point your VNC client at localhost:5900 ». C'est
l'écran d'une VM qu'on va chercher là, et le mode d'emploi arrivait à moitié
traduit.

Le diagnostic de l'autre branche l'était aussi — la VM sans port VNC, celle
qu'il faut redéfinir. Vérifié sur les deux VM du parc : celle qui expose son
écran et celle qui n'en expose pas.

--- EN ---

Ten lines of that path showed in English in a French session, including the one
that matters: "then point your VNC client at localhost:5900". What is being
fetched there is a VM's screen, and the instructions arrived half translated.

The other branch's diagnostic was in the same state — the VM with no VNC port,
the one that needs redefining. Checked on both VMs of the fleet: the one that
exposes its screen and the one that does not.

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
087ce3effb [FIX] script todo: viser l'IDE par son nom, pas par la ligne de commande
Le filet de fermeture de PyCharm cherchait « /opt/pycharm » dans les lignes de
commande. Or le script d'installation est passé en argument à ssh, et il
contient ce chemin : le pkill a tué la session ssh qui portait une installation
en cours. Elle est morte sans marqueur de sortie, et le tableau de bord a
montré un sablier pendant 54 minutes. Exécuter la suite de tests suffisait à
déclencher le défaut, puisqu'un test rejoue l'étape.

Le filet vise désormais les NOMS de processus, bornés au compte courant.
Mesuré dans la VM : par nom, 3 processus réels et aucun faux ; par ligne de
commande, 4 — le ssh compris. Un test plante un témoin nommé « sleep » dont la
ligne contient le chemin de l'IDE, et les tests bouchonnent pgrep et pkill.

--- EN ---

PyCharm's closing net looked for "/opt/pycharm" in command lines. But the
install script is passed to ssh as an argument, and it contains that path: the
pkill killed the ssh session carrying a running install. It died with no exit
marker, and the dashboard showed an hourglass for 54 minutes. Running the test
suite was enough to trigger it, since one test replays the step.

The net now targets process NAMES, scoped to the current account. Measured in
the VM: by name, 3 real processes and no false ones; by command line, 4 — the
ssh included. A test plants a witness named "sleep" whose command line holds
the IDE path, and the tests stub pgrep and pkill.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
84335e1b47 [ADD] script todo: afficher la RAM de l'hôte dans le suivi d'installation
La barre montrait le CPU et le disque, jamais la mémoire. C'est pourtant la
seule des trois dont l'épuisement ne se voit nulle part ailleurs : une
compilation mobile s'est fait tuer par le noyau sur une VM de 12 Go sans swap
pendant que la barre affichait une charge tranquille et du disque de reste.

Lue dans /proc/meminfo, sans dépendance — ce suivi tourne sur l'hyperviseur,
donc sous Linux, d'où viennent déjà getloadavg et libvirt. « MemAvailable »
plutôt que « MemFree », presque nul dès que le cache travaille. Le swap
n'occupe la barre que s'il existe, et alors même à zéro : une machine qui
commence à échanger explique une lenteur. Au passage, « charge » et « libre »
s'affichaient en français dans une session anglaise.

--- EN ---

The bar showed CPU and disk, never memory. Yet memory is the one of the three
whose exhaustion shows up nowhere else: a mobile build was killed by the kernel
on a 12 GB VM with no swap while the bar displayed a quiet load and disk to
spare.

Read from /proc/meminfo, no dependency — this monitor runs on the hypervisor,
so on Linux, where getloadavg and libvirt already come from. "MemAvailable"
rather than "MemFree", near zero as soon as the cache is working. Swap takes
room in the bar only when it exists, and then even at zero: a machine starting
to swap explains a slowdown. Along the way, "load" and "free" were showing in
French in an English session.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
aa94e460ec [FIX] script todo: ne plus empaqueter les dépôts du manifeste dans l'APK
La compilation butait sur « Too many zip entries 123678 (MAX=65535) » : un APK
est un ZIP, et le dépôt mobile verse 122 684 fichiers d'assets pour 337 qui
sont l'application. Le levier existe et il est documenté chez lui
(doc/SERVICES.md) : ERPLIBRE_MANIFEST_PATH, ici pointé sur un manifeste vide —
« ces dépôts-là : aucun ». Le plugin l'annonce, « 0 repos ».

Mesuré sur erplibre-ubuntu-2604-gnome : dist passe de 123 019 fichiers à 336,
l'APK sort à 59 Mo et 2 472 entrées, et la phase mobile entière rend 0, tests
Vitest compris — 75 fichiers, 1938 tests. Qui veut les dépôts pose la variable
lui-même : elle est respectée. Mesure d'attente, à retirer quand ils tiendront
sous le plafond du ZIP.

--- EN ---

The build hit "Too many zip entries 123678 (MAX=65535)": an APK is a ZIP, and
the mobile repo pours 122,684 asset files in for 337 that are the application.
The lever exists and that repo documents it (doc/SERVICES.md):
ERPLIBRE_MANIFEST_PATH, pointed here at an empty manifest — "those repos:
none". The plugin says so itself, "0 repos".

Measured on erplibre-ubuntu-2604-gnome: dist drops from 123,019 files to 336,
the APK comes out at 59 MB with 2,472 entries, and the whole mobile phase
returns 0, Vitest included — 75 files, 1938 tests. Set the variable yourself
and the repos come back. A stopgap, to drop once they fit under the ZIP
ceiling.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
9e69cfa49c [UPD] qemu doc: énumérer les causes que le diagnostic sait nommer
La liste s'arrêtait aux quatre premières et laissait croire que le reste
tombait dans « aucun motif connu ». Les deux pannes rencontrées cette semaine
y manquaient : un démon Gradle tué par le noyau, et un APK refusé pour ses
122 684 fichiers d'assets alors qu'un ZIP tient 65535 entrées.

La seconde n'a pas de correctif de notre côté, et la doc le dit : elle
appartient au dépôt mobile.

--- EN ---

The list stopped at the first four, implying the rest fell into "no known
pattern". Both failures met this week were missing from it: a Gradle daemon
killed by the kernel, and an APK refused for its 122,684 asset files when a ZIP
holds 65535 entries.

The second one has no fix on our side, and the doc says so: it belongs to the
mobile repository.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
b19713e38f [FIX] script todo: ne plus laisser PyCharm manger la compilation mobile
Sur erplibre-ubuntu-2604-gnome, l'APK s'est fait tuer par le noyau. La cause
est ici : « $! » désigne xvfb-run, un script, et le tuer n'atteint ni PyCharm
ni Xvfb — l'IDE tournait encore 45 minutes après son étape, avec 1,9 Go, quand
Gradle a demandé ses 6,8 Go sur 12. C'est le GROUPE qu'on tue maintenant, et
plus rien ne survit : mesuré, 2 Go rendus.

Le .idea n'était jamais écrit non plus : 122 684 des 123 021 fichiers d'assets
du dépôt mobile épuisaient les 65 536 watches inotify. Relevées à 524288, le
projet se crée. Restent 4 Go de swap avant de compiler, et un diagnostic qui
nomme la mémoire — avec le compte de l'oom-killer — puis la limite ZIP de
65 535 entrées, sur laquelle la compilation bute désormais, en amont.

--- EN ---

On erplibre-ubuntu-2604-gnome the APK was killed by the kernel. The cause is
here: "$!" is xvfb-run, a script, and killing it reaches neither PyCharm nor
Xvfb — the IDE was still running 45 minutes after its step, holding 1.9 GB,
when Gradle asked for its 6.8 GB out of 12. The GROUP is killed now, and
nothing survives it: 2 GB given back, measured.

The .idea was never written either: 122,684 of the mobile repo's 123,021 asset
files exhausted the 65,536 inotify watches. Raised to 524288, the project gets
created. Also 4 GB of swap before building, and a diagnostic naming memory —
with the oom-killer count — then the 65,535-entry ZIP limit the build now hits,
upstream of us.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
6177b1a6a9 [FIX] script todo: résumer les erreurs au lieu de chercher « error »
Le volet « d » et le compteur du tableau de bord cherchaient la sous-chaîne
« error ». Le journal de l'installation qui vient d'échouer — APK tué par le
noyau sur erplibre-ubuntu-2604-gnome — n'en contient AUCUNE : 0 ligne sur
8765, mesuré. Le volet annonçait « aucune erreur détectée » sur une machine
morte, et le tableau de bord 0 erreur.

Comptent désormais les marqueurs qui ne disent jamais « error » : « ⚠ ÉCHEC »,
« FAILURE », une trace Python, un « fatal: » de git, une mort par mémoire. Le
volet ouvre sur un résumé — l'étape en échec et son diagnostic, les signaux
durs, puis les répétitions comptées par forme. Sur ce journal : 1 étape nommée,
2 signaux, là où il n'affichait rien.

--- EN ---

The "d" pane and the dashboard counter looked for the "error" substring. The
log of the install that just failed — APK killed by the kernel on
erplibre-ubuntu-2604-gnome — contains NONE: 0 lines out of 8765, measured. The
pane said "no error detected" about a dead machine, and the dashboard 0 errors.

Markers that never say "error" now count: "⚠ ÉCHEC", "FAILURE", a Python
traceback, a git "fatal:", a death by memory. The pane opens on a summary — the
failed step with its diagnostic, the hard signals, then repeats counted by
shape. On that log: 1 named step and 2 signals, where it showed nothing.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
8f5a439148 [FIX] script todo: ne plus annoncer un émulateur qui n'a pas démarré
Trois défauts vus en conduisant le menu sur de vraies VM. « setsid » détache,
donc son code de retour vaut 0 même quand rien ne se lance : le menu disait
« Démarré » sur une VM sans SDK, dont le journal disait « not found ». Le
démarrage attend maintenant de VOIR le processus, et à défaut cite le journal.

Une sonde préalable lit binaire et AVD d'un coup : une VM déployée sans cocher
l'outil est le cas normal, pas une panne. Vérifié sur deux VM réelles — outillée
(True), migration (« aucun binaire emulator »), sans rien y démarrer.

Et tout ce qui n'était pas « 2 » démarrait l'émulateur : un « n » de travers
suffisait. Au passage, « Choice » n'était traduit dans aucun des trois menus
qui l'affichent.

--- EN ---

Three defects found while driving the menu against real VMs. setsid detaches,
so its exit code is 0 even when nothing launches: the menu said "Started" on a
VM with no SDK, whose log said "not found". Starting now waits to SEE the
process, and quotes the log when it never appears.

A prior probe reads binary and AVD in one go: a VM deployed without ticking the
tool is the normal case, not a failure. Verified on two real VMs — tooled
(True), migration ("no emulator binary") — without starting anything there.

And anything that was not "2" started the emulator; a stray "n" was enough.
Along the way, "Choice" was untranslated in all three menus showing it.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
58f5b9e8da [FIX] qemu doc: donner une commande d'émulateur qui fonctionne
Les deux commandes documentées échouent. « emulator » sans chemin absolu rend
« command not found », parce qu'un `ssh hôte 'commande'` ne lit ni ~/.profile
ni ~/.bashrc — l'erreur a été rencontrée telle quelle. Et le rendu annoncé,
« swiftshader_indirect », n'existe plus : l'émulateur répond « Selected GPU
option is not valid » et le code pose « swangle » depuis un moment.

La doc pointe maintenant d'abord le menu de todo.py, qui démarre l'émulateur
sans fenêtre et donne le tunnel adb : scrcpy reçoit du H.264 encodé par
l'appareil, là où `ssh -X` fait traverser chaque image en pixels bruts.

--- EN ---

Both documented commands fail. Bare `emulator` gives "command not found",
because `ssh host 'command'` reads neither ~/.profile nor ~/.bashrc — the
error was hit exactly like that. And the advertised renderer,
`swiftshader_indirect`, no longer exists: the emulator answers "Selected GPU
option is not valid", and the code has been setting `swangle` for a while.

The doc now points at todo.py's menu first, which starts the emulator without
a window and hands over the adb tunnel: scrcpy receives H.264 encoded by the
device, where `ssh -X` ships every frame as raw pixels.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
91ff4388e7 [FIX] script todo: rendre la console de l'hyperviseur atteignable
Le choix 3 du menu tunnel levait « _qemu_console_tunnel() takes 1 positional
argument but 3 were given » : la fonction lit name et src sans les recevoir.
flake8 le disait déjà (F821 sur name et src), personne ne l'écoutait.

Ce menu n'avait aucun test, et c'est ce qui a laissé passer l'appel. Ses
quatre choix sont donc parcourus pour de vrai, jusqu'à la commande imprimée,
console comprise. Le défaut remis en place fait bien tomber les tests.

--- EN ---

Choice 3 of the tunnel menu raised "_qemu_console_tunnel() takes 1 positional
argument but 3 were given": the function reads name and src without taking
them. flake8 already said so (F821 on name and src); nobody listened.

That menu had no test at all, which is how the call shipped. All four of its
choices are now walked for real, down to the printed command, the console
included. Putting the defect back does make the tests fail.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
13e8b1bd2d [ADD] script todo: lancer l'émulateur Android et son tunnel adb
Le tunnel n'était que du texte à recopier, et le démarrage restait manuel :
un second émulateur sur le même AVD tue le premier (« Running multiple
emulators »), rencontré deux fois. Le menu le détecte donc avant de lancer.

La fenêtre décide du reste. Sans elle, todo.py démarre à distance, détaché
par setsid dans le groupe kvm, et enchaîne sur scrcpy ; avec elle, la
commande doit partir du poste qui possède l'affichage, pas d'ici.

Mesuré : l'émulateur n'écoute que sur le 127.0.0.1 de la VM — l'hyperviseur
est refusé sur IP:5555 — d'où le -J qui met la VM en dernier saut. Vérifié
sur erplibre-mobile-proof : poignée de main adb CNXN rendant
sdk_gphone64_x86_64, enveloppe détachée survivant au ssh ; 30 tests.

--- EN ---

The tunnel was only text to copy, and starting the emulator stayed manual:
a second emulator on the same AVD kills the first ("Running multiple
emulators"), hit twice. The menu now detects it before starting.

The window decides the rest. Without one, todo.py starts it remotely,
detached by setsid in the kvm group, and chains into scrcpy; with one, the
command must run from the workstation that owns the display, not from here.

Measured: the emulator only listens on the VM's 127.0.0.1 — the hypervisor
is refused on IP:5555 — hence the -J putting the VM last. Verified on
erplibre-mobile-proof: an adb CNXN handshake returning sdk_gphone64_x86_64,
a detached wrapper outliving the ssh; 30 tests.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
7a65568098 [ADD] todo qemu: offer the adb tunnel for scrcpy, X11-free
X11 carries raw pixels: 0.62 megapixel per frame even after shrinking the
screen, rendered in software. scrcpy receives an H.264 stream encoded BY the
device, so the emulator runs with no window at all — no X11 anywhere. The
tunnel menu gained a fourth kind, reusing the target list it already reads from
~/.ssh/config, ProxyJump included: the hard part was already solved there.

It forwards 5555, the emulator's own adb port, not 5037: tunnelling the adb
server would force the user to kill the one on their workstation, which holds
the same port. Verified through the tunnel from the hypervisor: an adb CNXN
handshake answers device::ro.product.name=sdk_gphone64_x86, exactly what adb
connect does. ss -ltn in the VM confirms 5554, 5555 and 5037 all listen on
127.0.0.1.

--- FR ---

X11 transporte des pixels bruts : 0,62 mégapixel par image même après réduction
de l'écran, en rendu logiciel. scrcpy reçoit un flux H.264 encodé PAR
l'appareil, si bien que l'émulateur tourne sans aucune fenêtre — plus de X11
nulle part. Le menu de tunnel gagne un quatrième type, qui réutilise la liste de
cibles qu'il lit déjà dans ~/.ssh/config, ProxyJump compris : le plus dur y
était déjà résolu.

Il redirige 5555, le port adb de l'émulateur lui-même, et non 5037 : tunneliser
le serveur adb obligerait à tuer celui du poste, qui occupe le même port.
Vérifié à travers le tunnel depuis l'hyperviseur : une poignée de main adb CNXN
répond device::ro.product.name=sdk_gphone64_x86, ce que fait exactement adb
connect. « ss -ltn » dans la VM confirme que 5554, 5555 et 5037 écoutent tous
sur 127.0.0.1.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
1dcd19b02f [FIX] todo qemu: name a GPU mode the emulator still accepts
Reported from a workstation: "Selected GPU option 'swiftshader_indirect' is
not valid, switching to auto", then "Your GPU drivers may have a bug". That
mode no longer exists in emulator 37.1; it fell back to swangle by itself, so
it worked while printing two errors that read like a fault. emulator -help-gpu
lists exactly four modes: auto, host, swiftshader, swangle. The AVD now asks
for swangle. Checked on the VM: zero GPU errors where there were two,
vulkan:swiftshader gles:swangle selected outright, boot in 55 s.

--- FR ---

Remonté depuis un poste : « Selected GPU option 'swiftshader_indirect' is not
valid, switching to auto », puis « Your GPU drivers may have a bug ». Ce mode
n'existe plus dans l'émulateur 37.1 ; il retombait de lui-même sur swangle,
donc cela fonctionnait en affichant deux erreurs qui se lisent comme une panne.
« emulator -help-gpu » énumère exactement quatre modes : auto, host,
swiftshader, swangle. L'AVD demande désormais swangle. Vérifié sur la VM : zéro
erreur GPU là où il y en avait deux, vulkan:swiftshader gles:swangle choisis
d'emblée, boot en 55 s.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
ef57e1a097 [IMP] todo qemu: shrink the emulator screen so a remote display keeps up
"It launches but it is too slow." The Pixel profile gives 1080x2400 — 2.6
megapixels pushed frame by frame through SSH, in software rendering. The AVD
now declares 540x1140 at 240 dpi: 0.62 megapixel, 4.2 times less. Measured on
the VM after the change: wm size reports 540x1140, and a full-screen capture
drops from 220 KB to 49 KB. Android handles the density and the app does not
notice; whoever wants the real size overrides it with -skin 1080x2400.

The printed command also gained -XC over -X, X11 compression being free on a
remote display, and -no-boot-anim. Checked: ca.erplibre.home relaunched at the
new size, MainActivity resumed.

--- FR ---

« Ça se lance mais c'est trop lent. » Le profil Pixel donne 1080x2400, soit
2,6 mégapixels poussés image par image dans SSH, en rendu logiciel. L'AVD
déclare maintenant 540x1140 à 240 ppp : 0,62 mégapixel, 4,2 fois moins. Mesuré
sur la VM après le changement : « wm size » rend 540x1140, et une capture plein
écran tombe de 220 Ko à 49 Ko. Android gère la densité et l'application ne s'en
aperçoit pas ; qui veut la taille réelle l'écrase par « -skin 1080x2400 ».

La commande affichée gagne aussi « -XC » au lieu de « -X », la compression X11
étant gratuite sur un écran distant, et « -no-boot-anim ». Vérifié :
ca.erplibre.home relancé à la nouvelle taille, MainActivity au premier plan.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
07b4c81207 [FIX] todo qemu: find the APK where AGP writes it, and install it as it is
The app runs in the emulator now, and two of my own checks were wrong. With an
injected ABI, AGP writes to intermediates/apk/debug, not outputs/apk/debug: a
SUCCESSFUL build was reported "no APK produced" because only the second path
was searched. That same mode marks the APK testOnly, so adb refuses it without
-t — INSTALL_FAILED_TEST_ONLY. Both measured on the emulator, where
ca.erplibre.home then installed, launched, and ran its SQLite migrations.

The manifest also stops tracking sentencepiece at master, which made the build
non-reproducible and, since August, unbuildable: master fetches protobuf and
compiles protoc for the Android target before running it on the host. v0.2.1
ships its .pb.cc pre-generated. Its own absl subset still lacks
flags/flag.cc, so that library remains to be sorted out upstream.

--- FR ---

L'application tourne dans l'émulateur, et deux de mes propres contrôles étaient
faux. Avec une ABI injectée, AGP écrit dans intermediates/apk/debug et non dans
outputs/apk/debug : une compilation RÉUSSIE était rapportée « aucun APK
produit » parce que je ne regardais que le second chemin. Ce même mode marque
l'APK testOnly, et adb le refuse sans -t — INSTALL_FAILED_TEST_ONLY. Les deux
mesurés sur l'émulateur, où ca.erplibre.home s'est ensuite installé, lancé, et
a joué ses migrations SQLite.

Le manifeste cesse aussi de suivre sentencepiece à master, ce qui rendait la
compilation non reproductible et, depuis août, impossible : master récupère
protobuf et compile protoc pour la cible Android avant de l'exécuter sur
l'hôte. La v0.2.1 livre ses .pb.cc pré-générés. Son propre sous-ensemble absl
manque encore flags/flag.cc : cette bibliothèque reste à régler en amont.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
9ad2048bbb [FIX] todo qemu: the printed emulator command could not run as printed
Reported from a workstation: "emulator: command not found". The line the
emulator step printed relied on PATH, and "ssh host 'command'" reads neither
~/.profile nor ~/.bashrc — Ubuntu even opens the latter with a return for
non-interactive shells. So what install-android.sh writes there never applies
to those commands. Both printed lines now carry absolute paths.

Behind it, a second one: the emulator ships TWO qemu binaries and only the
headless one does without PulseAudio. The windowed one — the ssh -X case —
links libpulse.so.0, absent from cloud images, and stops on "cannot open
shared object file" even with -no-audio. Measured: it is the only missing
library, the Qt dependencies travel in the bundle. Checked after the fix: the
windowed binary starts and reports its version.

--- FR ---

Remonté depuis un poste : « emulator: command not found ». La ligne affichée
par l'étape émulateur comptait sur le PATH, or « ssh hôte 'commande' » ne lit
ni ~/.profile ni ~/.bashrc — Ubuntu ouvre même le second par un return pour
les shells non interactifs. Ce que install-android.sh y écrit ne s'applique
donc jamais à ces commandes. Les deux lignes affichées portent maintenant des
chemins absolus.

Derrière, une seconde : l'émulateur livre DEUX binaires qemu et seul le
headless se passe de PulseAudio. Celui qui ouvre une fenêtre — le cas du
ssh -X — lie libpulse.so.0, absente des images cloud, et s'arrête sur
« cannot open shared object file » même avec -no-audio. Mesuré : c'est la
seule bibliothèque qui manque, les dépendances Qt voyagent dans le bundle.
Vérifié après correction : le binaire fenêtré démarre et donne sa version.

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