[ADD] tui qemu: an ERPLibre version per VM

A branch list per row. The form's branch stays the default; picking
another for a VM creates an override, going back clears it -- the VM then
follows the form again, which is what one expects when restoring the
common choice.

It reaches all the way down to execution, like VM type before it.
"branch" now takes either a string for the whole fleet or a {name: branch}
map, and the remote command is built per machine as soon as branches
differ. The ground was ready: launch_installs already knows how to take
one command per VM.

A VM can therefore carry develop while another validates 1.6.0, in the
same deployment and the same monitoring table.

One trap: a row's branch fell back to develop on remount, the list being
rebuilt from the form rather than from the override it already held.

--- FR ---

Une liste de branches par rangée. La branche du formulaire reste le
défaut ; en choisir une autre pour une VM crée une surcharge, y revenir
l'efface — la VM suit alors de nouveau le formulaire, ce qui est le plus
attendu quand on remet le choix commun.

Elle descend jusqu'à l'exécution, comme le type de VM avant elle.
« branch » accepte désormais une chaîne pour tout le parc ou une carte
{nom: branche}, et la commande distante est bâtie par machine dès que les
branches diffèrent. Le terrain était prêt : launch_installs sait déjà
prendre une commande par VM.

Une VM peut donc porter develop pendant qu'une autre valide 1.6.0, sur le
même déploiement et dans le même tableau de suivi.

Un piège : la branche d'une rangée retombait sur develop au remontage, la
liste étant rebâtie depuis le formulaire plutôt que depuis la surcharge
qu'elle portait déjà.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-13 00:46:12 -04:00
parent 17d0d60741
commit 2eee924759
2 changed files with 50 additions and 9 deletions

View file

@ -166,6 +166,9 @@ def apply_profile(
# spec entière depuis qu'il se choisit machine par machine ;
# `desktop` n'est plus que le défaut commun.
"desktop": desktop,
# Branche ERPLibre. Même raison : « » signifie « celle du
# formulaire », et une surcharge la remplace pour cette VM.
"branch": "",
}
)
return out
@ -861,6 +864,11 @@ 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 _branch(self):
"""Branche du formulaire : le défaut de chaque VM."""
value = self.query_one("#f_branch", Select).value
return value if isinstance(value, str) else branches[0]
def _default_desktop(self):
"""« » pour un serveur, sinon la clé de la saveur choisie."""
"""Type de VM par défaut. Chaque rangée peut s'en écarter."""
@ -983,11 +991,17 @@ def run_deploy_form(ctx, run_app: bool = True):
Select(
[(b, b) for b in branches],
classes="vmbranch",
# Repli sur la branche du FORMULAIRE, jamais sur
# branches[0] : les rangées sont remontées dès que le
# jeu de VM change (une entrée cochée, une copie
# ajoutée, un renommage), et elles retombaient alors
# toutes sur « develop » quel que soit le choix commun.
value=(
item.get("branch")
or self.rows[i]["vm"].get("branch")
or branches[0]
),
self.rows[i]["vm"].get("branch")
if i < len(self.rows)
else ""
)
or self._branch(),
allow_blank=False,
id=f"v{i}_branch",
),
@ -1322,6 +1336,19 @@ 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 == "branch":
# « la branche du formulaire » n'est pas une surcharge :
# la VM doit suivre si on la change en haut.
current = vm_now.get("branch") or self._branch()
if event.value == current:
return
self._set_override(
index,
"branch",
"" if event.value == self._branch() else event.value,
)
self._recompute()
return
if field == "type":
new_desk = "" if event.value == SERVER else event.value
if new_desk == (vm_now.get("desktop") or ""):

View file

@ -4898,8 +4898,12 @@ class TODO:
# commande distante en dépend, donc elle se construit par VM ; celle-ci
# reste le défaut pour les noms absents du dict.
desk_map = desktop if isinstance(desktop, dict) else {}
# Même contrat que `desktop` : une chaîne pour tout le parc, ou
# 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
remote = self._qemu_erplibre_remote_cmd(
branch,
branch_def,
final_cmd,
prod,
"" if desk_map else desktop,
@ -4928,9 +4932,9 @@ class TODO:
"version": v,
"arch": a,
}
if desk_map:
if desk_map or branch_map:
entry["remote_cmd"] = self._qemu_erplibre_remote_cmd(
branch,
branch_map.get(name, branch_def),
final_cmd,
prod,
desk_map.get(name, ""),
@ -4943,7 +4947,9 @@ class TODO:
if not vms:
print(t("No VM to install."))
return
manifest = launch_installs(vms, branch, remote)
manifest = launch_installs(
vms, branch_def or next(iter(branch_map.values()), ""), remote
)
print(f"\n🖥 {t('Opening the interactive monitor...')}")
# Affiche tous les chemins de log (pour les consulter/partager même si
# on quitte le dashboard avant la fin).
@ -6291,6 +6297,14 @@ class TODO:
desktop = next((d for d in desktop_map.values() if d), "")
python_provider = spec.get("python_provider") or ""
app_store = spec.get("app_store") or "deb"
# Branche par VM : « » sur une VM veut dire « celle du formulaire ».
branch_map = {
vm["name"]: (vm.get("branch") or install_branch or "")
for vm in pending
}
for _n in deployed:
branch_map.setdefault(_n, install_branch or "")
branch_multi = len(set(branch_map.values())) > 1
ssh_key = spec.get("ssh_key")
add_ssh_config = spec["add_ssh_config"]
parallelism = spec["parallelism"]
@ -6360,7 +6374,7 @@ class TODO:
# Installs détachées en parallèle + dashboard Textual.
self._qemu_install_erplibre_monitored(
deployed,
install_branch,
branch_map if branch_multi else install_branch,
ip_map,
install["cmd"] if install else None,
install["prod"] if install else False,