From 8a946a0b89b760dcfd49699ab478be4fa0fa316a Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 13 Aug 2026 01:15:13 -0400 Subject: [PATCH] [ADD] tui qemu: an Odoo version per VM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A profile list per row, beside the branch: "ERPLibre + Odoo 18", "Odoo 17", "ERPLibre alone". The form's profile stays the default; picking another for a VM creates an override, going back clears it. It reaches down to execution, like the branch and the type before it. "final_cmd" now takes either a string for the whole fleet or a {name: command} map, and the remote command is built per machine as soon as profiles differ. A VM can install Odoo 18 while another validates Odoo 17, in the same deployment. Both lists are widened -- branch to 24 columns, profile to 28. Truncated, "develop" and "ERPLibre + Odoo 18" no longer showed what had been picked, and that is precisely what one wants to re-read before deploying. Two more defects: the global branch and Odoo choices reached nothing at all, and the summary announced the global profile for every VM, then named a version nothing would install. --- FR --- Une liste de profils par rangée, à côté de la branche : « ERPLibre + Odoo 18 », « Odoo 17 », « ERPLibre seul ». Le profil du formulaire reste le défaut ; en choisir un autre pour une VM crée une surcharge, y revenir l'efface. Il descend jusqu'à l'exécution, comme la branche et le type avant lui. « final_cmd » accepte maintenant une chaîne pour tout le parc ou une carte {nom: commande}, et la commande distante est bâtie par machine dès que les profils diffèrent. Une VM peut donc installer Odoo 18 pendant qu'une autre valide Odoo 17, dans le même déploiement. Les deux listes sont élargies : la branche passe à 24 colonnes, le profil à 28. Tronqués, « develop » et « ERPLibre + Odoo 18 » ne laissaient plus voir ce qu'on avait choisi — et c'est précisément ce qu'on veut relire avant de déployer. Deux autres défauts : les choix globaux de branche et d'Odoo n'atteignaient rien, et le sommaire annonçait le profil global pour toutes les VM, puis nommait une version que rien n'installait. Assisted-by: Claude Opus 5 --- script/todo/qemu_deploy_form.py | 81 +++++++++++++++++++++++++++++++-- script/todo/todo.py | 59 +++++++++++++++++++++--- 2 files changed, 131 insertions(+), 9 deletions(-) diff --git a/script/todo/qemu_deploy_form.py b/script/todo/qemu_deploy_form.py index 1f1f83e..04a787d 100644 --- a/script/todo/qemu_deploy_form.py +++ b/script/todo/qemu_deploy_form.py @@ -169,6 +169,12 @@ def apply_profile( # Branche ERPLibre. Même raison : « » signifie « celle du # formulaire », et une surcharge la remplace pour cette VM. "branch": "", + # Profil d'installation (« ERPLibre + Odoo 18 »). Même + # convention : « » = celui du formulaire. + "install_cmd": "", + # Libelle du profil, pour que le recapitulatif puisse dire + # « Odoo 18 » sans connaitre la liste des profils. + "install_label": "", } ) return out @@ -449,7 +455,10 @@ def run_deploy_form(ctx, run_app: bool = True): #body { height: 1fr; } #fields { width: 62; border: solid $accent; overflow-y: auto; } #right { width: 1fr; } - #plan { height: 1fr; border: solid $accent; } + #plan { + height: 1fr; border: solid $accent; + overflow-x: auto; scrollbar-size-horizontal: 1; + } #totals { height: auto; color: $text-muted; padding: 0 1; } .grouptitle { color: $accent; text-style: bold; padding: 1 0 0 0; } SelectionList { height: 10; border: solid $panel; } @@ -473,12 +482,22 @@ def run_deploy_form(ctx, run_app: bool = True): /* La branche porte des noms longs (« 1.6.0 », « develop », « feature/xyz ») : trop étroite, la liste les tronque et on ne sait plus ce qu'on a choisi. */ - .vmbranch { width: 18; } + + .vmcopy { width: 5; min-width: 5; } .vmhead { height: 1; } - .vmrow { height: 3; align-vertical: middle; } + /* « width: auto » et le défilement du plan : sans eux, une rangée + plus large que le panneau est COUPÉE au lieu d'être atteignable. */ + .vmrow { height: 3; width: auto; align-vertical: middle; } .vmrow Select { width: 15; } .vmrow Input { width: 11; } + /* Ces deux règles portent « .vmrow Select » EN PLUS de leur classe : + « .vmrow Select » (une classe + un type) l'emporte sur « .vmbranch » + (une classe) par spécificité CSS. Écrites simplement, elles étaient + silencieusement écrasées à 15 — et le test, qui ne vérifiait que la + présence de la classe, passait sans rien prouver. */ + .vmrow Select.vmbranch { width: 34; } + .vmrow Select.vmprof { width: 40; } #reslabel { color: $text-muted; } RenameScreen { align: center middle; } #renbox { @@ -864,6 +883,24 @@ def run_deploy_form(ctx, run_app: bool = True): def _mise_usable(self): return any(vm["arch"] in mise_arches for vm in self.vms) + def _profile_cmd(self): + """Commande du profil choisi en haut : le défaut de chaque VM.""" + if not profiles: + return "" + index = self.query_one("#f_profile_install", Select).value + return profiles[index if isinstance(index, int) else 0][1] + + def _row_profile_index(self, i): + """Rang du profil que la rangée doit AFFICHER.""" + cmd = "" + if i < len(self.rows): + cmd = self.rows[i]["vm"].get("install_cmd") or "" + cmd = cmd or self._profile_cmd() + for k, (_lbl, c) in enumerate(profiles): + if c == cmd: + return k + return 0 + def _branch(self): """Branche du formulaire : le défaut de chaque VM.""" value = self.query_one("#f_branch", Select).value @@ -1005,6 +1042,17 @@ def run_deploy_form(ctx, run_app: bool = True): allow_blank=False, id=f"v{i}_branch", ), + ( + Select( + [(lbl, i) for i, (lbl, _c) in enumerate(profiles)], + value=self._row_profile_index(i), + allow_blank=False, + classes="vmprof", + id=f"v{i}_prof", + ) + if profiles + else Static("", classes="vmprof") + ), Select( self._type_options(), value=vm.get("desktop") or SERVER, @@ -1093,6 +1141,7 @@ def run_deploy_form(ctx, run_app: bool = True): for wid, value in ( (f"#v{i}_type", vm.get("desktop") or SERVER), (f"#v{i}_branch", vm.get("branch") or self._branch()), + (f"#v{i}_prof", self._row_profile_index(i)), ): try: self.query_one(wid, Select).value = value @@ -1262,6 +1311,9 @@ def run_deploy_form(ctx, run_app: bool = True): continue for field in fields: changed |= self.overrides[key].pop(field, None) is not None + if field == "install_cmd": + # Le libelle n'a pas de sens sans sa commande. + self.overrides[key].pop("install_label", None) if not self.overrides[key]: self.overrides.pop(key, None) if changed: @@ -1336,6 +1388,21 @@ def run_deploy_form(ctx, run_app: bool = True): # suivra donc le profil s'il change — ce qui est aussi le plus # attendu quand on n'a rien changé de visible. vm_now = self.rows[index]["vm"] + if field == "prof": + label, cmd = profiles[event.value] + if cmd == ( + vm_now.get("install_cmd") or self._profile_cmd() + ): + return + same = cmd == self._profile_cmd() + self._set_override( + index, "install_cmd", "" if same else cmd + ) + self._set_override( + index, "install_label", "" if same else label + ) + self._recompute() + return if field == "branch": # « la branche du formulaire » n'est pas une surcharge : # la VM doit suivre si on la change en haut. @@ -1472,6 +1539,14 @@ def run_deploy_form(ctx, run_app: bool = True): "install_cmd": ( vm.get("install_cmd") or self._profile_cmd() ), + "install_label": ( + vm.get("install_label") + or ( + profiles[self._row_profile_index(index)][0] + if profiles + else "" + ) + ), } else: self.locked.discard(key) diff --git a/script/todo/todo.py b/script/todo/todo.py index c706508..7e6528a 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -4902,9 +4902,13 @@ class TODO: # une carte {nom: branche} quand elles diffèrent d'une VM à l'autre. branch_map = branch if isinstance(branch, dict) else {} branch_def = "" if branch_map else branch + # Idem pour le profil : « ERPLibre + Odoo 18 » peut differer d'une + # machine a l'autre, on valide alors deux versions d'un coup. + cmd_map = final_cmd if isinstance(final_cmd, dict) else {} + cmd_def = None if cmd_map else final_cmd remote = self._qemu_erplibre_remote_cmd( branch_def, - final_cmd, + cmd_def, prod, "" if desk_map else desktop, python_provider, @@ -4932,10 +4936,10 @@ class TODO: "version": v, "arch": a, } - if desk_map or branch_map: + if desk_map or branch_map or cmd_map: entry["remote_cmd"] = self._qemu_erplibre_remote_cmd( branch_map.get(name, branch_def), - final_cmd, + cmd_map.get(name, cmd_def), prod, desk_map.get(name, ""), python_provider, @@ -5361,10 +5365,33 @@ class TODO: gigs = self._parse_disk_gb(vm["disk"]) + ( self.ERPLIBRE_EXTRA_DISK_GB if branch else 0 ) + # Ce qui S'ECARTE du choix commun se dit sur la ligne de la VM. + # Sans cela le sommaire annoncait le profil general pour tout le + # monde, y compris pour une VM figee sur un autre — on lisait + # « Odoo 15 » avant de deployer une machine en Odoo 18. + apart = [] + # Seul ce qui DIFFERE vaut d'etre signale : une VM figee sur la + # meme branche que le global n'a rien de particulier a montrer, + # et repeter la valeur commune sur chaque ligne la noierait. + if install and vm.get("branch") and vm["branch"] != branch: + apart.append(vm["branch"]) + if ( + vm.get("install_label") + and install + and vm.get("install_cmd") != install.get("cmd") + ): + apart.append(vm["install_label"]) + if vm.get("desktop") and vm["desktop"] != spec.get("desktop"): + apart.append( + (self._QEMU_DESKTOP.get(vm["desktop"]) or {}).get( + "label", vm["desktop"] + ) + ) print( f" {vm['name']:<30} {vm['distro']} {vm['version']:<7} " f"[{vm['arch']:<5}] {vm['vcpus']} vCPU RAM {vm['ram']}Mo " f"{t('disk')} {gigs}G" + + (f" ⟵ {' · '.join(apart)}" if apart else "") ) if existing: print(f" {t('Existing, left untouched:')} {', '.join(existing)}") @@ -5374,9 +5401,22 @@ class TODO: if install["prod"] else t("development (~/git)") ) + # « par defaut » : chaque VM peut s'en ecarter, et sa ligne le dit. + # Ce qui sera REELLEMENT pose, pas le defaut du formulaire. Avec + # une seule VM figee sur un autre profil, annoncer le defaut + # revenait a nommer une version que rien n'installe — c'est + # exactement ce qu'on relit ici pour eviter de se tromper. + used_br = {vm.get("branch") or branch for vm in spec["vms"]} + used_lb = { + vm.get("install_label") or install["label"] + for vm in spec["vms"] + } + varies = t("varies, see each line") + br_txt = used_br.pop() if len(used_br) == 1 else varies + lb_txt = used_lb.pop() if len(used_lb) == 1 else varies print( - f" {t('ERPLibre install:')} {t('branch')} {branch}, " - f"{t('profile')} {install['label']}, {env}" + f" {t('ERPLibre install:')} {t('branch')} {br_txt}, " + f"{t('profile')} {lb_txt}, {env}" ) else: print(f" {t('ERPLibre install:')} {t('no')}") @@ -6305,6 +6345,13 @@ class TODO: for _n in deployed: branch_map.setdefault(_n, install_branch or "") branch_multi = len(set(branch_map.values())) > 1 + base_cmd = install["cmd"] if install else None + cmd_map = { + vm["name"]: (vm.get("install_cmd") or base_cmd) for vm in pending + } + for _n in deployed: + cmd_map.setdefault(_n, base_cmd) + cmd_multi = len(set(cmd_map.values())) > 1 ssh_key = spec.get("ssh_key") add_ssh_config = spec["add_ssh_config"] parallelism = spec["parallelism"] @@ -6376,7 +6423,7 @@ class TODO: deployed, branch_map if branch_multi else install_branch, ip_map, - install["cmd"] if install else None, + cmd_map if cmd_multi else base_cmd, install["prod"] if install else False, desktop=desktop_map, python_provider=python_provider,