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,