Integrate claude, improve selenium and support qemu #1
Loading…
Reference in a new issue
No description provided.
Delete branch "develop"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Generalise the deployer beyond Ubuntu with a distro registry and a new --distro flag (ubuntu default, debian, fedora). Each distro keeps its own codenames, osinfo ids and per-version minimum RAM/disk. Image URLs are built per distro (Ubuntu current/, Debian latest/, Fedora resolved from the release index since it has no "latest" link). Add --list-images to print the whole catalogue with specs, exposed in the todo menu ("List available images and specs"); the deploy/download flows now prompt for the distro first. Resilient osinfo: when the local osinfo-db does not know an id (e.g. ubuntu26.04, fedora43+), fall back to virt-install detect=on,require=off instead of failing. Ubuntu 26.04 (resolute) is now enabled. Also: throttle the download progress to integer-percent steps (single updating line on a TTY, at most 101 lines when captured by the menu), and skip the spurious "network is already active" error by checking the libvirt network state before net-start. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Every menu now prints a breadcrumb line (e.g. "📍 TODO › Execute › Deploy › QEMU/KVM") right above "Command:", so it is always clear where you are and the path can be copied to describe a menu unambiguously. The trail is derived from the call stack via a method-name -> label map, so no menu method had to change: fill_help_info and the three inline menus just render self._menu_header() instead of t("Command:"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>fill_help_info() now renders {"section": "..."} entries as a header line without consuming a number, so numbering stays continuous over the real commands (the hardcoded elif chains keep matching). The Deploy menu is split into "Local", "SSH (remote host)" and "Virtualization & notifications", making the long SSH block easy to scan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Three papercuts hit when creating a VM from the menu: 1. Disk size "30" was passed verbatim to qemu-img resize, which read it as 30 BYTES and failed ("use --shrink"). The prompt now shows units (e.g. 30G, 1T) and a bare number is normalised to GB ("30" -> "30G"), in the menu and in deploy_qemu.py itself. 2. The VM name is no longer required: leaving it blank uses the default erplibre-<distro>-<version> (e.g. erplibre-ubuntu-2604). Distro and version are now asked first so the default can be offered. 3. The menu ignored the deploy exit code and carried on to "wait for the VM IP" even after a failure, hanging forever with the error scrolled off. It now checks the return code and stops with a clear message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Root cause of "git/make: command not found": the bootstrap ran "apt-get update -qq && apt-get install -y $PKGS". Inside an && list, set -e does NOT abort on failure, so when apt-get update failed (flaky VM network) the install was silently skipped and the script marched on without git/make. Now: "apt-get update || true; apt-get install -y $PKGS" (update best-effort, install mandatory), followed by an explicit "command -v curl git make" check that exits with a clear message ("Outil manquant ... (reseau de la VM ?)") instead of a cryptic failure later. Shared by the streamed and the monitored install paths. Dashboard: add "c" to copy the selected VM's full log to the clipboard (OSC 52, works over SSH) with a notification; the ssh bar documents Shift+drag for native terminal selection; on close, print a ready-to- copy "tail -n +1 <logdir>/*.log" to share the logs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Tested make install_os on a real Fedora 42 VM (exit 0: gcc 15, psql, node 22 installed). Two fixes from the run: - postgresql-setup --initdb failed with "invalid locale settings" because Fedora cloud images ship no LANG; force a valid locale via PGSETUP_INITDB_OPTIONS=--locale=C.UTF-8 and wipe a partial data dir first (a failed init leaves /var/lib/pgsql/data/log behind and blocks the retry). Verified: cluster PG 16 initialised, service active, erplibre superuser created. - the dev-tools group was referenced by display name ("C Development Tools and Libraries", "No match"); use the group id c-development. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The Arch install failed at once: "error: failed to synchronize all databases (unable to lock database)" then "reflector: command not found". Cause: cloud-init was still running its own pacman (installing qemu-guest-agent/openssh) and held /var/lib/pacman/db.lck when our bootstrap started, so every pacman call failed and reflector never got installed. Fix: the remote install now waits for cloud-init to finish first ("cloud-init status --wait", 15 min timeout) before touching any package manager — this releases the apt/dnf/pacman lock cloud-init holds during its package stage (helps all distros). For Arch, also drop a stale db.lck when no pacman is actually running. Verified live: cloud-init done, no lock, reflector + refresh + curl/git/make all succeed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Quand le téléchargement échoue sur tous les miroirs, le message indique désormais le CHEMIN de destination visé et une commande prête à copier pour reprendre manuellement : Destination : /var/lib/.../fedora-cloud-41-aarch64.qcow2 Reprendre le téléchargement manuellement : sudo curl -fL -C - -o <dest> \ <url> puis relancez le déploiement (l'image en cache sera réutilisée). curl -C - reprend un .part partiel (ou repart de zéro), -f échoue proprement sur une erreur HTTP. Complète la détection de téléchargement incomplet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Sur un gros parc (ex. 26 VM), on ne voyait ni combien de VM allaient être déployées ni lesquelles étaient en cours (résultats parallèles dans le désordre). Deux améliorations : - Le prompt de parallélisme affiche le NOMBRE de VM à déployer : « Déploiements en parallèle (défaut : 24, 26 VM) : ». Pour cela, la séparation à-créer / déjà-existantes est faite AVANT le prompt (on connaît le vrai compte). - Chaque VM reçoit un ID « k/N » suivant l'ordre de préparation, affiché dans les résultats de déploiement (« ✅ [3/26] nom ») et dans la résolution d'IP (« [5/30] nom : ip »). L'ID reste stable même si les résultats reviennent dans le désordre -> on suit lesquelles sont traitées. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>- F3 fait maintenant défiler Arbre -> Kanban -> Liste -> Arbre. Le résumé et le footer annoncent la PROCHAINE vue (« F3 → Liste »). - Nouvelle vue Liste : tous les menus empilés verticalement, chacun avec ses sections et ses commandes exécutables. - Les colonnes Kanban et la vue Liste affichent les SECTIONS (── … ──) pour guider le choix, et chaque commande porte son ICÔNE. L'icône vient de t() : l'AST extrait la clé i18n (prompt_description / prompt_description_key), et _disp() la résout en libellé traduit + icône. Les sections sont capturées via _choice_entries (marqueurs {"section": …}). Validé headless : cycle F3, 11 en-têtes de menu, sections en Liste/Kanban. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Trois améliorations QEMU : - Extension du FS invité robuste : résolution d'IP avec BATTEMENT (_qemu_resolve_ips, parallèle, boot émulé lent) au lieu d'un timeout court ; en cas d'absence d'IP ou d'échec SSH, repli sur la CONSOLE SÉRIE avec la commande growpart/resize prête à coller (login erplibre/erplibre). Commande factorisée dans _GROW_FS_REMOTE. - Temps estimés PAR VERSION : nouveau mon.avg_by_version(distro, version) et _qemu_stat_avg("version", v, distro) -> suffixe « · ~46s moy (1) » dans les listes de versions (prompt simple + granulaire). - i18n : « Versions for » n'avait AUCUNE traduction (restait en anglais). Ajout FR « Versions des » (+ « Version des ») et capitalisation de la distro -> « Versions des Ubuntu : ». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Suivi d'installation (qemu_install_monitor.py) : - Table VM/État LISIBLE : largeurs de colonnes fixes (VM 26, ⚠ 4, État 12, Durée 7, Disque 8) -> l'État n'est plus tronqué à 3-4 caractères (« ❌ effacée », « ⏸ en pause » lisibles) ; table défilable (overflow-x, height 1fr) pour les longs noms et les gros parcs. - « w » web : offre désormais la LISTE des navigateurs CLI installés (_choose_browser) pour choisir lequel utiliser, + option [i] installer. Déploiement (deploy_qemu.py) : - guest-exec AUTORISÉ : on vide la liste de blocage de qemu-ga (block-rpcs/blacklist vides — pas allow-rpcs qui est une liste BLANCHE et casserait les autres RPC), + neutralise /etc/sysconfig/qemu-ga (Fedora). Installation (todo.py) : - Profils AVEC Odoo (install_odoo*) UNIQUEMENT : Odoo est enregistré comme service systemd (erplibre.service, inspiré de script/systemd/ install_daemon.sh) puis enable --now. Pas pour ERPLibre seul / mobile / Déploiement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Sur les images cloud, /var/lib/apt/lists est vide (package_update désactivé) -> « Unable to locate package qemu-guest-agent » et agent inactif. On rafraîchit l'index (timeout 120, || true) AVANT l'install dans le runcmd (après sshd, sans bloquer SSH). Idem pacman -Sy. Sous-shell ( ) et non accolades { } (indicateur YAML). Validé sur une VM de test Ubuntu 24.04 (redéployée) : - cloud-init status: done ; user erplibre créé + mot de passe (login console erplibre/erplibre OK) ; - qemu-guest-agent: active ; - guest-ping OK ; guest-exec autorisé (echo -> code 0, tourne en root). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Si le dashboard se ferme (bug, sortie), on peut désormais le ROUVRIR sur un run passé pour reprendre l'analyse. Nouvelle entrée [11] du menu QEMU : « 📈 Rouvrir le suivi d'installation (dernier run / historique) ». - mon.list_install_runs() : liste les runs (~/.erplibre/qemu-install/*/ session.json) triés du plus récent au plus ancien. - _qemu_reopen_monitor : affiche l'historique (VM par run), choix (défaut = le dernier), puis run_monitor(session.json) rouvre le dashboard. - « Lister les images » passe de [11] à [12] ; config -> 13+. Validé : 20 runs listés, dashboard reconstruit (titre + 5 VM) depuis un session.json passé. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>4f0c3cd) fd3226a053La colonne « État » réservait 12 caractères alors que le plus long libellé (« ❌ effacée ») en fait 10 -> elle paraissait trop large. Ramenée à 10 (sans troncature). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>- État : 10 -> 8. Le libellé « ❌ effacée » (10 cellules) forçait la largeur ; l'état « effacée » devient l'icône seule 🗑. Reste le plus long « ⏸ pause » (7), donc 8 suffit. - VM : 26 -> 22 (tient les noms standards « erplibre-ubuntu-2604 » = 20 ; les rares noms suffixés d'arch défilent via overflow-x). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Après « ✅ -> Neutralize database », on demande désormais si on veut POURSUIVRE la mise à jour maintenant ou ATTENDRE (défaut : poursuivre). Si on attend (« n »), on s'arrête proprement : l'état restore+neutralize est déjà sauvegardé, donc relancer « Migration de base de données » reprend à ce point sans refaire ces étapes. Permet d'inspecter/sauvegarder la base neutralisée avant de lancer la migration (uninstall/install/upgrade). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>L'entrée « Update - Update all developed staging source code » quitte le menu Execute (renumérotation 9->8 … 15->14, dispatch elif mis à jour) et rejoint le sous-menu « Code - Outil pour développeur » (dernière entrée, appelle prompt_execute_update). Le libellé/i18n (🔃) est réutilisé. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>4dd57e4c50todb1e9fa24fA leftover git daemon from an interrupted run kept port 9418, so the new daemon failed to bind ("Address already in use") and the final « kill $DAEMON_PID » failed on the already-dead PID, returning exit 1. That non-zero status bubbled up through apply_extra_modules() and set exit_code=1, which silently skipped the post-install steps (add_extra_to_config_conf, generate_config) -> CybroOdoo never landed in config.conf. - Kill any leftover daemon before starting a new one. Match the stable arguments, not "git daemon": « git daemon » execs into « git-daemon » (hyphen, /usr/lib/git-core/git-daemon), so a "git daemon" (space) pattern never matched the actual process. - Move daemon cleanup into an EXIT trap that tolerates an already-dead PID, so the script never exits non-zero just because the daemon is already gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>key is the only thing pairing a website copy with the module view it came from. A copy whose key matches nothing is never paired, so it never receives the new inherit_id, never changes shape, and never joins a view combination. Renaming the key is therefore enough to take it out of the way: UPDATE ir_ui_view SET key = '<prefix>.' || key, active = false active = false alone would NOT work: an inactive copy keeping the same key still shadows the module view. Nothing is deleted either, so inherit_id ondelete='restrict' and the website_page foreign keys are never touched, and the old arch stays in database as a readable archive. --restore undoes it. Written in plain psql on purpose: it runs on a database not yet migrated, where starting an Odoo shell of the target version is not guaranteed. It is a systematic step driven by the detector, valid for every bump, so it lives in the loop rather than in a per-version fix_migration file. Offered rather than forced, since those copies carry real customizations. Verified on a throwaway copy: dry-run writes nothing; apply renames the key and deactivates while keeping the 4058-char arch; a second apply is a no-op; restore returns the row to its exact initial state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Both prompts expected a raw number, so common values had to be retyped every time. They now list suggestions: [a] 20G [b] 40G [c] 60G [d] 80G [e] 120G [f] 200G New disk size in G, blank = keep (20G): Letters start at « a » so they can never collide with a value typed directly: anything starting with a digit is read as the value itself, and blank still keeps the current one. A letter outside the range is rejected instead of being silently taken for a size. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The migration now opens with the same choice the QEMU deployment does — TUI or line-by-line prompts — settled in advance by TODO > Configuration if you want it to be. Choosing the TUI loads the saved progression and asks the very same first question: where does this migration stand, and where do we resume? prompt_resume was one function that rendered, read and decided at once, so a second view would have meant a second copy of the decision. It is now three: resume_context() the progression -> plain data (file, database, target, steps with their icon and detail, version bumps) print_resume() renders it on the terminal apply_resume_answer() answer -> (progression, changed) The TUI returns the SAME answer strings as the prompt — c, n, r, q, 0..4, 4.<version> — so apply_resume_answer stays the only place that decides what a choice means. Neither view can drift into describing the migration differently, because both render the same context. In the TUI the steps are a table and the version bumps a list: Enter on either replays from there, which is what « [0-4] » and « [4.N] » meant in text. The cursor opens on the first unfinished step and on the first unmigrated version — where it stopped is where one usually wants to act. Both views gain « q », quit without doing anything. A TUI needs Escape to do something sane, and an escape hatch present in only one of the two views is exactly the kind of divergence this split exists to prevent. execute_odoo_ upgrade returns immediately on it, writing nothing. Verified on a 12->18 progression with steps 0-3 done and 2 of 6 bumps migrated: identical context feeding both views, Enter on step 2 giving « 2 », Enter on the 15 bump giving « 4.15 », the c/n/r/q/Escape shortcuts, and every answer producing the same progression through both paths — « 2 » leaving only the state of steps 0 and 1, « 4.15 » resetting the clone list from the third bump on so the half-migrated intermediate database gets rebuilt. « q » checked to leave the progression file byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Every session paid ~4 000 tokens for CLAUDE.md and .claude/rules/, and part of that content no longer described this repository: CLAUDE.md and 05-environments.md gave .venv.odoo18/bin/python — a path that does not exist. The real one carries BOTH versions, .venv.odoo18.0_python3.12.10. 02-project-structure.md announced addons/ and odoo12.0/, absent from the checkout. That is the reason for the cuts, more than the token count: the files drifted from the repository, while « ls » cannot. What a session can rebuild by reading the code is now left to the code. 02-project-structure.md deleted — a tree that ls gives, and gives right 05-environments.md deleted — a venv list that was simply false 04-code-conventions.md reduced to a pointer at .flake8, .editorconfig and pyproject.toml, which already hold every value it repeated, plus the Git conventions, which they do not 01-versions.md table dropped for the pointer at conf/supported_version_erplibre.json, whose keys already pair Odoo with Python Two blocks move to lazy loading — their body costs nothing until invoked: 03-commands.md -> skill erplibre-commands (kept whole: there is no « make help » target, and the per-module test recipes carry flags nobody would guess) 07-documentation.md -> skill erplibre-doc-i18n for the how-to, while the prohibition « never edit a generated .md » STAYS in the rules: a rule that must hold at all times cannot live in a file loaded on demand Resident guidance: ~3 997 -> ~1 939 est. tokens per session. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The interface choice offered two ways to ACT on a migration and none to just look at one. Entry [3] reads the progression file and the traces left under private/, writes nothing, and can be opened as often as one likes — [0] there returns to the interface choice, and a new [0] Cancel leaves the tool entirely instead of forcing a migration to be started. « stats » is deliberately not storable as a default: it does nothing, so landing on it every time would only be in the way. The dashboard answers what was asked — which modules were removed, and how far between Odoo versions — plus what the recorded data already knew and nobody was showing: · elapsed time since the migration started · module count per version, with the delta per bump — a jump losing 15 modules does not mean the same thing as one losing none · modules reported missing or duplicated · which fix hooks exist and which have run · COW snapshots taken, with their view count over time · how many commands ran, and the annotated decisions among them Then, on demand: the removed modules with their justification, the same list comma-separated to paste, a diff between any two COW snapshots (through the existing snapshot_cow_views.py --diff), the decisions, and the last commands. Computation lives in migration_stats.py as pure functions fed by resume_context() — the same context the resume screen and its TUI already render, so the step and version-bump state can never be described two different ways. read_uninstall is INJECTED rather than reimplemented: it resolves private-then-global itself, so the screen shows exactly the list that would be applied. Verified on a realistic progression: 1 j 03 h elapsed, 312 -> 297 -> 282 modules (-15 each), 5 removals across two bumps with their reasons, 4 COW snapshots ordered by time, the fix applied for 14.0 and pending for 15.0, and the progression dict byte-identical afterwards. The menu was checked to map '' /1/2/3/0/9 to tui/tui/cli/stats/None/tui, and [0] to leave without asking anything else. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Third COW incident of this migration, and the first two tools did not cover it. A copy freezes the module view it came from; version after version the MODULE view is modernised and the copy is not, until a child xpath lands on an anchor the copy never had: Element '<xpath expr="//head/script[@id='web.layout.odooscript']">' cannot be located in parent view On 14.0 -> 15.0 that was web.layout, copied in the 12.0 era: no id on the script tag, QWeb still saying t-raw. It had survived only because the 14.0 module xpath carried a fallback — //head/script[@id='…'] | //head/script [last()] — that 15.0 removed. The breakage was years old; 15.0 merely stopped hiding it. The test is DIFFERENTIAL, and it has to be. Resolving a child's xpath against its parent's own arch proves nothing: Odoo resolves against the COMBINED arch of the whole chain, so a child of website.layout legitimately targets //header coming from an ancestor. Measured on the real database, that naive rule reported 5 copies where only 1 had a problem. Resolving twice — against the module twin and against the copy — and keeping only what the twin satisfies and the copy cannot, isolates drift and nothing else. Two accuracy fixes the real data forced: · inactive children are skipped; Odoo never applies them · a missing lxml is now a LOUD failure. The first version fell back to « everything resolves », so the checker answered « all clean » while checking nothing — the worst possible outcome for a checker. Verified: the bare system python3 has no lxml, .venv.erplibre does. --reset copies the module arch over the copy, after saving the previous arch to a timestamped file and printing the diff, because a copy can hold a real customisation buried in the drift — on web.layout it was one <meta viewport> among six differences. Timing matters: drift only exists once the module views carry the new version, so run this AFTER OpenUpgrade and BEFORE update_addons_all. Run on the 14.0 database it finds nothing about web.layout, and rightly so — there the module view says t-raw too. Verified end to end on a scratch database rebuilt from the real arches: detection of the exact reported failure, dry-run changing nothing, --apply resetting it, the backup holding the previous arch, and a clean re-check. Then across the four migration databases, where it flags two latent drifts (website_sale.product, website_blog.blog_post_complete) that no version bump has surfaced yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Reaching a VM's Odoo from the workstation browser meant remembering the -L syntax and which side « localhost » refers to. Entry [3] of the Deploy menu asks for a host, a remote port (8069 by default) and a local one, then holds the tunnel open. Nothing has to be said about jumps: the ProxyJump already in ~/.ssh/config applies on its own, which is what makes a NESTED VM reachable — its address means nothing from here, only from its parent. Two guards, both from getting it wrong by hand: · a local port already in use is reported before ssh fails on it; · a local port that differs from the remote one gets a warning, because Odoo redirects using web.base.url and would send the browser to an address that does not exist locally. With matching ports and the usual web.base.url = http://localhost:8069, there is nothing to adjust. The host list is read from ~/.ssh/config by a small shared helper: it expands a Host line carrying several names and drops the wildcard patterns, which are rules rather than machines. Verified against a config holding « Host * », a plain host and a two-name line: the three real names listed in order, selection by number and by name, 8069/8069 by default, 9072:localhost:8072 warning about web.base.url, matching ports staying silent, an empty host cancelling without running anything, and the busy-port probe answering correctly on a socket bound then released. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>A stopped service on the far side showed up only as a wall of « channel N: open failed: connect failed: Connection refused », one line per browser request, saying nothing about WHICH end refused. The tunnel itself was fine; there was simply nothing to reach. Diagnosing it took three commands. The port is now probed first, and the answer is plain: ⚠ Rien n'écoute sur le port 8069 de test-vm_02+erplibre-ubuntu-2404 Démarrer le service là-bas, ou continuer quand même. Continuer quand même ? (o/N) The probe opens a real TCP connection to « localhost:<port> » from the remote host rather than reading its listening table. That is exactly what the tunnel will do — same host resolution, same IPv4/IPv6 choice — so it cannot say open where the tunnel would fail. It also needs no ss or netstat, which minimal images lack. Three outcomes, three behaviours: listening goes straight through, closed warns and asks (default no), and an inconclusive probe — unreachable host, no bash — says so and continues rather than blocking on its own uncertainty. Verified against the real VM: port 22 open, port 9999 closed, an unknown host inconclusive, and port 8069 correctly reported closed after the Odoo service had been stopped — the very case that prompted this. Then at flow level: refusing aborts without opening anything, forcing opens the tunnel anyway, and an inconclusive probe still opens it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The two entries — « local VMs » and « nested, recursive » — wrote the same thing and differed only by how far they walked. That is a question, not a menu, so there is now a single flow and the depth is asked: Profondeur (1 = ces machines seulement, défaut : 2) Depth counts LEVELS of machines including the roots, so 1 writes the roots and probes nothing. The old code always probed, which is why « local VMs only » needed to be a separate entry at all. The first question is where to look, because the machine to configure is not always a VM of this host: [1] Local QEMU VMs (virsh) the previous behaviour [2] Hosts from ~/.ssh/config pick among what is already known; each is probed over SSH and only those actually running QEMU get their guests configured [3] Type a host or an IP a raw IP gets an alias, without which neither the children's ProxyJump nor virt-manager would have a name to use A root taken from ~/.ssh/config carries no IP: its address is already in the file, so nothing is rewritten — the walk simply starts from it. That is the single change that let the two flows merge. Verified on a config holding a wildcard, a bastion and a hypervisor: local VMs reaching their nested guest, ssh-config hosts skipping the bastion (no QEMU) while configuring the hypervisor's guest and offering only the hypervisor to virt-manager, a raw IP producing qemu-10-9-9-9 and its child, depth 1 probing nothing, and [0] asking nothing further. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Adopting a host from ~/.ssh/config only to write « User erplibre » under its guests defeats the point: those hosts are not necessarily ERPLibre VMs, and a guest follows its parent's convention because the parent created it. The declared User is now read and propagated — to the guest entries, to the summary line, and to the qemu+ssh URI handed to virt-manager. Only when nothing is declared does QEMU_VM_USER apply, the cloud-init account of VMs deployed from here, now a named constant instead of a literal repeated at each call site. Resolution follows OpenSSH: the FIRST User among the matching blocks wins, wildcards included. That is the opposite of what one expects, so it was checked against ssh -G rather than the manual: with « Host * / User global » placed first, ssh really does report global even for a host that declares its own — which is why the manual tells you to put « Host * » last. The implementation matches on both layouts. « user@host » is also accepted when typing a raw address, and otherwise the account is asked with QEMU_VM_USER as default. ssh-copy-id needed no change: it is given the alias, whose block now carries the right User. Verified on a config with a per-host User, a host without one and a trailing « Host * »: the three resolutions correct, the guest written with mathben and not erplibre, and virt-manager offered ('hyperviseur', 'mathben'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>« Installez textual pour le TUI (pip) » left the user to work out which pip, which interpreter, which package — and it was printed from eight different places. Every TUI screen now asks: ⚠ Textual est nécessaire pour cet écran. L'installer maintenant ? (O/n, défaut : oui) The command targets sys.executable, the interpreter that will have to import it — installing a distribution package would land somewhere the venv never looks. Outside a venv it adds --user, which is also what gets past the refusal of distributions whose environment is externally managed (PEP 668). Two details that decide whether this works at all: · importlib.invalidate_caches() after installing. A failed import is remembered, so without it textual stays « missing » for the rest of the session despite having just been installed. · a pip that exits non-zero never reports success. The check is « is it importable NOW », not « did pip return 0 », and the failure suggests the distribution package by name. It lives in its own module rather than as a TODO method: todo_upgrade needs it too and is imported BY todo, so putting it there would close a cycle. One call site is deliberately NOT converted. The statistics screen only reads files; it never touches Textual, and its old message claimed otherwise. An import failure there is a real module problem and now says so. Verified: already-present asks nothing and runs nothing; refusal installs nothing; pip failing returns False and points at python3-textual; pip succeeding returns True; prompt=False reports without asking. Then each of the four TUI entries — telemetry, deploy form, deploy progress, migration resume — offers and falls back cleanly on refusal, while the statistics screen stays silent. Also caught by those tests: « import importlib » alone does not expose importlib.util, so availability could not be checked at all. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>« Créer la clé SSH si absente et la déployer ? » was asked before anything had been attempted — so before knowing whether a key was needed at all. On a fleet already reachable it was pure noise, and answering no left every later failure reported as a flat « injoignable ». The question now appears where the problem does: 🔒 hote: SSH refused the identity. Permission denied (publickey). Aucune clé SSH dans ~/.ssh. En créer une et la déployer ? (O/n) and the host is probed again straight after, so the walk carries on into its guests instead of stopping. That required telling a refused identity from an unreachable host, which the probe could not do: both returned None. It now reports which — auth or net — by matching ssh's own wording (permission denied, too many authentication failures, no such identity, host key verification failed). An unreachable host never triggers the key question, because a key would not help it, and the real ssh message is printed either way rather than a generic label. A key created mid-walk is picked up by the entries written afterwards, so their IdentityFile names it. Verified against ssh's actual messages: the eight classified correctly, including « Identity file not accessible » which is a warning about a missing file, not a refusal. Then the flow: a fleet that answers straight away is never asked about keys at all, a refusal asks and — once accepted — creates, deploys, re-probes and configures the guest, declining reports « accès refusé » without deploying, and an unreachable host says so without mentioning identity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>« db --list » rendait un code de retour que personne ne lisait. Comme la sortie et l'erreur passent par le même flux (stderr=STDOUT, execute.py), les lignes de la trace d'appel devenaient les entrées du menu : [1] Traceback (most recent call last): [2] File "…/odoo-bin", line 8, in <module> [0] Retour Choisir [1] renvoyait « Traceback (most recent call last): » comme nom de base à l'appelant, qui le passait à sa commande — sauvegarde, restauration, shell. Le code de retour était pourtant là, 120, dans la variable « status », écrasée deux lignes plus bas par la réponse de click.prompt. Le contrôle se fait donc AVANT de construire le menu : code non nul, on affiche les dernières lignes de l'erreur réelle et on rend False. Une liste vide dit aussi ce qu'elle est, au lieu d'un écran ne portant que « Retour ». Au passage, la branche de saisie invalide appelait t("cmd_not_found") — une clé absente de TRANSLATIONS, donc affichée telle quelle, en anglais technique. Les autres menus disent t("Command not found !") ; celui-ci le dit maintenant aussi. « quiet=True » s'ajoute parce que la sortie brute n'a plus de raison d'être vue : le menu affiche la liste, et en cas d'échec c'est l'erreur qui est montrée. Vérifié sur les quatre chemins, PGPORT=1 pour simuler la panne : injoignable rend False sans afficher un seul menu ; [0] rend False ; [1] rend « test » ; une saisie invalide affiche un message traduit dans les deux langues. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.