Commit graph

688 commits

Author SHA1 Message Date
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