Commit graph

183 commits

Author SHA1 Message Date
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
f3be40d74d [ADD] todo qemu: add an Android emulator, and fix what the real run exposed
Running it on a VM was the only way to find these. install_os never installed
python3-venv, so .venv.erplibre was born crippled — bin/python but no pip, no
activate — and everything downstream failed on "No module named git"; one line
of dependency fixes it. Capacitor 8 needs a JDK 21 where the mobile
repository's installer puts 17, and Gradle must RUN on it. That installer is
not idempotent either, so it is replayed only when something is missing.

The new emulator option creates an AVD from the SDK's own device list — the
newest plain Pixel, smallest screen — with software rendering written into its
config so ssh -X does not open a black screen, and adds the user to the kvm
group, without which it refuses to start. Checked on a VM: boot completed in
10 s, adb sees emulator-5554, Android 16 x86_64, no KVM refusal. Vitest: 1938
tests pass. The APK still fails, upstream: sentencepiece builds protoc for the
target then runs it on the host.

--- FR ---

Seule l'exécution sur une VM pouvait trouver ceci. install_os n'installait pas
python3-venv, si bien que .venv.erplibre naissait infirme — bin/python mais ni
pip ni activate — et tout ce qui en dépend tombait sur « No module named
git » ; une ligne de dépendance suffit. Capacitor 8 réclame un JDK 21 là où
l'installateur du dépôt mobile pose un 17, et Gradle doit TOURNER dessus. Cet
installateur n'est pas idempotent non plus : il n'est rejoué que s'il manque
quelque chose.

La nouvelle option crée un AVD depuis la liste de profils du SDK — le Pixel
simple le plus récent, plus petit écran —, écrit le rendu logiciel dans sa
configuration pour qu'ssh -X n'ouvre pas un écran noir, et ajoute
l'utilisateur au groupe kvm, sans quoi il refuse de démarrer. Vérifié sur une
VM : boot en 10 s, adb voit emulator-5554, Android 16 x86_64, aucun refus de
KVM. Vitest : 1938 tests passent. L'APK échoue encore, en amont :
sentencepiece bâtit protoc pour la cible puis l'exécute sur l'hôte.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
1da54d0ba5 [IMP] todo qemu: let Android Studio find the SDK the build already installed
install-android.sh writes its exports into ~/.bashrc, which a GNOME session
never reads: Android Studio launched from the menu saw no ANDROID_HOME and
offered to download a second SDK, next to the one the mobile build had just
installed. environment.d is the channel the user session actually reads, so
both point at $HOME/android. An empty ~/.android/repositories.cfg is created
too — without it the first-run wizard stops on an error instead of offering
anything.

--- FR ---

install-android.sh écrit ses exports dans ~/.bashrc, qu'une session GNOME ne
lit jamais : Android Studio lancé depuis le menu ne voyait aucun ANDROID_HOME
et proposait de télécharger un second SDK, à côté de celui que la compilation
mobile venait de poser. environment.d est le canal que la session utilisateur
lit vraiment, et les deux pointent désormais $HOME/android. Un
~/.android/repositories.cfg vide est créé au passage : sans lui, l'assistant
de première ouverture s'arrête sur une erreur au lieu de proposer quoi que ce
soit.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
5150cfde9c [ADD] todo qemu: build and test the mobile app, and fail the VM if it breaks
A VM was declared ready without anything proving it. The mobile option now
adds the repository to the manifest (additive, so it rides along an Odoo 18
install), runs the mobile repository's own install-android.sh — licences
accepted included — then npm ci, vite build, cap sync, gradlew assembleDebug
and npm test. A failure fails the VM: the exit code reaches the dashboard.

Two gaps in that upstream installer had to be filled: unzip and wget, which
no cloud image ships, and the SDK platform — it installs android-34 while
variables.gradle asks for compileSdk 36, so the number is read from the file
rather than frozen. Gradle writes tens of megabytes and hundreds of harmless
lines carrying the word error: that output goes to a file of its own, and the
log gets the named cause instead.

--- FR ---

Une VM était déclarée prête sans que rien ne le prouve. L'option mobile ajoute
maintenant le dépôt au manifeste (additif, donc il accompagne une installation
Odoo 18), lance l'install-android.sh du dépôt mobile lui-même — licences
acceptées comprises —, puis npm ci, vite build, cap sync, gradlew
assembleDebug et npm test. Un échec fait échouer la VM : le code de sortie
remonte au tableau de bord.

Deux manques de cet installateur amont ont dû être comblés : unzip et wget,
qu'aucune image cloud ne livre, et la plateforme SDK — il pose android-34
quand variables.gradle réclame compileSdk 36, d'où le chiffre lu dans le
fichier plutôt que figé. Gradle écrit des dizaines de mégaoctets et des
centaines de lignes anodines portant le mot error : cette sortie part dans un
fichier à part, et le journal reçoit la cause nommée.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
6efe16a991 [FIX] todo qemu: open the PyCharm project without a screen, and unlicensed
Two measurements, one after the other. The unified PyCharm that
code=PCC&latest now serves stops on its licence: the log says
NoValidIdeLicense then "Get licenses: request requires authentication", and
no project is ever opened — so no .idea, so nothing for the install to
configure. The Community line asks for no account and is still patched
(2025.2.6.2 on 2026-07-29); it is resolved from the release feed, so no
version is frozen here.

That build then froze 1.3 s after startup: the trust dialog, invisible under
Xvfb and waiting for a click. idea.trust.all.projects unblocks it. Checked
on an Ubuntu 26.04 VM: .idea complete in 195 s, and pycharm_configuration.py
writes its exclusions into erplibre.iml.

--- FR ---

Deux mesures, l'une après l'autre. Le PyCharm unifié que sert désormais
code=PCC&latest s'arrête sur sa licence : le journal dit NoValidIdeLicense
puis « Get licenses: request requires authentication », et aucun projet ne
s'ouvre — donc pas de .idea, donc rien à configurer pour l'installation. La
ligne Community ne demande aucun compte et reste corrigée (2025.2.6.2 le
2026-07-29) ; elle est résolue depuis le flux des versions, sans qu'aucun
numéro ne soit figé ici.

Ce build se figeait ensuite 1,3 s après le démarrage : la fenêtre de
confiance, invisible sous Xvfb et attendant un clic.
idea.trust.all.projects la lève. Vérifié sur une VM Ubuntu 26.04 : .idea
complet en 195 s, et pycharm_configuration.py écrit ses exclusions dans
erplibre.iml.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
c2852089a0 [ADD] todo qemu: offer PyCharm, Android Studio and GNOME extensions
A graphical VM was a desktop and nothing else: every developer tool had to
be installed by hand afterwards. A check list now carries them, filtered
per machine — Android Studio is x86_64 only, Google publishes no Linux
aarch64 build — and their disk cost reaches the plan before any qcow2 is
created. They are installed BEFORE the clone: PyCharm writes the .idea/ of
the repository, and the install that follows is what runs
pycharm_configuration.py, through update_env_version.pycharm_update().

Its launcher is named studio, which is enough to conclude the install
failed; it now answers to android-studio too. GNOME extensions come from
the site by UUID, per running Shell: the same endpoint serves gTile v59
for GNOME 46 and v62 for 48. Checked with stubs: every tool can fail and
the install's exit code still wins, 45 tests.

--- FR ---

Une VM graphique n'était qu'un bureau : chaque outil de développement
restait à poser à la main. Une liste à cocher les porte, filtrés machine
par machine — Android Studio n'existe qu'en x86_64, Google ne publiant
aucune archive Linux aarch64 — et leur place disque atteint le plan avant
qu'un seul qcow2 ne soit créé. Ils sont posés AVANT le clone : PyCharm
écrit le .idea/ du dépôt, et c'est l'installation qui suit qui lance
pycharm_configuration.py, via update_env_version.pycharm_update().

Son lanceur s'appelle studio, ce qui suffit à conclure à un échec ; il
répond désormais aussi à android-studio. Les extensions GNOME viennent du
site par UUID, selon le Shell qui tourne : le même point d'entrée sert
gTile v59 pour GNOME 46 et v62 pour 48. Vérifié avec des leurres : chaque
outil peut échouer sans que le code de sortie de l'installation ne change,
45 tests.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
4df994f721 [FIX] todo qemu : lire les distros par arch dans deploy_qemu
L'ecran de deploiement portait sa PROPRE copie de S390X_DISTROS, sous un
commentaire promettant qu'elle restait « coherente avec deploy_qemu.py ».
Une copie ne tient aucune promesse : Debian a gagne s390x la-bas et
l'ecran ne le proposait toujours pas.

_qemu_arch_distros lit desormais ARCH_DISTRO_SUPPORT, et _qemu_arches_for
y passe aussi pour que « toutes les architectures » n'offre rien que
deploy_qemu refuserait ensuite. Les tuples locaux restent en repli si
l'import echoue. L'import est memorise : le catalogue l'interrogeait une
fois par couple (distro, version).

Plancher memoire de 2048 Mio sur la voie installateur, annonce et jamais
abaissant : d-i deplie un systeme de fichiers en RAM la ou une image
cloud arrive installee, et le manque s'y voit comme un ecran fige.

--- EN ---

The deploy screen carried its OWN copy of S390X_DISTROS, under a comment
promising it stayed "consistent with deploy_qemu.py". A copy keeps no
promise: Debian gained s390x over there and the screen still did not
offer it.

_qemu_arch_distros now reads ARCH_DISTRO_SUPPORT, and _qemu_arches_for
goes through it too, so "all architectures" offers nothing deploy_qemu
would later refuse. The local tuples remain as a fallback if the import
fails. The import is memoised: the catalogue queried it once per
(distro, version) pair.

A 2048 MiB memory floor on the installer path, announced and never
lowering: d-i unpacks a filesystem into RAM where a cloud image arrives
installed, and running short shows up as a frozen screen.

Assisted-by: Claude Opus 5
(cherry picked from commit f0ee70c1ac43ba2bc9f5fb78fe71f821a15daf7d)
2026-08-23 02:07:42 -04:00
0da6c7fcbe [FIX] todo qemu : la console vise l'hyperviseur, pas la VM
Le choix [3] listait les domaines libvirt de la machine qui execute
todo.py. Or on pilote un parc imbrique depuis l'exterieur : cette
machine ne connait pas les VM d'un orchestrateur, et la bonne cible
n'apparaissait donc jamais dans la liste.

L'ecran VNC appartient a QEMU, donc a l'hyperviseur. Tunneler vers
l'invite ne trouve rien : le socket n'existe pas de ce cote. La cible
est le ProxyJump declare pour l'hote, lu par « ssh -G » — la seule
lecture qui couvre Match, wildcards et Include. Le nom compose
« saut+vm » n'est qu'un libelle.

Le port est lu sur cet hyperviseur, sans sudo d'abord : le groupe
libvirt suffit souvent, et « sudo -n » distant echoue faute de TTY.

--- EN ---

Choice [3] listed the libvirt domains of the machine running todo.py.
But a nested fleet is driven from outside: that machine knows nothing
of an orchestrator's VMs, so the right target never showed up.

The VNC screen belongs to QEMU, hence to the hypervisor. Tunnelling to
the guest finds nothing: the socket does not exist on that side. The
target is the ProxyJump declared for the host, read via "ssh -G" — the
only reading that covers Match, wildcards and Include. The composite
"jump+vm" name is just a label.

The port is read on that hypervisor, without sudo first: libvirt group
membership often suffices, and remote "sudo -n" fails for lack of a TTY.

Assisted-by: Claude Opus 5
(cherry picked from commit 2fb846d502cc8026732b58e1f487d7a6726f17eb)
2026-08-23 02:07:42 -04:00
437ac4b457 [FIX] smoke: name the URL that actually failed, keep the previous log
The report named the sitemap URL, never the one the redirect chain
ended on. On this site every page goes through two or three hops --
measured, 146 for 55 pages, between the language prefix and the
canonical slug -- so a 500 at the end was reported against a page that
answers perfectly well. One goes and checks it, finds it healthy, and
concludes the tool is wrong.

urllib carries the answer: HTTPError.url is the URL that produced the
error, not the one requested. fetch now returns it and the report shows
it when it differs.

A timeout was never reported as 500 -- fetch returns 0 and the report
writes "no answer" -- but the evidence for a real 500 did evaporate: the
Odoo log was opened with "w", so replaying the test erased the trace of
the failure one had just seen. One generation is kept now.

--- FR ---

Le rapport nommait l'URL du sitemap, jamais celle où la chaîne de
redirections aboutit. Sur ce site chaque page en traverse deux ou trois
— mesuré, 146 pour 55 pages — donc un 500 au bout était imputé à une
page qui répond très bien. On va la vérifier, on la trouve saine, et
l'on conclut que l'outil se trompe.

urllib porte la réponse : HTTPError.url est l'URL qui a produit
l'erreur. `fetch` la rend, et le rapport l'affiche quand elle diffère.

Un dépassement de délai n'a jamais été rendu comme un 500 — `fetch`
rend 0, écrit « aucune réponse » — mais la preuve d'un vrai 500
s'évaporait : le journal Odoo était ouvert en « w », donc rejouer le
test effaçait la trace qu'on venait de voir. Une génération est gardée.

Assisted-by: Claude Opus 5
2026-08-23 02:07:42 -04:00
7e30377e56 [ADD] migration: convert the <tree> tags Odoo 18 renamed to <list>
OpenUpgrade converts the COLUMN and never the ARCH:

    def _fix_list_view_type(cr):
        cr.execute("UPDATE ir_ui_view SET type='list' WHERE type='tree'")

so the load dies on "the root node of a list view should be <list>".
A module view recovers at its next update, which rewrites the arch from
XML. A view with no xmlid -- a hand-made list, a website copy -- is
rewritten by nothing and stays broken forever. The report says which is
which, because the two call for different work.

Every occurrence, not just the root: a <tree> nested in a form is a
one2many list and is refused just the same. Odoo 18's own addons hold
none, so nothing legitimate is at risk. Gated on version 18: before it,
the tag is correct and "fixing" it would break healthy views.

Four tests run the SQL against a real PostgreSQL. A text assertion
cannot see what a query does -- it took a live run to prove every
language of the jsonb arch is converted, not just en_US.

--- FR ---

OpenUpgrade convertit la COLONNE et jamais l'ARCH, d'où l'échec sur « le
nœud racine d'une vue list devrait être <list> ». Une vue de module s'en
remet à sa prochaine mise à jour ; une vue sans xmlid — liste faite
main, copie de site — n'est réécrite par rien. Le rapport distingue les
deux, car le travail diffère.

Toutes les occurrences, pas seulement la racine : un <tree> imbriqué
dans un formulaire est une liste one2many, refusée pareillement. Les
addons d'Odoo 18 n'en portent aucun. Bridé à la 18 : avant, la balise
est juste.

Quatre tests exécutent le SQL contre un vrai PostgreSQL. Une assertion
sur du texte ne voit pas ce qu'une requête fait — il a fallu l'exécuter
pour prouver que toutes les langues du jsonb sont converties.

Assisted-by: Claude Opus 5
2026-08-22 07:24:00 -04:00
300238fe83 [ADD] migration: offer the theme uninstall where the theme is blamed
The question was asked once, at step one, and a flag kept it from ever
returning. But a theme only becomes incompatible at a given bump, hours
later -- asking at the start could not cover it. Entry [6] appears when
TWO conditions hold: a theme is installed, and the recent output names
it. One condition alone would offer to wreck the site's design over an
error that has nothing to do with it.

Also, the 17 to 18 rename OpenUpgrade declares and never applies:
  forum.post / tag_ids : column1 is now 'forum_post_id' ('forum_id')
website_forum/18.0.1.2/ holds only an analysis, no script, so the load
dies on a foreign key to a column that does not exist and leaves the
database half migrated.

--- FR ---

La question était posée une fois, à l'étape 1, et un drapeau
l'empêchait de revenir. Or un thème ne devient incompatible qu'à un
palier donné, des heures plus tard : la poser au départ ne pouvait pas
suffire. L'entrée [6] paraît quand DEUX conditions tiennent — un thème
installé, et la sortie récente qui le nomme. Une seule offrirait de
casser le design du site pour une panne étrangère.

Et le renommage 17 → 18 qu'OpenUpgrade déclare sans jamais l'appliquer :
  forum.post / tag_ids : column1 is now 'forum_post_id' ('forum_id')
website_forum/18.0.1.2/ ne porte qu'une analyse, aucun script : le
chargement meurt sur une clé étrangère vers une colonne absente et
laisse la base à moitié migrée.

Assisted-by: Claude Opus 5
2026-08-22 07:24:00 -04:00
89ef6a97fd [FIX] migration: exit 1 means findings, not failure, for the view fixer
Entry [5] never asked "Correct them?". The tool ran, printed its
diagnosis, exited 1 -- and todo_upgrade_execute read that as a failure
and reopened ITS error menu on top of ours. The menu looped and nothing
was ever corrected.

wait_at_error=False on both calls, as the COW tool next door already
does. The fake in the test now models the real contract: it refuses to
return a non-zero status when the flag is missing, exactly as the real
one refuses by recursing into its prompt.

--- FR ---

L'entrée [5] ne posait jamais la question « Les corriger ? ». L'outil
tournait, affichait son diagnostic, sortait 1 — et todo_upgrade_execute
y lisait un échec et rouvrait SON menu d'erreur par-dessus le nôtre. Le
menu tournait en rond et rien n'était corrigé.

wait_at_error=False sur les deux appels, comme le fait déjà l'outil COW
juste à côté. Le faux du test modèle désormais le vrai contrat : il
refuse de rendre un statut non nul quand le drapeau manque, comme le
vrai refuse en repartant dans son invite.

Assisted-by: Claude Opus 5
2026-08-22 07:24:00 -04:00
068022db82 [ADD] migration: repair views whose stored type contradicts their parent
Odoo refused to load a database: it validated a <search> arch with tree
rules. The view is not a COW copy -- the COW tools were right to say
they had nothing to reset -- its stored `type` simply lies.

Nothing would ever have fixed it. In ir_ui_view.py, `type` is filled in
`create`, and only when absent; `write` never recomputes it. A wrong
value stays wrong, and `-u module` fails on the validation it causes
before it could rewrite anything.

Plain SQL, no ORM: the registry is what will not load, and a repair that
needed Odoo to fix what stops Odoo would be useless.

The rule is Odoo's own -- an inherited view takes its parent's type --
and it holds: zero disagreement on a pristine 18 install and on four
migrated databases, exactly one on the database that refused to load.

--- FR ---

Odoo refusait de charger une base : il validait un arch <search> avec
les règles d'un tree. La vue n'est pas une copie COW — les outils COW
avaient raison de dire qu'ils n'avaient rien à réinitialiser — c'est son
`type` stocké qui ment.

Rien ne l'aurait jamais réparé. Dans ir_ui_view.py, `type` est rempli
dans `create`, et seulement s'il est absent ; `write` ne le recalcule
jamais. Une valeur fausse le reste, et `-u module` échoue sur la
validation qu'elle provoque avant de pouvoir réécrire quoi que ce soit.

En SQL, sans ORM : c'est le registre qui ne charge plus, et une
réparation qui aurait besoin d'Odoo ne servirait à rien.

La règle est celle d'Odoo — une vue héritée prend le type de son parent
— et elle tient : zéro écart sur une 18 neuve et sur quatre bases
migrées, exactement un sur celle qui refusait de charger.

Assisted-by: Claude Opus 5
2026-08-22 07:24:00 -04:00
ff8b6bcaf9 [ADD] migration quality: lay OpenUpgrade's declared changes over the real ones
The report counted + and -. A field that stops being stored is neither:
it loses its column and keeps its existence. Read as a loss it cries
wolf every step; ignored it would mask a real one. OpenUpgrade already
knows, in the upgrade_analysis.txt files the checkout carries -- the
same source the OCA coverage pages are generated from, read locally, at
the checkout's exact version.

Three categories keep the signal from drowning: a field whose model went
away is not a second finding (544 of them on one real step), a field of
a module OpenUpgrade does not analyse was never its business, and the
rest is grouped by field name -- __last_update counted itself 391 times.

--- FR ---

Le rapport comptait des + et des -. Un champ qui cesse d'être stocké
n'est ni l'un ni l'autre : il perd sa colonne et garde son existence. Lu
comme une perte, il alarme à chaque palier ; ignoré, il en masquerait
une vraie. OpenUpgrade le sait déjà, dans les upgrade_analysis.txt que
porte le checkout -- la source des pages de couverture de l'OCA, lue
localement, à la version exacte.

Trois catégories évitent que le signal se noie : un champ dont le modèle
a disparu n'est pas une trouvaille de plus (544 sur un palier réel), un
champ d'un module non analysé n'a jamais relevé d'OpenUpgrade, et le
reste est groupé par nom -- __last_update se comptait 391 fois.

Assisted-by: Claude Opus 5
2026-08-22 07:24:00 -04:00
8c7ff096b3 [ADD] migration: repair DMS at the 13 bump, and catch the whole class
The repair now runs by itself where the breakage happens: MuK becomes
OCA DMS at 13 and nowhere else, so the hook is gated on that bump. It
stays quiet on databases without DMS and on ones already repaired, and
a successful --apply now exits 0 instead of 1.

The wider hole stays open otherwise: data PRESENT that a global rule
hides entirely. Row counts said everything was there — true. The smoke
test served every page — true. Nobody could reach the records. The new
check runs at every bump and names any model with rows that no active
internal user can see a single one of. Proven both ways on a copy: 85
models clean after the repair, dms.file and dms.directory named before
it.

--- FR ---

La réparation s'exécute désormais là où la casse a lieu : MuK devient
OCA DMS au palier 13 et nulle part ailleurs, d'où le garde. Elle se tait
sur une base sans DMS et sur une base déjà réparée, et un --apply réussi
rend 0 au lieu de 1.

Le trou plus large restait ouvert : des données PRÉSENTES qu'une règle
globale masque entièrement. Les comptages disaient « tout est là » —
vrai. Le test de fumée servait les pages — vrai. Personne n'atteignait
les données. Le nouveau test tourne à chaque palier et nomme tout modèle
dont aucun utilisateur interne actif ne voit une seule ligne. Éprouvé
dans les deux sens sur une copie : 85 modèles sains après réparation,
dms.file et dms.directory nommés avant.

Assisted-by: Claude Opus 5
2026-08-22 07:24:00 -04:00
a9d9a08574 [ADD] migration: restore DMS visibility lost with MuK's access model
Nothing was lost at the 13 bump: 69 files, 16 folders and 23 MB of
content_binary are identical from 12 to 18. What changed is the security
model. MuK filtered on company alone; OCA DMS adds a GLOBAL rule on
permission_read, granted only through a dms.access.group or through
storages saved as attachment. The migration created no group — MuK had
none to convert — and the storages are database. Both doors shut, every
user sees zero.

Reports without writing unless --apply. The visibility count uses a real
user: the global rule spares the superuser, so a sudo count would call a
mute database healthy.

--- FR ---

Rien n'a été perdu au palier 13 : 69 fichiers, 16 dossiers et 23 Mo de
content_binary sont identiques de la 12 à la 18. C'est le modèle de
sécurité qui a changé. MuK ne filtrait que sur la société ; OCA DMS
ajoute une règle GLOBALE sur permission_read, accordée par une
dms.access.group ou par un stockage en attachment. La migration n'a créé
aucun groupe — MuK n'en avait aucun à convertir — et les stockages sont
en database. Les deux portes fermées, personne ne voit rien.

Rapport sans écriture sauf --apply. Le comptage de visibilité passe par
un vrai utilisateur : la règle globale épargne le super-utilisateur, donc
un comptage en sudo déclarerait saine une base muette.

Assisted-by: Claude Opus 5
2026-08-22 07:24:00 -04:00
d2f70f6535 [ADD] migration quality: explain ir_property and tracking values
ir_property never empties, it GROWS — 211 rows in 12, 457 in 17 — then
vanishes in 18. Its fields are jsonb columns there, verified on
res_partner.property_payment_term_id.

mail_tracking_value is a REAL loss, not a rename: `field` goes from
varchar to a foreign key in 14, and what would not resolve was deleted.
1528 of the 2336 belonged to the removed agile module; 706 more survived
under a new name. New kind "pruned" so the report cannot call a living
table retired.

--- FR ---

ir_property ne se vide jamais, elle GROSSIT — 211 lignes en 12, 457 en
17 — puis disparaît en 18. Ses champs y sont des colonnes jsonb, vérifié
sur res_partner.property_payment_term_id.

mail_tracking_value est une VRAIE perte, pas un renommage : `field`
passe de varchar à clé étrangère en 14, et ce qui ne se résolvait pas a
été supprimé. 1528 des 2336 venaient du module agile retiré ; 706 autres
ont survécu sous un autre nom. Nouveau genre « pruned » pour que le
rapport ne déclare pas retirée une table bien vivante.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
53fa936168 [ADD] migration quality: biggest losses first, and the full lists behind « d »
Fifty-seven losses are read from the top, not in alphabetical order. They
are now sorted by rows LOST, not by rows there before: a table of ten
thousand that drops two matters less than one of a thousand that empties.

« d » cycles the pane through the complete lists — modules, models,
fields, COW copies, tables — and back to the summary. The summary cuts at
eight entries and is right to; but when one is looking for whether ONE
module survived, the truncated list does not answer, and that is exactly
when it is needed.

Two inventories were missing and both matter. FIELDS: a lost field is a
lost column of data, finer than a model, which can survive emptied of
half of its own. COW COPIES by key: it is the key one resets, and by the
key one finds them again across versions. On a real 12 → 18 they read
11 036 → 15 069 fields and 64 → 131 copies, of which 28 disappeared —
customisations nothing was reporting until now.

One mode replaces two flags: « show missing files » and « show the model
list » cannot both be true, and two booleans let that impossible state be
written. The mode is named in the sub-title, because a pane that changes
without saying why reads as a broken screen.

--- FR ---

[ADD] qualité de migration : les plus grosses pertes d'abord, les listes derrière « d »

Cinquante-sept pertes se lisent par le haut. Elles sont triées sur le
volume PERDU, non sur le volume présent avant : une table de dix mille qui
en perd deux compte moins qu'une de mille qui se vide.

« d » fait défiler le panneau à travers les listes entières — modules,
modèles, champs, copies COW, tables — puis revient au résumé.

Deux inventaires manquaient. Les CHAMPS : un champ perdu est une colonne
de données perdue. Les COPIES COW par leur clé : sur une vraie 12 → 18,
28 ont disparu — des personnalisations que rien ne signalait.

Un seul mode remplace deux drapeaux, qui laissaient écrire un état
impossible. Il est nommé dans le sous-titre.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
e75bc585ba [ADD] migration quality: tell Odoo's own redesigns from real losses
« 81 tables lost rows » buried the questions worth asking. The largest of
them — ir_translation, 32 984 rows — is a redesign Odoo made in 16, when
translations moved into jsonb columns. Putting redesigns and real losses
on the same footing is the surest way not to see the second kind.

A map now names what Odoo moves or retires, and the report splits the
list in two: unexplained first, then what Odoo did on purpose. On a real
12 → 18 the split is 57 and 14, and the partition is exact — an explained
loss is annotated, never removed, because removing it was the one thing
that could hide a real one.

Every entry was VERIFIED on that migration by counting both sides, not
written from memory: crm_lead_tag 6 → crm_tag +6, mail_channel 26 →
discuss_channel +26, account_invoice 651 → account_move +441. A map
written from memory would explain losses that are not losses.

And a merge whose target gained nothing says so: the explanation does not
hold, and that is exactly what the map was meant to surface.

--- FR ---

[ADD] qualité de migration : distinguer les refontes d'Odoo des vraies pertes

« 81 tables ont perdu des lignes » enterrait les questions qui valaient la
peine. La plus grosse — ir_translation, 32 984 lignes — est une refonte
d'Odoo 16, quand les traductions sont passées en colonnes jsonb.

Une carte nomme désormais ce qu'Odoo déplace ou retire, et le rapport
sépare la liste en deux : l'inexpliqué d'abord, puis ce qu'Odoo a fait
exprès. Sur une vraie 12 → 18 : 57 et 14, et la partition est exacte —
une perte expliquée est annotée, jamais retirée.

Chaque entrée a été VÉRIFIÉE sur cette migration en comptant les deux
côtés : crm_lead_tag 6 → crm_tag +6, mail_channel 26 → discuss_channel
+26, account_invoice 651 → account_move +441.

Et une fusion dont la table d'accueil n'a rien reçu le dit : l'explication
ne tient pas, et c'est précisément ce que la carte devait révéler.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
951b88abab [FIX] todo analyse: give the migration entry its icon
An entry with no icon, in a menu where every other one has one, reads as
unfinished. The section gets 🚚 and the entry 📐 — the same symbol the
quality report already wears in its own title, because it is the same
thing.

The spacing follows a convention this menu keeps without stating it: an
icon of « neutral » width — 🗄, 🖼 — occupies one column in a terminal
and takes TWO spaces to line up, while a wide one — 📏, 📐 — takes one.
Getting it wrong shifts the line, and nothing says so until it is under
your eyes. A test now states it.

--- FR ---

[FIX] todo analyse : donner son icône à l'entrée de migration

Une entrée sans icône, dans un menu où toutes les autres en ont une, se
lit comme une entrée inachevée. La section reçoit 🚚 et l'entrée 📐 — le
symbole que le rapport de qualité porte déjà dans son propre titre,
puisque c'est la même chose.

L'espacement suit une convention que ce menu tient sans la dire : une
icône de largeur « neutre » — 🗄, 🖼 — s'affiche sur une colonne dans un
terminal et prend DEUX espaces pour s'aligner, une icône large — 📏, 📐 —
un seul. Se tromper décale la ligne, et rien ne le signale avant de
l'avoir sous les yeux. Un test l'énonce désormais.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
37c9f216de [ADD] migration quality: name the missing files, and show every delta
« 254 attachment files missing » does not say which. « m » now lists
them, grouped by model AND FIELD — the field is the most useful thing in
the row, because it says which field lost its image. On a real migration
it splits into 234 res.country/image flags, 13 payment.provider logos,
and a handful of scattered attachments including one 255 kB event photo:
the first two are module data a reinstall restores, the last is not, and
the raw list put them on the same footing.

The metadata is read ONLY when asked. One more query per database would
lengthen a survey that runs in four seconds, for something one looks at
by asking for it.

And every figure of a step now carries its change against the previous
one. « 2283 views » is a number; « +61 » is information. The first step
gets none: inventing a zero there would claim a comparison that does not
exist.

--- FR ---

[ADD] qualité de migration : nommer les fichiers absents, et montrer les écarts

« 254 fichiers de pièces jointes absents » ne dit pas lesquels. « m » en
donne la liste, groupée par modèle ET PAR CHAMP — le champ est le
renseignement le plus utile de la ligne, car il dit lequel a perdu son
image. Sur une vraie migration : 234 drapeaux res.country/image, 13 logos
de fournisseurs de paiement, et une poignée de pièces jointes éparses dont
une photo d'événement de 255 ko. Les deux premiers groupes sont des
données de module qu'une réinstallation restaure, le dernier non.

Les métadonnées ne sont lues QU'À LA DEMANDE : une requête de plus par
base allongerait un parcours qui tient en quatre secondes.

Et chaque chiffre d'un palier porte son écart avec le précédent. « 2283
vues » est un nombre, « +61 » est une information. Le premier palier n'en
a pas : y inventer un zéro annoncerait une comparaison qui n'existe pas.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
0fdfeaa9ce [FIX] migration state: open the quality screen in its own process
Pressing « k » crashed with « asyncio.run() cannot be called from a
running event loop ». `suspend()` hands the terminal over but does NOT
stop the loop, and `app.run()` calls `asyncio.run()` underneath. A
subprocess has its own loop and paints the terminal it was given.

My test asserted that suspend() was CALLED, and that structure was
correct — the launch inside it was not. Structure is not behaviour, and
the test that would have caught this is the one that presses the key.
It exists now, and a guard refuses the nested start with a sentence
instead of forty lines of traceback.

The screen module also lost its shebang: it has no main, so the line
claimed something that was not true, and the permission rule was right
to say so.

--- FR ---

[FIX] état de migration : ouvrir l'écran de qualité dans son processus

« k » plantait sur « asyncio.run() cannot be called from a running event
loop ». `suspend()` rend le terminal mais n'arrête PAS la boucle, et
`app.run()` appelle `asyncio.run()` en dessous. Un sous-processus a sa
propre boucle.

Mon test vérifiait que suspend() était APPELÉ — et cette structure était
juste ; c'est le lancement à l'intérieur qui ne l'était pas. La structure
n'est pas le comportement, et le test qui aurait attrapé cela est celui
qui presse la touche. Il existe désormais, et un garde-fou refuse le
lancement imbriqué par une phrase plutôt que par quarante lignes de trace.

Le module d'écran perd aussi son shebang : sans `main`, cette ligne
annonçait une intention qui n'existait pas.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
4028421cb8 [ADD] analyse: what a migration gained and lost, step by step
A migration leaves one database per step and they all still exist, so the
tool compares them side by side rather than replaying anything. Six Odoo
starts would cost an hour, write to the databases and need the checkout
switched at each step; the same inspection in SQL takes under half a
second per database and touches nothing. Measured on a real migration:
seven databases surveyed in 3.8 seconds.

It reports what each step gained and lost — modules, models, views, menus,
attachments — and ends on the comparison of the ENDS, which is not the sum
of the steps: a module dropped at 15 and put back at 17 lost nothing, and
adding the steps would count it twice.

The signal that matters is rows. A module removed is visible; a table
going from four thousand rows to zero is visible nowhere.

Two rename heuristics were tried and rejected on real data before the one
that holds. Row count alone paired an account tag table with dms_directory
— both had seven rows. A shared five-letter word paired two cleanup
tables. Overall name similarity separates them. And a renamed table stays
in the loss list, annotated: removing it was the real danger, since a
wrong pairing would have hidden a genuine loss.

--- FR ---

[ADD] analyse : ce qu'une migration a gagné et perdu, palier par palier

Une migration laisse une base par palier et elles existent toutes encore :
l'outil les compare côte à côte plutôt que de rejouer quoi que ce soit.
Six démarrages d'Odoo coûteraient une heure et écriraient dans les bases ;
la même inspection en SQL prend moins d'une demi-seconde par base. Mesuré
sur une vraie migration : sept bases inspectées en 3,8 secondes.

Le bilan final compare les EXTRÉMITÉS, ce qui n'est pas la somme des
paliers : un module retiré en 15 puis remis en 17 n'a rien perdu.

Le signal qui compte, ce sont les lignes. Un module en moins se voit ; une
table qui passe de quatre mille lignes à zéro ne se voit nulle part.

Deux rapprochements de renommage ont été essayés et rejetés sur des
données réelles. Et une table renommée reste dans la liste des pertes,
annotée : l'en retirer était le vrai danger.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
6715700a16 [ADD] migration state: read the server log so nobody has to
A step log reaches thirteen megabytes — measured on a real migration.
Nobody reads that, and the question one asks in front of it is two words
long: where did it go wrong? The screen now answers it.

Each step carries its count of ERROR and CRITICAL, and the pane lists the
DISTINCT messages with how many times each occurred. Forty-eight
« Model X has no table » is one problem seen forty-eight times, not
forty-eight problems, and the raw list buries everything else. WARNING is
left out of the count: a migration produces thousands, and a total that
includes them means nothing.

Each message keeps its logger and its DATABASE, which is what tells the
six bumps apart — they shared one file before a step opened its own.

Measured: thirteen megabytes scanned in 0.09 s, seventy occurrences
reduced to fifteen messages, and cached on (size, mtime) because the
screen repaints on every keystroke.

--- FR ---

[ADD] état de migration : lire le journal pour que personne n'ait à le faire

Un journal d'étape atteint treize mégaoctets — mesuré. Personne ne le
lit, et la question qu'on se pose devant lui tient en deux mots : où
est-ce que ça a mal tourné ? L'écran y répond désormais.

Chaque étape porte son compte d'ERROR et de CRITICAL, et le panneau liste
les messages DISTINCTS avec leur nombre. Quarante-huit fois « Model X has
no table » est un problème vu quarante-huit fois, pas quarante-huit
problèmes. Les WARNING sont hors du compte : une migration en produit des
milliers.

Chaque message garde son logger et sa BASE, ce qui distingue les six
paliers — ils partageaient un fichier avant qu'une étape n'ouvre le sien.

Mesuré : treize mégaoctets analysés en 0,09 s, soixante-dix occurrences
ramenées à quinze messages.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
6dff7aa304 [ADD] migration: announce the countdown, and cycle three panel states
Every prompt now carries « ⏱5s » before the question. One that looks like
it waits forever does not invite you to step away, which is the whole
point of auto-run. The same clock reports the answer afterwards: it
announces the wait, then says what it decided.

Enter on the auto-run question already meant no; it now says so, with an
explicit default instead of a deduction. And the flag is cleared before
asking — an ERPLIBRE_AUTO_EXECUTE inherited from the shell would have
answered yes on someone's behalf, and that answer commits every later one.

« p » cycles three states rather than toggling one: a plain toggle freed
four lines, while what takes the room is the left COLUMN — on a server log
every character of width counts. All → no summary → detail only, and back.
The state is named in the sub-title, which stays visible in all three: a
panel that vanishes without a word reads as a broken screen.

--- FR ---

[ADD] migration : annoncer le compte à rebours, et trois états de panneaux

Chaque invite porte désormais « ⏱5s » devant la question. Une invite qui a
l'air d'attendre indéfiniment n'invite pas à s'absenter, et c'est pourtant
tout l'intérêt du mode auto. Le même chronomètre rend compte ensuite.

Entrée sur la question d'auto-exécution voulait déjà dire non ; elle le
DIT maintenant, par un défaut explicite plutôt qu'une déduction. Et le
drapeau est effacé avant de demander : une variable héritée du shell
aurait répondu « oui » à la place de quelqu'un.

« p » enchaîne trois états au lieu d'en basculer un : ce qui prend la
place, c'est la colonne de gauche. Tout → sans résumé → détail seul, et
l'on revient. L'état est nommé dans le sous-titre.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
e981500c94 [ADD] migration state: how long the migration took
From the first write to the LAST one. The progression file is rewritten
after every move, so its update date is the end — or the present moment
while the migration is still running. Both dates were already shown; what
was missing was the one number one actually wants.

The statistics screen already computed this. It is reused rather than
rewritten: two formulas would give two durations for the same migration
depending on which screen one opens.

--- FR ---

[ADD] état de migration : combien de temps elle a duré

Du premier écrit au DERNIER. Le fichier de progression est réécrit après
chaque geste, donc sa date de mise à jour est la fin — ou l'instant
présent tant que la migration tourne. Les deux dates étaient déjà
affichées ; il manquait le seul chiffre qu'on cherche.

L'écran de statistiques portait déjà ce calcul. Il est réutilisé plutôt
que réécrit : deux formules donneraient deux durées pour la même
migration selon l'écran qu'on ouvre.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
c0c5144680 [ADD] migration state: l, p, r and the two resize keys
Four keys, and one bug behind them. « l » separates the commands from the
server log: the first says what was LAUNCHED, the second what it
ANSWERED, and an update writing tens of thousands of lines buries three
commands. « p » hides the summary panel, « - » and « + » move the split,
« r » re-reads the disk — the migration writes while one watches, and
closing the screen to see the next bump is what one ended up doing.

The « missing logs » had two causes, both real. A tail was shown without
saying it was one; it now names how many lines are hidden. And the first
two steps run before the database is named, so their logs landed under
« sans-nom » — outside the migration they belong to, invisible from the
screen. They are brought back the moment the name is known, appended
never overwritten.

The step 4 bug was already fixed: its log held all six bumps because only
print_step opened one, and the bump loop never calls it.

--- FR ---

[ADD] état de migration : l, p, r et les deux touches de taille

Quatre touches, et un défaut derrière. « l » sépare les commandes du
journal : la première dit ce qui a été LANCÉ, le second ce que cela a
RÉPONDU, et une mise à jour qui écrit des dizaines de milliers de lignes
enterre trois commandes. « p » cache le panneau de résumé, « - » et « + »
déplacent la séparation, « r » relit le disque — la migration écrit
pendant qu'on regarde.

Les « logs manquants » avaient deux causes. On montrait une fin sans dire
que c'en était une ; le nombre de lignes cachées est désormais nommé. Et
les deux premières étapes tournent avant que la base ne soit nommée :
leurs journaux atterrissaient sous « sans-nom », hors de la migration à
laquelle ils appartiennent. Ils la rejoignent dès que le nom est connu.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
1e591dd949 [FIX] migration state: a test result belongs to a step, not to the run
You were right that it read as global — and the cause ran deeper than the
display. Twenty step headers go through add_comment_progression against
seven through print_step, and the whole version-bump loop uses only the
first. The current step was set in the other one, so every tool verdict
of every bump carried a stale step.

The header itself now becomes the step, at the one place the journal is
already cut on. And the summary groups by (step, tool) instead of by tool
alone: a migration runs the smoke test at EVERY bump, and one line said
« smoke_public_url ✅ » while bump 14 had passed and bump 17 had fallen.

Within a step the last verdict still wins, with its run count — that is a
repair, not two bumps. The order is the journal's, because sorting step
names puts « 4.10 » before « 4.2 ».

--- FR ---

[FIX] état de migration : un résultat de test appartient à une étape

Vous aviez raison, cela se lisait comme global — et la cause était plus
profonde que l'affichage. Vingt en-têtes d'étape passent par
add_comment_progression contre sept par print_step, et toute la boucle des
paliers n'utilise que la première. L'étape courante était posée dans
l'autre : chaque verdict d'outil portait donc une étape périmée.

L'en-tête devient désormais l'étape, au seul endroit où le journal est
déjà découpé. Et le résumé groupe par (étape, outil) : une migration lance
le test de fumée à CHAQUE palier, et une seule ligne annonçait
« smoke_public_url ✅ » quand le palier 14 était passé et le 17 tombé.

Dans une étape, le dernier verdict l'emporte toujours — c'est une
réparation, pas deux paliers.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
0f0f853be4 [ADD] migration state: colour the commands, and know when not to
A list of commands in flat text blends into its own headings the moment it
outgrows the screen — which is exactly when it gets read. Commands are now
cyan, steps bold blue, failed ones red, and each verdict wears the colour
of its meaning: 1 is amber, because « there is something to look at » is
not a crash and painting it red worries for nothing.

Colour is refused three times over, and each refusal has cost someone: a
log file full of escape codes, a grep that finds nothing, a terminal that
prints them as text. A pipe is not a screen, and NO_COLOR is honoured.

The full screen goes through Text.from_ansi, which does two things: it
renders the colours, and it takes the rest literally. Textual parses Rich
markup, so a command containing « [1] » was being eaten as a tag and
vanished from the pane without a word. That was true before the colour.

--- FR ---

[ADD] état de migration : colorer les commandes, et savoir s'en abstenir

Une liste de commandes en texte plat se confond avec ses titres dès
qu'elle dépasse l'écran — et c'est justement quand elle le dépasse qu'on
la lit. Les commandes sont en cyan, les étapes en bleu gras, celles qui
ont échoué en rouge, et chaque verdict porte la couleur de son sens : 1
est ambre, car « il y a quelque chose à regarder » n'est pas une panne.

La couleur se refuse trois fois, et chaque refus a coûté à quelqu'un : un
fichier de journal truffé de codes d'échappement, un grep qui ne trouve
plus rien, un terminal qui les affiche en clair.

Le plein écran passe par Text.from_ansi, qui rend les couleurs ET prend le
reste au pied de la lettre : une commande contenant « [1] » était avalée
comme du balisage Rich et disparaissait sans un mot.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
4e0bf7cc8b [FIX] migration: the countdown names the answer the prompt offered
« ⏱ → d » named a reply that was not on the menu. The prompt reads
« Enter = delete, after saving them, k = keep » and mentions « d »
nowhere, so the line asking to be trusted with a decision described it in
a vocabulary the reader had never been given.

The decision itself was right — Enter deletes, and that is what happened.
Only its report was written from the inside. It now says Enter, in the
migration's language, with the value in brackets for whoever wants the
precision. What the caller receives is unchanged.

--- FR ---

[FIX] migration : le compte à rebours nomme la réponse que l'invite offrait

« ⏱ → d » nommait une réponse qui n'était pas au menu. L'invite annonce
« Entrée = effacer, après les avoir sauvegardés, k = garder » et ne
mentionne « d » nulle part : la ligne qui demandait qu'on lui confie une
décision la décrivait donc dans un vocabulaire jamais montré.

La décision, elle, était juste — Entrée efface, et c'est ce qui a eu lieu.
Seul son compte rendu était écrit du dedans. Il dit maintenant Entrée,
dans la langue de la migration, la valeur entre parenthèses pour qui veut
la précision. Ce que l'appelant reçoit ne change pas.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
e214df7329 [FIX] smoke: judge the back office AFTER the repair, and say why it failed
No timeout was hit and no test was skipped. The back office failed for the
same reason the site did: the COW copy of website.submenu breaks
/web/login exactly as it breaks the public pages — both go through the
same layout. The pass simply ran BEFORE the reset, and unlike the URLs it
was never looked at again. It is now, on the same second server.

« Session expired » described the consequence and hid the cause. The login
page returned 500 and nothing said so, so one looks at the password. The
status of that page, and a missing CSRF token, are now named.

Diagnosing this, I started Odoo 18 on a 17 database and got 500 on all
thirty-seven URLs and on /web/login: a report that says the site is
entirely broken when nothing is. database_cleanup already refused that;
the smoke tools now share the same guard rather than reimplementing it.

--- FR ---

[FIX] smoke : juger le back-office APRÈS la réparation, et dire pourquoi

Aucun délai n'a été atteint et aucun test n'a été sauté. Le back-office a
échoué pour la raison même qui cassait le site : la copie COW de
website.submenu casse /web/login comme elle casse les pages publiques —
les deux passent par le même gabarit. La passe tournait avant la
réinitialisation et, contrairement aux URL, n'était jamais revue.

« Session expired » décrivait la conséquence et cachait la cause : la page
de connexion rendait 500 sans que rien ne le dise, et l'on cherchait du
côté du mot de passe.

En diagnostiquant, j'ai lancé un Odoo 18 sur une base 17 : 500 partout, un
rapport qui déclare le site entièrement cassé quand rien ne l'est.
database_cleanup refusait déjà cela ; les outils de fumée partagent
désormais sa garde au lieu d'en écrire une seconde.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
19d807a81e [ADD] migration: keep the logs on disk, one file per step
The state screen showed « no tool has run yet » after closing and
reopening. It was reading the progression file, and that file is archived
and reset when a migration restarts — so everything it knew vanished at
the exact moment one wants to understand why the restart was needed.

Each step now has its own log file, appended never overwritten, and the
failures and tool verdicts go to an append-only JSONL beside them. Both
outlive the progression, and the screen reads disk first, memory second,
without counting the overlap twice.

The command output itself is captured where every line already passes,
in the executor's read loop: the terminal still shows it live and nothing
about the run changes. What goes through the real terminal cannot be
captured — a pipe there makes full screens refuse, that lesson is paid —
so those keep at least their command and their exit code.

--- FR ---

[ADD] migration : garder les journaux sur disque, un fichier par étape

L'écran d'état affichait « aucun outil n'a encore tourné » après une
fermeture. Il lisait le fichier de progression, or celui-ci est archivé
puis remis à zéro quand on recommence : tout ce qu'il savait disparaissait
au moment précis où l'on cherche pourquoi il a fallu recommencer.

Chaque étape a désormais son fichier, en ajout et jamais en écrasement, et
les échecs comme les verdicts d'outils vont dans un JSONL à côté. Les deux
survivent à la progression, et l'écran lit le disque d'abord.

La sortie des commandes est captée là où chaque ligne passe déjà, dans la
boucle de lecture de l'exécuteur : le terminal la montre toujours en
direct. Ce qui passe par le vrai terminal n'est pas captable — un tube y
ferait renoncer les pleins écrans — et garde au moins son verdict.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
566070b4f8 [FIX] migration: Enter exports the backup at the end
After several hours of migration the archive is what one wanted anyway,
and the prompt asked for it with an empty default: pressing Enter — or
letting auto-run answer — walked past the only artifact of the whole run.

The three-way answer moved into a function of its own, because the third
way is the one that traps. « n » refuses, « y » or Enter takes the
timestamped name, and EVERYTHING ELSE is a filename. So « non.zip » is a
file, not a refusal: comparing the whole answer rather than its first
letter is what keeps it one.

--- FR ---

[FIX] migration : Entrée exporte la sauvegarde à la fin

Au bout de plusieurs heures de migration, l'archive est ce qu'on voulait
de toute façon, et l'invite la proposait avec un défaut vide : appuyer sur
Entrée — ou laisser l'auto-exécution répondre — passait à côté du seul
artefact de toute la course.

La réponse à trois sens est devenue une fonction à part, car c'est le
troisième qui piège. « n » refuse, « y » ou Entrée prend le nom horodaté,
et TOUT LE RESTE est un nom de fichier. « non.zip » est donc un fichier et
non un refus : comparer la réponse entière plutôt que sa première lettre
est ce qui le garantit.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
3a182f2b0d [ADD] migration: reach the state screen from the statistics too
The statistics screen answers « what was removed, and why ». The state
screen answers « where are we, and what broke ». Two neighbouring
questions asked at the same moment: separating them by a menu entry
rather than by two commands to remember is what makes them usable.

Same letter as the prompts. One that changes meaning from one screen to
the next is not learned, it is looked up — and nobody looks it up.

Adding it surfaced two ways that screen could not survive auto-run. The
interface question is the FIRST one asked after auto-run is switched on,
and it was a bare input(): an unattended migration stopped there before
it had begun. The statistics menu was one too. And the state screen
writes the progression before reading it — but the statistics screen runs
before the progression is even loaded, so that write failed on an
attribute that does not exist yet.

--- FR ---

[ADD] migration : atteindre l'état depuis les statistiques aussi

L'écran de statistiques répond « qu'a-t-on supprimé, et pourquoi ». Celui
de l'état répond « où en est-on, et qu'est-ce qui a cassé ». Deux
questions voisines, posées au même moment : les séparer par une entrée de
menu plutôt que par deux commandes à retenir est ce qui les rend
utilisables.

La même lettre que dans les invites. Une lettre qui change de sens d'un
écran à l'autre ne s'apprend pas, elle se cherche — et personne ne la
cherche.

L'ajout a révélé deux façons dont cet écran ne survivait pas au mode
auto : la question d'interface, PREMIÈRE posée après l'activation, était
un input() nu, et le menu des statistiques aussi. Et l'écran d'état écrit
la progression avant de la lire, alors que les statistiques s'ouvrent
avant même qu'elle ne soit chargée.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
6c186f71e9 [ADD] migration: « t » shows where the migration stands
A migration crosses six bumps, runs hundreds of commands and lasts hours.
The journal said what had been LAUNCHED; it never said what came of it.
Three hours in, one reads two hundred command lines without knowing which
one failed, nor what the smoke test concluded.

« v » could not carry this: it already means « view the differences » in
three prompts, and a letter meaning two things is worse than a letter
meaning nothing. « t » was free, and it is the same everywhere — one
shared string carries both shortcuts, so no prompt can drift.

Looking is not answering: the same question comes back afterwards.

What was missing was the data. Failures and tool verdicts are now
recorded with the step they happened in, bounded so a progression file
cannot grow without end. A tool rerun after a repair keeps its LAST
verdict and the count of its runs — showing both without distinction
would read a repair as a lasting failure.

--- FR ---

[ADD] migration : « t » montre où en est la migration

Une migration traverse six paliers, lance des centaines de commandes et
dure des heures. Le journal disait ce qui avait été LANCÉ, jamais ce que
cela avait donné. Trois heures plus tard on relit deux cents lignes sans
savoir laquelle a échoué, ni ce que le test de fumée a conclu.

« v » ne pouvait pas porter cela : il veut déjà dire « voir les
différences » dans trois invites, et une lettre qui signifie deux choses
est pire qu'une lettre qui ne signifie rien. « t » était libre, et il est
le même partout — une seule chaîne porte les deux raccourcis.

Regarder n'est pas répondre : la même question revient ensuite.

Ce qui manquait, c'étaient les données. Les échecs et les verdicts
d'outils sont désormais retenus avec l'étape où ils se sont produits.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
fc23bd6f96 [FIX] migration: a skipped back office must not read as a healthy one
The back-office pass was already there, and it works. What did not work
was the way it declined: one discreet line at the end of a long report,
saying « the database was not neutralized ». On six databases of a real
migration the test user is present up to the 15 bump and GONE at 17 and
18 — so the pass stopped silently exactly where a migration does the most
damage, and said something that was not even true.

The migration knows what it neutralized, so it now asks for the back
office by name. A missing test user on a database it neutralized is a
finding, printed loudly and counted as a failure. A missing tool is too:
returning None made the whole pass vanish without a word.

And it says UP FRONT which passes will run, on that database, by name.

--- FR ---

[FIX] migration : un back-office sauté ne doit pas se lire comme un sain

La passe back-office était déjà là et elle fonctionne. Ce qui ne
fonctionnait pas, c'est sa façon de renoncer : une ligne discrète en fin
d'un long rapport, disant « la base n'a pas été neutralisée ». Sur les six
bases d'une vraie migration, l'utilisateur test est présent jusqu'au
palier 15 et ABSENT en 17 et 18 — la passe s'arrêtait donc sans bruit là
où une migration fait le plus de dégâts, en disant quelque chose de faux.

La migration sait ce qu'elle a neutralisé : elle réclame désormais le
back-office. Un utilisateur test manquant sur une base qu'elle a
neutralisée est une trouvaille, affichée fort et comptée comme un échec.
Un outil absent aussi : rendre None faisait disparaître la passe entière.

Et elle annonce AVANT de lancer ce qui sera parcouru.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
1c7664409e [FIX] migration: the module-upgrade step no longer waits forever
Six « press to continue » prompts on the module-code migration were bare
input() calls. Auto-run never reached them, so it stopped there and said
nothing — the question had been asked, after all. They now go through the
timed reader, where Enter and the countdown mean the same thing.

They were English too, on a path the rest of which speaks the system
language.

The guard that forbids a bare input() only looked at a handful of
methods, and internal_module_upgrade was not among them — which is
exactly how these six survived the first sweep. It is now covered, and
the module menu's own prompts are documented as deliberately blocking:
they ask for a module name, a path, a version, and « » after five
seconds would be a wrong answer, not a convenience.

--- FR ---

[FIX] migration : l'étape de migration de code n'attend plus indéfiniment

Six invites « appuyez pour continuer » de la migration de code étaient des
input() nus. L'auto-exécution ne les atteignait pas : elle s'arrêtait là
sans rien dire — la question avait bien été posée. Elles passent
désormais par le lecteur temporisé, où Entrée et le compte à rebours font
la même chose.

Elles étaient aussi en anglais, sur un chemin dont tout le reste parle la
langue du système.

Le garde-fou qui interdit les input() nus ne regardait qu'une poignée de
méthodes, et internal_module_upgrade n'en faisait pas partie — c'est
ainsi que ces six-là avaient survécu. Il la couvre maintenant, et les
invites du menu de modules sont documentées comme bloquantes à dessein :
elles demandent un nom, un chemin, une version.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
7296a03179 [FIX] migration: repair, replay, and know when to stop
The failure that stops an upgrade is almost always the same one — a COW
copy left behind on the previous version — and the repair is known. So
Enter now repairs, and a repair that actually changed something replays
the command by itself.

A default that acts must know when to stop, and here it must twice over.
Repairing when there is nothing left to repair, then offering it again,
loops without end: measured, « no COW copy has drifted » over and over.
And a repair that does not help would replay the command forever. Three
attempts, then the prompt says plainly that this one needs a developer.

The turn counter is deliberately redundant with that logic: both guard
the same failure, but the counter holds even if the logic is broken one
day by accident. An endless loop in an unattended migration costs a
night.

Also: the smoke tool forced `ask=input` on its own prompt, which
short-circuited auto-run — the question waited for a keystroke nobody
was there to give.

--- FR ---

[FIX] migration : réparer, rejouer, et savoir s'arrêter

L'échec qui arrête une migration est presque toujours le même — une copie
COW restée sur la version d'avant — et la réparation est connue. Entrée
répare donc, et une réparation qui a vraiment changé quelque chose rejoue
la commande d'elle-même.

Un défaut qui agit doit savoir s'arrêter, et ici deux fois plutôt qu'une.
Réparer quand il n'y a plus rien à réparer, puis le reproposer, boucle
sans fin : mesuré, « Aucune copie COW n'a dérivé », encore et encore. Et
une réparation qui ne suffit pas rejouerait indéfiniment. Trois
tentatives, puis l'invite dit qu'il faut un développeur.

Le compteur de tours double volontairement cette logique : il tient même
si elle est cassée un jour par mégarde.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
58bdd70526 [FIX] database_cleanup: purge the batch, and say what you are doing
Seventeen minutes with no output looks exactly like an infinite loop. It
was not one: measured on the running process, zero lock waits and queries
changing every sample. It was working, silently, one module every thirty
seconds.

Both halves were mine. Purging line by line — added so one refusal could
not sink a category — calls button_immediate_uninstall() per line, and
that reloads the WHOLE registry: 5984 modules re-read each time. The
batch is now purged in one call, exactly as the OCA wizard intends, and
the per-line isolation only costs when a refusal actually happens.

And the tool captured its child's output, so nothing showed before the
end. It now relays each step as it arrives, with elapsed seconds, and a
timer kills the process group when the deadline passes.

--- FR ---

[FIX] database_cleanup : purger le lot, et dire ce qu'on fait

Dix-sept minutes sans une ligne ressemblent trait pour trait à une boucle
infinie. Ce n'en était pas une : mesuré sur le processus vivant, zéro
verrou en attente et des requêtes qui changeaient à chaque instantané. Il
travaillait, en silence, à raison d'un module toutes les trente secondes.

Les deux moitiés étaient de mon fait. Purger ligne par ligne — ajouté
pour qu'un refus n'emporte pas la catégorie — appelle
button_immediate_uninstall() par ligne, ce qui recharge TOUT le registre :
5984 modules relus à chaque fois. Le lot est désormais purgé en un seul
appel, comme le module OCA le prévoit, et l'isolement ne coûte que
lorsqu'un refus survient vraiment.

Et l'outil capturait la sortie de son enfant : rien avant la fin. Il
relaie maintenant chaque étape, temps écoulé compris.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
eaf70bdebf [ADD] migration: open /my as the test user, and blame the right request
The portal is a third rendering, neither the public site nor the back
office: QWeb frontend with counters that each query their own model. The
sitemap does not list /my — the page needs a session — and no RPC call
goes through it. A migration can break it with nothing else noticing.

Measured on a real database: /my answered 500 on a user field a module no
longer defines, while every public page and sixteen apps were fine.

Two guards the run proved necessary. Requested without a session, /my
redirects to the login form and answers 200: counting that as a success
would announce a healthy portal never seen. And the production error page
shows no traceback, so the reason comes from the server log — attributed
by PATH, because the portal is checked first and taking the last trace
blamed it for an app's failure. A wrong cause costs more than none.

--- FR ---

[ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête

Le portail est un troisième rendu, ni le site public ni le back-office :
du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle.
Le sitemap ne liste pas /my — la page demande une session — et aucun appel
RPC n'y passe. Une migration peut le casser sans que rien ne le dise.

Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un
module ne définit plus, alors que les pages publiques et seize
applications allaient bien.

Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my
redirige vers le formulaire de connexion et rend 200 : le compter pour une
réussite annoncerait un portail jamais vu. Et la page d'erreur de
production ne montre aucune trace : la raison vient du journal, attribuée
par CHEMIN — le portail est interrogé en premier, et prendre la dernière
trace l'accusait de la panne d'une application.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
d8794f0e10 [ADD] migration: open every app as the neutralization test user
The public smoke test opens what a visitor reaches. It says nothing about
the back office, which is where a migration does most of its damage: a
field dropped from a model but still named in a form, a module installed
in the database whose code no longer ships with the target version. None
of it stops the module loading — it stops the day someone opens the app.

Neutralizing installs a `test` / `test` login carrying the SYSTEM user's
groups, and it survives the module's uninstall. So the tool checks for
that user rather than trusting a flag, and it rides the server the public
pass already started: booting Odoo is what costs minutes, not requests.

Measured on a real 18.0 database of 25 apps, which found four defects of
mine: web_search_read changed signature in 17, a bare 404 hides an
unregistered model, embedded sub-views are not the parent's fields, and
an empty psql result meant two different things.

--- FR ---

[ADD] migration : ouvrir chaque application avec l'utilisateur test

Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du
back-office, où une migration fait pourtant l'essentiel de ses dégâts : un
champ retiré du modèle mais toujours nommé dans un formulaire, un module
installé en base dont le code n'accompagne plus la version cible. Rien de
cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli.

La neutralisation pose un compte `test` / `test` portant les groupes du
superutilisateur, et il survit à la désinstallation du module. L'outil
vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur
déjà démarré : c'est le démarrage qui coûte, pas les requêtes.

Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre
défauts de mon fait.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
95a6ba558e [ADD] migration: make Enter mean the answer you always give
Auto-run promised to "take the default after five seconds", but every
default was EMPTY: it took nothing. Worse, half the prompts of a
migration are asked by separate processes — the theme uninstaller, the
stale-SCSS detector, the smoke test. They knew nothing of auto-run and
waited forever for a keystroke that never came.

The countdown now lives in one file and travels through the ENVIRONMENT,
the only channel a fork shares. Enter takes the default everywhere, not
only under auto-run: a prompt that prints (Y/n) and does otherwise is
worse than no prompt. Every text was rewritten to say what Enter does,
and every default kept an explicit way out.

Two defaults now write. Both are tenable only because the backup runs
first, and a test locks that ORDER rather than trusting a promise.

--- FR ---

[ADD] migration : faire d'Entrée la réponse qu'on donne toujours

L'auto-exécution promettait « le défaut après cinq secondes », mais tous
les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des
invites d'une migration sont posées par d'autres processus — le
désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils
ignoraient l'auto-exécution et attendaient sans fin une frappe.

Le compte à rebours tient désormais dans un seul fichier et voyage par
l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut
partout, pas seulement en auto : une invite qui affiche (Y/n) et fait
l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée,
et chaque défaut garde une issue explicite.

Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe
d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
0c0ed07aa8 [FIX] migration: the cleanup installs its own module, and survives a refusal
Three defects, all mine, all found on a real run.

Creating a wizard could fail and leave the transaction ABORTED. The next
name read died on it, outside any guard, and the whole script stopped
with no report at all — only a traceback. Creating and reading the names
now happen inside the savepoint, and a report is printed whatever
happens: knowing what was done matters more than the trace of what
broke.

« No orphaned models found » is a UserError: the module signals the
EMPTY by raising. Counting it as a failure made a healthy database look
broken, four warnings out of five kinds.

The module was installed at step 3, after being used at step 2, so the
first run had no wizard at all and answered « nothing to do ». It is now
installed by the tool itself, and step 3 no longer asks to redo by hand
what has just been done automatically.

--- FR ---

[FIX] migration : le nettoyage pose son module, et survit à un refus

Trois défauts, tous à moi, tous trouvés sur une vraie exécution.

Créer un assistant pouvait échouer en laissant la transaction AVORTÉE.
La lecture de nom suivante mourait dessus, hors de tout garde, et le
script s'arrêtait sans aucun rapport — juste une trace. La création et
la lecture des noms sont désormais dans le point de reprise, et un
rapport est imprimé quoi qu'il arrive : savoir ce qui a été fait vaut
mieux que la trace de ce qui a cassé.

« No orphaned models found » est une UserError : le module signale le
VIDE en levant. Le compter comme un échec faisait passer une base saine
pour cassée, quatre avertissements sur cinq catégories.

Le module était installé à l'étape 3, après avoir servi à l'étape 2 : le
premier passage n'avait donc aucun assistant et répondait « rien à
faire ». L'outil le pose lui-même, et l'étape 3 ne demande plus de
refaire à la main ce qui vient d'être fait.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
cb77394869 [ADD] migration: an auto-run that takes the default after five seconds
A migration asks dozens of questions whose answer is almost always the
one offered, and someone has to stand there pressing Enter for hours.
Asked before the version choice — so it covers that choice too, now
that it has a default — and off by default: it decides in your place.

Every prompt of the migration goes through one reader, seventeen of
them. A prompt left as a bare input() would know nothing of auto mode
and would block the run with nothing to say why; a test reads the
function and rejects any that appears.

click.prompt had to go: it cannot hand back control after a delay, so
auto would have stopped at the first question. select() rather than a
thread or an alarm — a thread would leave a blocked input() behind it,
stealing the next keystroke.

--- FR ---

[ADD] migration : une auto-exécution qui prend le défaut après cinq secondes

Une migration pose des dizaines de questions dont la réponse est presque
toujours celle proposée, et il faut rester devant à taper Entrée pendant
des heures. Posée avant le choix de version — donc elle vaut aussi pour
lui, maintenant qu'il a un défaut — et éteinte par défaut : elle décide
à votre place.

Toutes les invites de la migration passent par une seule lecture,
dix-sept d'un coup. Une invite laissée en input() nu ne saurait rien du
mode auto et bloquerait la migration sans rien dire ; un test lit la
fonction et refuse toute apparition.

click.prompt devait partir : il ne sait pas rendre la main après un
délai, l'auto se serait arrêté à la première question. select() plutôt
qu'un fil ou une alarme — un fil laisserait un input() bloqué derrière
lui, qui volerait la frappe suivante.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
1831b234ea [ADD] migration: run the OCA database cleanup before testing the pages
The eight purges are not independent: purging a model frees the columns
that referenced it, purging a table frees the data pointing at it. One
pass is never enough, so the requested order runs again until a full
pass repairs nothing new.

Refusals are expected — a foreign key still holds, a module says no.
Purging a list at once loses everything to the first one, so each entry
purges inside its own savepoint: a refusal rolls back that entry alone.
What one pass could not take, the next may, once its neighbours are
gone. Leftovers are reported as a warning: a database can carry some
that nothing removes, and stopping there would help no one.

It refuses a version mismatch. Measured while building it: a shell on
Odoo 14 opened against a 17.0 database went rewriting ir_model before
dying on a jsonb it did not know. An older Odoo does not merely fail on
a newer database — it writes on the way.

--- FR ---

[ADD] migration : lancer le nettoyage OCA avant de tester les pages

Les huit purges ne sont pas indépendantes : purger un modèle libère les
colonnes qui le référençaient, purger une table libère les données qui
la visaient. Une passe ne suffit jamais, l'ordre demandé est donc rejoué
jusqu'à ce qu'une passe entière ne répare plus rien.

Les refus sont attendus — une clé étrangère tient, un module dit non.
Purger d'un bloc perdrait tout au premier : chaque entrée passe dans son
propre point de reprise, un refus n'emporte que la sienne. Ce qu'une
passe n'a pu prendre, la suivante le peut, une fois les voisines
parties. Les restes sont un avertissement : une base peut en porter que
rien ne retire, et s'arrêter là n'aiderait personne.

Il refuse une version qui ne correspond pas. Mesuré en le construisant :
un shell en Odoo 14 ouvert sur une base 17.0 est parti réécrire ir_model
avant de mourir sur un jsonb inconnu de lui. Un Odoo plus ancien
n'échoue pas simplement sur une base plus récente — il écrit en chemin.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
95ffbe6f13 [ADD] migration: browse the drifted COW copies full screen
The text report puts a thousand lines between the two things that decide:
what the copy holds that the module view does not — all a reset gives up
— and which child no longer finds its anchor, which is why anything
breaks. Space switches between them, « c » copies the reset command.

Offered as [4] on the error prompt, next to check and reset, and run on
the real terminal: a full screen started through the capturing executor
falls back to the text report with nothing to tell the two apart.

The diff is rendered in one place, shared with the command line. Two
renderings would drift without anything saying so.

--- FR ---

[ADD] migration : parcourir les copies COW dérivées en plein écran

Le rapport texte met mille lignes entre les deux choses qui décident :
ce que la copie porte et que la vue module n'a pas — tout ce qu'une
réinitialisation abandonne — et quel enfant ne trouve plus son ancrage,
la raison pour laquelle quoi que ce soit casse. L'espace bascule, « c »
copie la commande de réparation.

Proposé en [4] à l'invite d'erreur, à côté de vérifier et réinitialiser,
et lancé sur le vrai terminal : un plein écran passé par l'exécuteur qui
capture retombe sur le rapport texte, sans rien qui les distingue.

Le diff est rendu à un seul endroit, partagé avec la ligne de commande.
Deux rendus dériveraient sans que rien ne le signale.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00
2eac9e9848 [ADD] migration: measure the public URLs before the first bump too
Without a starting point, a page that already answered 500 reads as
damage done by the migration, and the search goes the wrong way for
hours. Measured on the real one: two URLs were already failing before
anything had been migrated.

Asked in step 2, on the database before the bump — not on a bump
database that does not exist yet — and it says what it measures: a page
broken now will still be broken after, and that is not the migration.

--- FR ---

[ADD] migration : mesurer les URL publiques avant le premier palier aussi

Sans point de départ, une page qui rendait déjà 500 se lit comme un
dégât de la migration, et l'on cherche des heures du mauvais côté.
Mesuré sur la vraie : deux URL cassaient avant que quoi que ce soit
n'ait été migré.

Posée à l'étape 2, sur la base d'avant le palier — pas sur une base de
palier qui n'existe pas encore — et elle dit ce qu'elle mesure : une
page cassée maintenant le sera encore après, et ce ne sera pas la
migration.

Assisted-by: Claude Opus 5
2026-08-22 07:23:59 -04:00