2026-08-07 03:22:26 -04:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
"""Formulaire Textual de déploiement QEMU, et vue de progression.
|
|
|
|
|
|
|
|
|
|
|
|
Deux interfaces mènent au même déploiement : les invites en ligne de
|
|
|
|
|
|
`todo.py` et ce formulaire. Toutes deux produisent la MÊME structure — la
|
|
|
|
|
|
« spec » — que `TODO._qemu_run_spec` consomme. Rien n'est décidé ici qui ne
|
|
|
|
|
|
puisse l'être là-bas, et réciproquement.
|
|
|
|
|
|
|
|
|
|
|
|
- build_vms(...) / plan_rows(...) : logique pure, testable sans terminal.
|
|
|
|
|
|
- run_deploy_form(ctx, run_app=True) : le formulaire ; renvoie une spec ou None.
|
|
|
|
|
|
- run_deploy_progress(jobs, ...) : blocs repliables par VM pendant le
|
|
|
|
|
|
déploiement, avec copie du log vers le presse-papiers (OSC 52).
|
|
|
|
|
|
|
|
|
|
|
|
Le formulaire ne lance AUCUNE commande privilégiée ni réseau : tout appel
|
|
|
|
|
|
`virsh` passe par sudo et une invite de mot de passe casserait l'affichage
|
|
|
|
|
|
Textual. Les données coûteuses (domaines existants, branches distantes) sont
|
|
|
|
|
|
préchargées par l'appelant et arrivent dans `ctx`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
import re
|
2026-08-07 03:22:26 -04:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from script.todo.todo_i18n import t
|
|
|
|
|
|
except Exception: # pragma: no cover - repli si i18n indisponible
|
|
|
|
|
|
|
|
|
|
|
|
def t(key: str) -> str:
|
|
|
|
|
|
return key
|
|
|
|
|
|
|
|
|
|
|
|
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# Le socle commun aux deux formulaires (QEMU/KVM et Proxmox VE). Réexporté
|
|
|
|
|
|
# tel quel : les appelants historiques importent encore ces noms ICI.
|
|
|
|
|
|
from script.todo.deploy_form_extras import SERVER, ExtrasMixin
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
from script.todo.deploy_form_lib import ( # noqa: F401
|
|
|
|
|
|
CLIP_LIMIT,
|
|
|
|
|
|
CSS_BASE,
|
|
|
|
|
|
FREE,
|
|
|
|
|
|
INPUT_TO_FIELD,
|
|
|
|
|
|
RES_FIELDS,
|
|
|
|
|
|
RES_FMT,
|
|
|
|
|
|
SELECT_TO_FIELD,
|
|
|
|
|
|
apply_overrides,
|
|
|
|
|
|
apply_profile,
|
2026-08-24 05:38:28 -04:00
|
|
|
|
branch_default,
|
|
|
|
|
|
branch_order,
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
build_spec,
|
|
|
|
|
|
build_vms,
|
|
|
|
|
|
clean_hostname,
|
|
|
|
|
|
clip_payload,
|
|
|
|
|
|
copy_name,
|
|
|
|
|
|
disk_gb,
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
|
disk_note,
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
entry_key,
|
|
|
|
|
|
expand_copies,
|
|
|
|
|
|
fmt_dur,
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
|
gib,
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
motifs_hors_ligne,
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
parse_disk,
|
|
|
|
|
|
parse_ram,
|
|
|
|
|
|
plan_rows,
|
|
|
|
|
|
plan_totals,
|
|
|
|
|
|
positive_int,
|
|
|
|
|
|
res_choices,
|
|
|
|
|
|
res_labels,
|
|
|
|
|
|
res_row_widgets,
|
|
|
|
|
|
res_value,
|
|
|
|
|
|
run_deploy_progress,
|
|
|
|
|
|
vm_name,
|
|
|
|
|
|
vm_status,
|
|
|
|
|
|
)
|
|
|
|
|
|
from script.todo.deploy_form_plan import ( # noqa: F401
|
|
|
|
|
|
PlanMixin,
|
|
|
|
|
|
preview_screen,
|
|
|
|
|
|
rename_screen,
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# Formulaire Textual
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
def run_deploy_form(ctx, run_app: bool = True):
|
|
|
|
|
|
"""Formulaire de déploiement. Renvoie une spec, ou None si annulé.
|
|
|
|
|
|
`run_app=False` renvoie l'instance sans la lancer (tests headless)."""
|
|
|
|
|
|
from textual.app import App, ComposeResult
|
[IMP] qemu deploy : pré-configurer la VM, et dire ce qui s'y installe
L'option des outils d'assistance posait trois installateurs amont, puis
laissait tout à retaper : hook global de rtk, zdiff3, hooks git du dépôt,
commandes Claude, activation du venv. Elle les pose, en deux temps parce que
hooks et gabarits VIVENT dans le dépôt ; le complément suit le clone, rend
toujours 0 — un confort ne fait pas échouer une VM — et sans clone la moitié
manquante est nommée. core.editor n'est posé qu'à défaut, l'hôte transmettant
déjà le sien. L'aide « ? » et F1 dit ce que chaque option installe, là où un
libellé de case porte trois des huit poses. Vérifié : 51 tests, les deux
écrans montés sans terminal, « bash -n » sur les fragments distants.
--- EN ---
The AI tools box posed three upstream installers, then left every setting to
retype: rtk's global hook, zdiff3, the checkout's git hooks, the Claude
commands, activating the venv. It poses them, in two phases because hooks and
templates LIVE in the checkout; the complement follows the clone, always
returns 0 — a comfort must not fail a VM — and with no clone the missing half
is named. core.editor is posed only where there is none, the host already
transmitting its own. The `?` and F1 help says what each option installs,
where a checkbox label carries three of this one's eight poses. Checked: 51
tests, both screens mounted headless, `bash -n` on the remote fragments.
Assisted-by: Claude Opus 5
2026-09-04 02:03:52 -04:00
|
|
|
|
from textual.binding import Binding
|
2026-08-07 03:22:26 -04:00
|
|
|
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
|
|
|
|
from textual.widgets import (
|
[ADD] tui qemu: deploy several copies of one entry
A "+" per row adds a copy of the entry, a "−" removes it. The "−" only
appears on a copy: the original cannot be deleted by mistake, it is
unticked in the catalogue.
The name adapts on its own -- erplibre-ubuntu-2404, then -2, then -3.
The first keeps the catalogue name, so existing deployments keep theirs.
The real work is in identity. entry_key was (distro, version, arch): two
copies shared it, so setting the first set the second and their locks
fought each other. It now carries the instance number, and overrides as
well as locks follow the right machine. Each instance is a COPY of the
dictionary, never a shared reference.
Removing a copy forgets its settings: keeping them would resurrect old
values on the next copy, with nothing to explain it.
--- FR ---
Un « + » par rangée ajoute une copie de l'entrée, un « − » la retire. Le
« − » n'apparaît que sur une copie : l'original ne se supprime pas par
mégarde, il se décoche au catalogue.
Le nom s'adapte seul — erplibre-ubuntu-2404, puis -2, puis -3. Le
premier garde le nom du catalogue, pour que les déploiements existants
gardent le leur.
Le vrai travail est dans l'identité. entry_key valait
(distro, version, arch) : deux copies la partageaient, donc régler la
première réglait la seconde et leurs verrous se marchaient dessus. Elle
porte maintenant le numéro d'exemplaire, et surcharges comme verrous
suivent la bonne machine. Chaque exemplaire est une COPIE du
dictionnaire, jamais une référence partagée.
Retirer une copie oublie ses réglages : les garder ferait resurgir
d'anciennes valeurs à la copie suivante, sans que rien ne l'explique.
Assisted-by: Claude Opus 5
2026-08-13 00:37:45 -04:00
|
|
|
|
Button,
|
2026-08-07 03:22:26 -04:00
|
|
|
|
Checkbox,
|
|
|
|
|
|
Footer,
|
|
|
|
|
|
Header,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
RadioButton,
|
|
|
|
|
|
RadioSet,
|
|
|
|
|
|
Select,
|
|
|
|
|
|
SelectionList,
|
|
|
|
|
|
Static,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[FIX] tui qemu: the form crashed on the resource totals
"TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection'"
as soon as the custom resource profile was picked.
Textual 8 turned Select.BLANK into a deprecated alias worth False, the
sentinel having become Select.NULL. The guard compared against False and
filtered nothing; NoSelection, lacking __bool__ and thus truthy, sailed
through "or default" into the plan totals.
The sentinel is now resolved at runtime, and both summed fields are
coerced to a positive int inside the pure function -- where the invariant
belongs, whatever Textual version is installed.
--- FR ---
« TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection' »
dès qu'on choisissait le profil personnalisé.
Textual 8 a ramené Select.BLANK à un alias déprécié valant False, le
sentinelle étant devenu Select.NULL. La garde comparait donc à False et
ne filtrait plus rien ; NoSelection, dépourvu de __bool__ et tenu pour
vrai, traversait « ou valeur par défaut » jusqu'aux sommes du plan.
Le sentinelle est résolu à l'exécution, et les deux champs additionnés
sont ramenés à un entier positif dans la fonction pure — c'est là que
l'invariant appartient, quelle que soit la version de Textual.
Assisted-by: Claude Opus 5
2026-08-11 06:57:23 -04:00
|
|
|
|
# Textual 8 a ramené Select.BLANK à un alias déprécié valant False ; le
|
|
|
|
|
|
# sentinelle « rien de choisi » est Select.NULL. Comparer à BLANK ne
|
|
|
|
|
|
# filtrait donc plus rien, et NoSelection — dépourvu de __bool__, donc
|
|
|
|
|
|
# tenu pour vrai — passait pour une valeur jusque dans les totaux.
|
|
|
|
|
|
SELECT_NULL = getattr(Select, "NULL", Select.BLANK)
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
catalog = ctx["catalog"]
|
|
|
|
|
|
arches = ctx["arches"]
|
|
|
|
|
|
domains = set(ctx.get("domains") or [])
|
|
|
|
|
|
profiles = ctx.get("install_profiles") or []
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
|
# {système: (libellé, commande)} — ce qu'un système impose d'installer.
|
|
|
|
|
|
distro_profiles = ctx.get("distro_profiles") or {}
|
|
|
|
|
|
# Les commandes qui ne posent PAS ERPLibre : sa marge disque ne les suit
|
|
|
|
|
|
# pas. DÉDUITES des profils imposés — une seconde clé de contexte à tenir
|
|
|
|
|
|
# en accord avec la première aurait fini par en différer, et la marge
|
|
|
|
|
|
# serait revenue sans qu'on le voie. Jugé sur la commande effective de la
|
|
|
|
|
|
# rangée : un choix explicite compte donc autant que la règle du système.
|
|
|
|
|
|
no_erplibre = {
|
|
|
|
|
|
impose[1].strip() for impose in distro_profiles.values() if impose
|
|
|
|
|
|
}
|
2026-08-24 05:38:28 -04:00
|
|
|
|
branches = branch_order(
|
|
|
|
|
|
ctx.get("branches") or ["master"], ctx.get("branch_current")
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
host_cpu = ctx.get("host_cpu") or 2
|
|
|
|
|
|
free_ram = ctx.get("free_ram") or 0
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
|
free_disk = ctx.get("free_disk") or 0
|
|
|
|
|
|
total_disk = ctx.get("total_disk") or 0
|
2026-08-07 03:22:26 -04:00
|
|
|
|
base_vcpus = ctx.get("base_vcpus") or 2
|
|
|
|
|
|
extra_disk = ctx.get("extra_disk_gb") or 0
|
[ADD] qemu: a graphical VM name carries its desktop
erplibre-ubuntu-2404-gnome, erplibre-ubuntu-2404-mint. A graphical VM is
recognisable from a "virsh list", and its hostname says so too --
deploy_qemu uses the name as hostname when --hostname is not given.
This is not only cosmetic. The name is the COLLISION KEY: a graphical VM
and its server twin carried the same one, so the second was reported as
"already exists" and silently skipped. They are now distinct.
The suffix is applied AFTER overrides, the only point where each
machine's type is known now that it is chosen VM by VM. In the form the
name therefore follows the choice live, both ways.
"mint" rather than "cinnamon": that is the name chosen for the fleet,
the installed package still being Cinnamon from the distribution's own
repositories. The suffix lives with the flavour, in _QEMU_DESKTOP, and
the CLI calls the same function as the TUI rather than writing a second.
--- FR ---
erplibre-ubuntu-2404-gnome, erplibre-ubuntu-2404-mint. Une VM graphique
se reconnaît d'un « virsh list », et son nom d'hôte le dit aussi —
deploy_qemu prend le nom pour hôte quand --hostname n'est pas donné.
Ce n'est pas que cosmétique. Le nom sert de CLÉ DE COLLISION : une VM
graphique et sa jumelle serveur portaient le même, donc la seconde était
signalée « existe déjà » et silencieusement ignorée. Elles se
distinguent maintenant.
Le suffixe est appliqué APRÈS les surcharges, seul moment où le type de
chaque machine est connu depuis qu'il se choisit VM par VM. Dans le
formulaire, le nom suit donc le choix en direct, dans les deux sens.
« mint » et non « cinnamon » : c'est le nom retenu pour le parc, le
paquet installé restant Cinnamon depuis les dépôts de la distribution.
Le suffixe vit avec la saveur, dans _QEMU_DESKTOP, et la CLI appelle la
même fonction que la TUI plutôt que d'en écrire une seconde.
Assisted-by: Claude Opus 5
2026-08-12 03:29:33 -04:00
|
|
|
|
# {clé de saveur: suffixe de nom}, fourni par todo.py qui décrit les
|
|
|
|
|
|
# saveurs — on ne le redéfinit pas ici.
|
|
|
|
|
|
desktop_suffixes = dict(ctx.get("desktop_suffixes") or {})
|
2026-08-07 03:22:26 -04:00
|
|
|
|
defaults = ctx.get("defaults") or {}
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# Le cache est-il en marche sur cet hôte ? Lu du CONTEXTE, comme les
|
|
|
|
|
|
# autres mesures de la machine, et non de « defaults » : celui-là ne porte
|
|
|
|
|
|
# que ce qu'on veut PRÉ-COCHER, et il est vide au premier affichage — la
|
|
|
|
|
|
# case ne paraissait donc jamais, quel que soit l'état du cache.
|
|
|
|
|
|
cache_offert = bool(ctx.get("cache_offert"))
|
2026-08-07 03:22:26 -04:00
|
|
|
|
result = {"spec": None}
|
|
|
|
|
|
|
|
|
|
|
|
AUTO = "__auto__"
|
|
|
|
|
|
|
|
|
|
|
|
def entry_label(e):
|
|
|
|
|
|
star = " *" if e.get("default") else ""
|
|
|
|
|
|
return (
|
|
|
|
|
|
f"{e['distro']} {e['version']}{star} [{e['arch']}] "
|
|
|
|
|
|
f"RAM≥{e['ram']}Mo {e['disk']}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
class DeployForm(ExtrasMixin, PlanMixin, App):
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
# Le socle porte la mise en page et les modales ; ne reste ici que ce
|
|
|
|
|
|
# qui nomme les widgets propres à QEMU/KVM.
|
|
|
|
|
|
CSS = (
|
|
|
|
|
|
CSS_BASE
|
|
|
|
|
|
+ """
|
2026-08-07 03:22:26 -04:00
|
|
|
|
SelectionList { height: 10; border: solid $panel; }
|
|
|
|
|
|
RadioSet { height: auto; layout: horizontal; }
|
[ADD] tui qemu: an Odoo version per VM
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
2026-08-13 01:15:13 -04:00
|
|
|
|
/* 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
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
présence de la classe, passait sans rien prouver. 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. */
|
[ADD] tui qemu: an Odoo version per VM
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
2026-08-13 01:15:13 -04:00
|
|
|
|
.vmrow Select.vmbranch { width: 34; }
|
|
|
|
|
|
.vmrow Select.vmprof { width: 40; }
|
2026-08-07 03:22:26 -04:00
|
|
|
|
"""
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
# Touches de fonction plutôt que ctrl+lettre : ctrl+p est pris par la
|
|
|
|
|
|
# palette de commandes de Textual, et une lettre seule serait avalée
|
|
|
|
|
|
# par le champ de saisie qui a le focus.
|
|
|
|
|
|
BINDINGS = [
|
|
|
|
|
|
("f5", "deploy", t("Deploy")),
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
("f4", "clear_vm", t("Reset VM")),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
("f3", "preview", t("Preview")),
|
[ADD] tui qemu: F9 dumps the form state, widgets and model
A per-VM setting appears not to be taken into account, and nothing on
screen tells which link failed: the gap may be between the widget and
the model, or between the model and the spec. A screenshot shows
neither.
F9 writes both side by side into ~/.erplibre/deploy-form-dump.txt:
profile, shared values, overrides, locks, row generation, then VM by VM
what the list DISPLAYS against what the model HOLDS, and finally the
spec that would go to deployment.
The file can be pasted into a message, unlike an image, and it answers
the question on its own: if the list shows 16384 while the model says
1024, the defect is in the intake; if they agree and the created VM
differs, it is downstream, in deploy_qemu.
--- FR ---
Un réglage par VM ne semble pas pris en compte, et rien dans ce que
l'écran montre ne permet de trancher : l'écart peut être entre le widget
et le modèle, ou entre le modèle et la spec. Une capture d'écran ne dit
ni l'un ni l'autre.
F9 écrit les deux côte à côte dans ~/.erplibre/deploy-form-dump.txt :
profil, valeurs communes, surcharges, verrous, génération des rangées,
puis VM par VM ce que la liste AFFICHE contre ce que le modèle CONTIENT,
et enfin la spec qui partirait au déploiement.
Le fichier se recopie dans un message, contrairement à une image, et il
répond seul à la question : si la liste montre 16384 et que le modèle
dit 1024, le défaut est dans la prise en compte ; s'ils s'accordent et
que la VM créée diffère, il est en aval, dans deploy_qemu.
Assisted-by: Claude Opus 5
2026-08-13 04:51:27 -04:00
|
|
|
|
("f9", "dump_state", t("Diagnostic dump")),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
("f6", "select_all", t("All")),
|
|
|
|
|
|
("f7", "select_main", t("Main versions")),
|
|
|
|
|
|
("f8", "select_none", t("None")),
|
[IMP] qemu deploy : pré-configurer la VM, et dire ce qui s'y installe
L'option des outils d'assistance posait trois installateurs amont, puis
laissait tout à retaper : hook global de rtk, zdiff3, hooks git du dépôt,
commandes Claude, activation du venv. Elle les pose, en deux temps parce que
hooks et gabarits VIVENT dans le dépôt ; le complément suit le clone, rend
toujours 0 — un confort ne fait pas échouer une VM — et sans clone la moitié
manquante est nommée. core.editor n'est posé qu'à défaut, l'hôte transmettant
déjà le sien. L'aide « ? » et F1 dit ce que chaque option installe, là où un
libellé de case porte trois des huit poses. Vérifié : 51 tests, les deux
écrans montés sans terminal, « bash -n » sur les fragments distants.
--- EN ---
The AI tools box posed three upstream installers, then left every setting to
retype: rtk's global hook, zdiff3, the checkout's git hooks, the Claude
commands, activating the venv. It poses them, in two phases because hooks and
templates LIVE in the checkout; the complement follows the clone, always
returns 0 — a comfort must not fail a VM — and with no clone the missing half
is named. core.editor is posed only where there is none, the host already
transmitting its own. The `?` and F1 help says what each option installs,
where a checkbox label carries three of this one's eight poses. Checked: 51
tests, both screens mounted headless, `bash -n` on the remote fragments.
Assisted-by: Claude Opus 5
2026-09-04 02:03:52 -04:00
|
|
|
|
("f1", "help", t("Help")),
|
|
|
|
|
|
# « ? » aussi, parce que c'est la touche qu'on essaie d'abord.
|
|
|
|
|
|
# Cachée du pied de page : la même action deux fois s'y lirait
|
|
|
|
|
|
# comme deux aides. Un champ de saisie qui a le focus l'avale, et
|
|
|
|
|
|
# c'est pourquoi F1 existe à côté.
|
|
|
|
|
|
Binding("?", "help", t("Help"), show=False),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
("escape", "cancel", t("Cancel")),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.arch = ctx.get("native") or arches[0]
|
|
|
|
|
|
self.profile = "1"
|
|
|
|
|
|
self.custom = {}
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
self._free = {}
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self.overrides = {}
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# Vrai pendant qu'on repositionne les widgets nous-mêmes : sans
|
|
|
|
|
|
# ce verrou, poser une valeur déclencherait on_select_changed, qui
|
|
|
|
|
|
# réécrirait une surcharge — une boucle qui se nourrit seule.
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
self._syncing = False
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# Jeu de VM actuellement monté dans le panneau droit.
|
|
|
|
|
|
self._shown_ids = ()
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
|
self.extras_init(ctx, branches, profiles)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# Génération du jeu de rangées monté. Les identifiants de widgets
|
|
|
|
|
|
# portent un RANG, et le rang se décale quand on coche ou décoche
|
|
|
|
|
|
# une entrée : un événement émis par un widget déjà détruit
|
|
|
|
|
|
# s'appliquerait alors à la VM qui a pris sa place. Chaque widget
|
|
|
|
|
|
# de rangée retient sa génération ; ceux d'une génération périmée
|
|
|
|
|
|
# sont ignorés.
|
|
|
|
|
|
self._gen = 0
|
|
|
|
|
|
# VM dont les ressources sont FIGÉES, par identité de catalogue.
|
|
|
|
|
|
# Distinct des surcharges : une VM peut être modifiée sans être
|
|
|
|
|
|
# verrouillée, et le verrou fige TOUT, pas seulement ce qu'on a
|
|
|
|
|
|
# touché.
|
|
|
|
|
|
self.locked = set()
|
|
|
|
|
|
# {clé de base: nombre d'exemplaires EN PLUS du premier}.
|
|
|
|
|
|
self.copies = {}
|
|
|
|
|
|
self.rows = []
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self.vms = []
|
|
|
|
|
|
self.rows = []
|
|
|
|
|
|
|
|
|
|
|
|
# -- construction de l'écran ----------------------------------- #
|
|
|
|
|
|
def compose(self) -> ComposeResult:
|
|
|
|
|
|
yield Header()
|
|
|
|
|
|
with Horizontal(id="body"):
|
|
|
|
|
|
with VerticalScroll(id="fields"):
|
|
|
|
|
|
yield Static(t("Architecture"), classes="grouptitle")
|
|
|
|
|
|
with RadioSet(id="f_arch"):
|
|
|
|
|
|
for a in arches:
|
|
|
|
|
|
label = a if a != "all" else t("all archs")
|
|
|
|
|
|
yield RadioButton(label, value=a == self.arch)
|
|
|
|
|
|
yield Static(t("Catalog"), classes="grouptitle")
|
|
|
|
|
|
yield SelectionList(id="f_catalog")
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
yield Static(
|
|
|
|
|
|
t("Resources — applied to ALL VMs"),
|
|
|
|
|
|
classes="grouptitle",
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
with RadioSet(id="f_profile"):
|
|
|
|
|
|
for label in ("x1", "x2", "x3", "x4"):
|
|
|
|
|
|
yield RadioButton(label, value=label == "x1")
|
|
|
|
|
|
yield RadioButton(t("custom"))
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
# Chaque ressource offre ses suggestions plus « libre… »,
|
|
|
|
|
|
# qui révèle une saisie. C'est l'équivalent TUI de la règle
|
|
|
|
|
|
# de la CLI : une lettre choisit une suggestion, un chiffre
|
|
|
|
|
|
# vaut pour lui-même.
|
2026-08-07 03:22:26 -04:00
|
|
|
|
yield Select(
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
[(str(c), c) for c in ctx["cpu_presets"]]
|
|
|
|
|
|
+ [(t("free value…"), FREE)],
|
2026-08-07 03:22:26 -04:00
|
|
|
|
prompt=t("vCPU"),
|
|
|
|
|
|
id="f_vcpus",
|
|
|
|
|
|
disabled=True,
|
|
|
|
|
|
)
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
yield Input(
|
|
|
|
|
|
placeholder=t("vCPU"),
|
|
|
|
|
|
id="c_vcpus",
|
|
|
|
|
|
classes="freeval",
|
|
|
|
|
|
disabled=True,
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
yield Select(
|
|
|
|
|
|
[
|
|
|
|
|
|
(f"{m} ({m // 1024}G)", m)
|
|
|
|
|
|
for m in ctx["ram_presets"]
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
]
|
|
|
|
|
|
+ [(t("free value…"), FREE)],
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
prompt=t("RAM: 2048 or 8G"),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
id="f_ram",
|
|
|
|
|
|
disabled=True,
|
|
|
|
|
|
)
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
yield Input(
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
placeholder=t("RAM: 2048 or 8G"),
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
id="c_ram",
|
|
|
|
|
|
classes="freeval",
|
|
|
|
|
|
disabled=True,
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
yield Select(
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
[(d, d) for d in ctx["disk_presets"]]
|
|
|
|
|
|
+ [(t("free value…"), FREE)],
|
2026-08-07 03:22:26 -04:00
|
|
|
|
prompt=t("Disk"),
|
|
|
|
|
|
id="f_disk",
|
|
|
|
|
|
disabled=True,
|
|
|
|
|
|
)
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
yield Input(
|
|
|
|
|
|
placeholder=t("Disk (e.g. 250G, 1.5T)"),
|
|
|
|
|
|
id="c_disk",
|
|
|
|
|
|
classes="freeval",
|
|
|
|
|
|
disabled=True,
|
|
|
|
|
|
)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('The profile and these fields change EVERY VM.')}"
|
|
|
|
|
|
f"\n {t('A VM edited on the right (marked) keeps its own.')}",
|
|
|
|
|
|
id="scopetarget",
|
|
|
|
|
|
)
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
yield from self.compose_vm_type()
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
# La case commande TOUTE installation — ERPLibre, Odoo, mais
|
|
|
|
|
|
# aussi l'hyperviseur Proxmox VE. Nommée « ERPLibre », elle
|
|
|
|
|
|
# laissait croire qu'un système Proxmox s'installerait
|
|
|
|
|
|
# quand même : rapporté, une VM Proxmox est restée une
|
|
|
|
|
|
# Debian nue. Placée SOUS le type de VM, juste avant les
|
|
|
|
|
|
# sections qu'elle commande.
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
t("Installation"),
|
|
|
|
|
|
id="t_install",
|
|
|
|
|
|
classes="grouptitle",
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
yield Checkbox(
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
t("Install software in the VM"),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
value=defaults.get("install", True),
|
|
|
|
|
|
id="f_install",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Select(
|
|
|
|
|
|
[(b, b) for b in branches],
|
2026-08-24 05:38:28 -04:00
|
|
|
|
value=branch_default(
|
|
|
|
|
|
branches, ctx.get("branch_current")
|
2026-08-07 03:22:26 -04:00
|
|
|
|
),
|
|
|
|
|
|
allow_blank=False,
|
|
|
|
|
|
id="f_branch",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Select(
|
|
|
|
|
|
[(lbl, i) for i, (lbl, _c) in enumerate(profiles)],
|
[FIX] tui qemu: the form crashed on the resource totals
"TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection'"
as soon as the custom resource profile was picked.
Textual 8 turned Select.BLANK into a deprecated alias worth False, the
sentinel having become Select.NULL. The guard compared against False and
filtered nothing; NoSelection, lacking __bool__ and thus truthy, sailed
through "or default" into the plan totals.
The sentinel is now resolved at runtime, and both summed fields are
coerced to a positive int inside the pure function -- where the invariant
belongs, whatever Textual version is installed.
--- FR ---
« TypeError: unsupported operand type(s) for +: 'int' and 'NoSelection' »
dès qu'on choisissait le profil personnalisé.
Textual 8 a ramené Select.BLANK à un alias déprécié valant False, le
sentinelle étant devenu Select.NULL. La garde comparait donc à False et
ne filtrait plus rien ; NoSelection, dépourvu de __bool__ et tenu pour
vrai, traversait « ou valeur par défaut » jusqu'aux sommes du plan.
Le sentinelle est résolu à l'exécution, et les deux champs additionnés
sont ramenés à un entier positif dans la fonction pure — c'est là que
l'invariant appartient, quelle que soit la version de Textual.
Assisted-by: Claude Opus 5
2026-08-11 06:57:23 -04:00
|
|
|
|
value=0 if profiles else SELECT_NULL,
|
2026-08-07 03:22:26 -04:00
|
|
|
|
allow_blank=not profiles,
|
|
|
|
|
|
id="f_profile_install",
|
|
|
|
|
|
)
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
yield from self.compose_install_extras()
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# Aucune case pour le cache : l'interception est
|
|
|
|
|
|
# transparente et vaut pour tout le pont, donc une VM ne
|
|
|
|
|
|
# peut pas s'y soustraire. Décocher aurait produit une VM
|
|
|
|
|
|
# détournée SANS l'autorité, cassée sur chaque
|
|
|
|
|
|
# téléchargement HTTPS. Le seul contournement est
|
|
|
|
|
|
# d'arrêter le service, ce que le guide dit.
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
yield from self.compose_timezone()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
yield Static("SSH", classes="grouptitle")
|
|
|
|
|
|
yield Input(
|
|
|
|
|
|
value=ctx.get("ssh_key") or "",
|
|
|
|
|
|
placeholder=t("SSH public key path"),
|
|
|
|
|
|
id="f_key",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
|
t("Add each VM to ~/.ssh/config"),
|
|
|
|
|
|
value=defaults.get("add_ssh_config", True),
|
|
|
|
|
|
id="f_sshcfg",
|
|
|
|
|
|
)
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
yield from self.compose_python()
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
# Hors de la section « Installation » : le suivi
|
|
|
|
|
|
# regarde la VM ARRIVER, même quand rien ne s'installe.
|
|
|
|
|
|
# Rangé dans cette section, il se serait grisé avec elle —
|
|
|
|
|
|
# et décocher ERPLibre avait déjà fait disparaître le
|
|
|
|
|
|
# tableau de bord une fois.
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
t("Monitoring and parallelism"),
|
|
|
|
|
|
id="t_deploy",
|
|
|
|
|
|
classes="grouptitle",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
|
t("Monitoring dashboard"),
|
|
|
|
|
|
value=defaults.get("monitor", True),
|
|
|
|
|
|
id="f_monitor",
|
|
|
|
|
|
)
|
[ADD] qemu deploy : case 3D à la création, même sans écran virtuel
Une VM sans console peut vouloir un virtio-gpu accéléré — rendu hors
écran, ou émulateur qui tourne dedans — et « auto » ne l'accorde jamais :
il s'abstient sans écran, pour ne pas poser un périphérique vidéo que
personne n'a demandé. La case envoie donc « --gpu on », qui l'accorde
désormais. « --graphics none » est alors écarté : il dit « aucun
affichage », et egl-headless EST un affichage. Le repli le rend quand la
3D tombe, sinon la VM repartirait sur le défaut de virt-install.
Vérifié : 8 tests neufs et 2 sur le repli, rougis par trois mutations.
Le rendu de la case dans le terminal plein écran n'est pas testé.
--- EN ---
A VM without a console may want an accelerated virtio-gpu — offscreen
rendering, or an emulator running inside — and « auto » never grants it:
it abstains without a screen, so as not to add a video device nobody
asked for. The box therefore sends « --gpu on », which now grants it.
« --graphics none » is then dropped: it means « no display », and
egl-headless IS a display. The fallback gives it back when 3D fails,
otherwise the VM would restart on virt-install's default.
Checked: 8 new tests and 2 on the fallback, turned red by three
mutations. The box's rendering in the full-screen terminal is not tested.
Assisted-by: Claude Opus 5
2026-09-03 00:50:48 -04:00
|
|
|
|
# Indépendante du type de VM : une machine sans console
|
|
|
|
|
|
# peut vouloir un virtio-gpu accéléré — rendu hors écran,
|
|
|
|
|
|
# ou émulateur qui tourne dedans. Sans écran,
|
|
|
|
|
|
# « egl-headless » n'ouvre aucun port.
|
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
|
t("3D acceleration (host GPU), even without a screen"),
|
|
|
|
|
|
value=defaults.get("gpu3d", False),
|
|
|
|
|
|
id="f_gpu3d",
|
|
|
|
|
|
)
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# Offerte seulement là où elle a un effet : sans cache
|
|
|
|
|
|
# actif, rien n'intercepte, et une case qui ne change
|
|
|
|
|
|
# rien apprend au lecteur une chose fausse.
|
|
|
|
|
|
if cache_offert:
|
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
|
t("Keep this VM out of the download cache"),
|
|
|
|
|
|
value=defaults.get("cache_bypass", False),
|
|
|
|
|
|
id="f_cache_bypass",
|
|
|
|
|
|
)
|
[ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.
Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.
--- EN ---
One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.
Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.
Assisted-by: Claude Opus 5
2026-09-03 04:12:14 -04:00
|
|
|
|
# Révélés par la case « AI coding tools » du bloc des
|
|
|
|
|
|
# outils : sans elle, ni l'agent ni l'identité git n'ont
|
|
|
|
|
|
# d'objet, et trois widgets de plus encombrent un écran
|
|
|
|
|
|
# déjà dense. Le nom et le courriel sont pré-remplis avec
|
|
|
|
|
|
# l'identité de l'HÔTE — c'est ce que la VM reçoit
|
|
|
|
|
|
# aujourd'hui, et un champ vide la ferait croire absente.
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('AI coding tools')}",
|
|
|
|
|
|
id="t_ai",
|
|
|
|
|
|
classes="grouptitle",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Select(
|
|
|
|
|
|
[("Claude Code", "claude"), ("opencode", "opencode")],
|
|
|
|
|
|
value=defaults.get("ai_agent") or "claude",
|
|
|
|
|
|
allow_blank=False,
|
|
|
|
|
|
id="f_ai_agent",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Input(
|
|
|
|
|
|
value=defaults.get("git_name", ""),
|
|
|
|
|
|
placeholder=t("Name for git"),
|
|
|
|
|
|
id="f_git_name",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Input(
|
|
|
|
|
|
value=defaults.get("git_email", ""),
|
|
|
|
|
|
placeholder=t("Email for git"),
|
|
|
|
|
|
id="f_git_email",
|
|
|
|
|
|
)
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
# Le parallélisme reste dans « Déploiement » : c'est le
|
|
|
|
|
|
# nombre de VM menées de front, pas une option
|
|
|
|
|
|
# d'installation.
|
|
|
|
|
|
yield Static(f" {t('Parallelism')}")
|
2026-08-11 08:59:10 -04:00
|
|
|
|
# Cochée, la case donne une exécution PAR installation :
|
|
|
|
|
|
# le plafond du nombre de CPU ne s'applique plus. Décochée,
|
|
|
|
|
|
# le nombre reprend la main, et son défaut suit l'hôte —
|
|
|
|
|
|
# la CLI comptait déjà les CPU, la TUI restait figée à 4.
|
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
|
t("One run per install"),
|
|
|
|
|
|
value=defaults.get("par_per_install", True),
|
|
|
|
|
|
id="f_par_all",
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
yield Select(
|
|
|
|
|
|
[(str(n), n) for n in range(1, host_cpu + 1)],
|
2026-08-11 08:59:10 -04:00
|
|
|
|
value=host_cpu,
|
2026-08-07 03:22:26 -04:00
|
|
|
|
allow_blank=False,
|
2026-08-11 08:59:10 -04:00
|
|
|
|
disabled=True,
|
2026-08-07 03:22:26 -04:00
|
|
|
|
id="f_par",
|
|
|
|
|
|
)
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# Offerte seulement là où le cache tourne : sans lui il
|
|
|
|
|
|
# n'y a pas d'amont à couper, et la case ne ferait rien.
|
|
|
|
|
|
# Elle coupe l'internet du cache, la sortie directe des VM
|
|
|
|
|
|
# et la résolution des noms par l'internet, le temps du
|
|
|
|
|
|
# déploiement ; les VM gardent l'hôte : le cache, et des
|
|
|
|
|
|
# noms que l'hôte leur répond seul.
|
|
|
|
|
|
if cache_offert:
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
t("Network"),
|
|
|
|
|
|
id="t_network",
|
|
|
|
|
|
classes="grouptitle",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Checkbox(
|
|
|
|
|
|
t("No internet connection"),
|
|
|
|
|
|
value=defaults.get("offline", False),
|
|
|
|
|
|
id="f_offline",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('Cuts internet for the cache and the VMs: proves')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('the install builds from what the cache holds.')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
# Découvert par la case, comme le bloc IA : ce qui
|
|
|
|
|
|
# suit ne concerne que celui qui vient de la cocher.
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" ⚠ {t('The cut hits every user of the cache:')}",
|
|
|
|
|
|
id="t_offline_w1",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('a deployment run from another terminal')}",
|
|
|
|
|
|
id="t_offline_w2",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('goes offline too, without asking for it.')}",
|
|
|
|
|
|
id="t_offline_w3",
|
|
|
|
|
|
)
|
|
|
|
|
|
# La fin de la coupure suit celle des installations,
|
|
|
|
|
|
# pas celle du tableau de bord : c'est ce qu'on ne
|
|
|
|
|
|
# devine pas, le tableau de bord étant détachable.
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('Nothing is cut before F5: the upstream falls')}",
|
|
|
|
|
|
id="t_offline_w4",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('at launch and comes back when the last install')}",
|
|
|
|
|
|
id="t_offline_w5",
|
|
|
|
|
|
)
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('ends (12 h at most), even with the monitor closed.')}",
|
|
|
|
|
|
id="t_offline_w6",
|
|
|
|
|
|
)
|
|
|
|
|
|
# Dit pourquoi le suivi est grisé : seul le chemin
|
|
|
|
|
|
# suivi confie ce retour à une unité systemd. Sans
|
|
|
|
|
|
# lui, l'amont revient dès que les VM ont une adresse
|
|
|
|
|
|
# quand rien ne s'installe, ou reste coupé pour de
|
|
|
|
|
|
# bon si le terminal se ferme pendant une
|
|
|
|
|
|
# installation synchrone.
|
|
|
|
|
|
yield Static(
|
|
|
|
|
|
f" {t('The monitor stays ticked: it is what arms that return.')}",
|
|
|
|
|
|
id="t_offline_w7",
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
with Vertical(id="right"):
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# Une liste de widgets, pas un tableau : chaque VM porte
|
|
|
|
|
|
# SES listes déroulantes, modifiables sur place. Un
|
|
|
|
|
|
# DataTable ne sait pas héberger de widget.
|
|
|
|
|
|
yield VerticalScroll(id="plan")
|
2026-08-07 03:22:26 -04:00
|
|
|
|
yield Static("", id="totals")
|
|
|
|
|
|
yield Footer()
|
|
|
|
|
|
|
[ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.
Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.
--- EN ---
One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.
Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.
Assisted-by: Claude Opus 5
2026-09-03 04:12:14 -04:00
|
|
|
|
# Les widgets que la case « AI coding tools » découvre.
|
|
|
|
|
|
_AI_WIDGETS = ("#t_ai", "#f_ai_agent", "#f_git_name", "#f_git_email")
|
|
|
|
|
|
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# L'avertissement que la case « Sans connexion internet » découvre.
|
|
|
|
|
|
_OFFLINE_WIDGETS = tuple(f"#t_offline_w{n}" for n in range(1, 8))
|
|
|
|
|
|
|
|
|
|
|
|
def _sync_offline(self) -> None:
|
|
|
|
|
|
"""Montre l'avertissement quand la coupure est demandée, et y
|
|
|
|
|
|
force le suivi.
|
|
|
|
|
|
|
|
|
|
|
|
Il dit ce qu'on ne devine pas : la coupure vaut pour TOUS les
|
|
|
|
|
|
usagers du cache ; elle ne tombe qu'au lancement — cocher la case
|
|
|
|
|
|
ne coupe rien, l'écran reste utilisable — et ne se lève qu'à la
|
|
|
|
|
|
fin de la dernière installation, tableau de bord fermé ou non.
|
|
|
|
|
|
|
|
|
|
|
|
Cette dernière promesse n'est tenue que par le déploiement suivi,
|
|
|
|
|
|
le seul qui confie la levée à une unité systemd. Le suivi est donc
|
|
|
|
|
|
coché et grisé tant que la case l'est ; la décocher le rend
|
|
|
|
|
|
modifiable, avec la valeur qu'il avait avant.
|
|
|
|
|
|
"""
|
|
|
|
|
|
case = self.query("#f_offline")
|
|
|
|
|
|
vu = bool(case) and bool(case.first(Checkbox).value)
|
|
|
|
|
|
for sel in self._OFFLINE_WIDGETS:
|
|
|
|
|
|
for widget in self.query(sel):
|
|
|
|
|
|
widget.display = vu
|
|
|
|
|
|
suivi = self.query_one("#f_monitor", Checkbox)
|
|
|
|
|
|
if vu and not suivi.disabled:
|
|
|
|
|
|
self._suivi_avant = suivi.value
|
|
|
|
|
|
suivi.value = True
|
|
|
|
|
|
suivi.disabled = True
|
|
|
|
|
|
elif not vu and suivi.disabled:
|
|
|
|
|
|
suivi.disabled = False
|
|
|
|
|
|
suivi.value = getattr(self, "_suivi_avant", True)
|
|
|
|
|
|
|
[ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.
Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.
--- EN ---
One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.
Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.
Assisted-by: Claude Opus 5
2026-09-03 04:12:14 -04:00
|
|
|
|
def _sync_ai(self) -> None:
|
|
|
|
|
|
"""Montre ou cache le bloc IA selon la case des outils.
|
|
|
|
|
|
|
|
|
|
|
|
Cacher plutôt que griser : un champ grisé occupe la place et se
|
|
|
|
|
|
lit comme un réglage qu'on aurait le droit de changer."""
|
|
|
|
|
|
case = self.query("#f_tool_aidev")
|
|
|
|
|
|
vu = bool(case) and bool(case.first(Checkbox).value)
|
|
|
|
|
|
for sel in self._AI_WIDGETS:
|
|
|
|
|
|
for widget in self.query(sel):
|
|
|
|
|
|
widget.display = vu
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def on_mount(self) -> None:
|
|
|
|
|
|
self.title = t("Deploy ERPLibre VM(s)!")
|
|
|
|
|
|
self._reload_catalog(first_load=True)
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
self._sync_install_deps()
|
[ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.
Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.
--- EN ---
One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.
Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.
Assisted-by: Claude Opus 5
2026-09-03 04:12:14 -04:00
|
|
|
|
self._sync_ai()
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
self._sync_offline()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
|
|
|
|
|
|
# -- catalogue et recalcul ------------------------------------- #
|
|
|
|
|
|
def _entries(self):
|
|
|
|
|
|
return catalog.get(self.arch, [])
|
|
|
|
|
|
|
|
|
|
|
|
def _reload_catalog(self, first_load=False):
|
|
|
|
|
|
"""(Re)charge la liste à cocher.
|
|
|
|
|
|
|
|
|
|
|
|
RIEN n'est coché d'avance : déployer coûte cher, et une case
|
|
|
|
|
|
pré-cochée ferait créer une VM que personne n'a demandée. Le « * »
|
|
|
|
|
|
marque toujours la version principale, et F7 les coche toutes.
|
|
|
|
|
|
|
|
|
|
|
|
Après un changement d'architecture, les cases déjà cochées sont
|
|
|
|
|
|
conservées quand l'entrée existe encore — l'identité est
|
|
|
|
|
|
(distro, version, archi), pas le rang dans la liste."""
|
|
|
|
|
|
widget = self.query_one("#f_catalog", SelectionList)
|
|
|
|
|
|
keep = (
|
|
|
|
|
|
set()
|
|
|
|
|
|
if first_load
|
|
|
|
|
|
else {
|
|
|
|
|
|
entry_key(self._entries_before[i])
|
|
|
|
|
|
for i in widget.selected
|
|
|
|
|
|
if i < len(self._entries_before)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
widget.clear_options()
|
|
|
|
|
|
entries = self._entries()
|
|
|
|
|
|
for i, e in enumerate(entries):
|
|
|
|
|
|
widget.add_option((entry_label(e), i, entry_key(e) in keep))
|
|
|
|
|
|
self._entries_before = entries
|
|
|
|
|
|
self._recompute()
|
|
|
|
|
|
|
|
|
|
|
|
def _selected_entries(self):
|
|
|
|
|
|
widget = self.query_one("#f_catalog", SelectionList)
|
|
|
|
|
|
entries = self._entries()
|
|
|
|
|
|
return [entries[i] for i in sorted(widget.selected)]
|
|
|
|
|
|
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
# ------------------------------------------------------------ #
|
|
|
|
|
|
# Ce que le socle du plan attend de nous
|
|
|
|
|
|
# ------------------------------------------------------------ #
|
|
|
|
|
|
def _presets(self):
|
|
|
|
|
|
"""Choix offerts par ressource. Le socle s'en sert pour savoir
|
|
|
|
|
|
quand une valeur est « libre »."""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"vcpus": ctx["cpu_presets"],
|
|
|
|
|
|
"ram": ctx["ram_presets"],
|
|
|
|
|
|
"disk": ctx["disk_presets"],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def _auto_name(self, index):
|
|
|
|
|
|
"""Le nom du catalogue, suffixé du bureau : ce que la VM
|
|
|
|
|
|
reprendrait si on effaçait son nom."""
|
|
|
|
|
|
return vm_name(
|
|
|
|
|
|
self._plan_entries()[index]["name"],
|
|
|
|
|
|
self.rows[index]["vm"].get("desktop"),
|
|
|
|
|
|
desktop_suffixes,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _lock_fields(self, index):
|
|
|
|
|
|
"""TOUT ce que la VM tient d'un choix commun est recopié, pas
|
|
|
|
|
|
seulement les ressources : la branche et le profil Odoo en font
|
|
|
|
|
|
partie. Les oublier laissait une VM « figée » changer de version
|
|
|
|
|
|
d'ERPLibre dès qu'on touchait au choix générique, ce qui vide le
|
|
|
|
|
|
mot de son sens.
|
|
|
|
|
|
|
|
|
|
|
|
Les deux se résolvent AVANT d'être figés : « » y signifie « celle
|
|
|
|
|
|
du formulaire », et geler une chaîne vide ne gèlerait rien."""
|
|
|
|
|
|
vm = self.rows[index]["vm"]
|
|
|
|
|
|
return {
|
|
|
|
|
|
"vcpus": vm["vcpus"],
|
|
|
|
|
|
"ram": vm["ram"],
|
|
|
|
|
|
"disk": vm["disk"],
|
|
|
|
|
|
"desktop": vm.get("desktop") or "",
|
|
|
|
|
|
"branch": vm.get("branch") or self._branch(),
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
|
"install_cmd": (
|
|
|
|
|
|
vm.get("install_cmd") or self._row_default_cmd(index)
|
|
|
|
|
|
),
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
"install_label": (
|
|
|
|
|
|
vm.get("install_label")
|
|
|
|
|
|
or (
|
|
|
|
|
|
profiles[self._row_profile_index(index)][0]
|
|
|
|
|
|
if profiles
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def _recompute(self):
|
[ADD] tui qemu: deploy several copies of one entry
A "+" per row adds a copy of the entry, a "−" removes it. The "−" only
appears on a copy: the original cannot be deleted by mistake, it is
unticked in the catalogue.
The name adapts on its own -- erplibre-ubuntu-2404, then -2, then -3.
The first keeps the catalogue name, so existing deployments keep theirs.
The real work is in identity. entry_key was (distro, version, arch): two
copies shared it, so setting the first set the second and their locks
fought each other. It now carries the instance number, and overrides as
well as locks follow the right machine. Each instance is a COPY of the
dictionary, never a shared reference.
Removing a copy forgets its settings: keeping them would resurrect old
values on the next copy, with nothing to explain it.
--- FR ---
Un « + » par rangée ajoute une copie de l'entrée, un « − » la retire. Le
« − » n'apparaît que sur une copie : l'original ne se supprime pas par
mégarde, il se décoche au catalogue.
Le nom s'adapte seul — erplibre-ubuntu-2404, puis -2, puis -3. Le
premier garde le nom du catalogue, pour que les déploiements existants
gardent le leur.
Le vrai travail est dans l'identité. entry_key valait
(distro, version, arch) : deux copies la partageaient, donc régler la
première réglait la seconde et leurs verrous se marchaient dessus. Elle
porte maintenant le numéro d'exemplaire, et surcharges comme verrous
suivent la bonne machine. Chaque exemplaire est une COPIE du
dictionnaire, jamais une référence partagée.
Retirer une copie oublie ses réglages : les garder ferait resurgir
d'anciennes valeurs à la copie suivante, sans que rien ne l'explique.
Assisted-by: Claude Opus 5
2026-08-13 00:37:45 -04:00
|
|
|
|
entries = self._plan_entries()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self.vms = build_vms(
|
|
|
|
|
|
entries,
|
|
|
|
|
|
self.profile,
|
|
|
|
|
|
base_vcpus,
|
|
|
|
|
|
host_cpu,
|
|
|
|
|
|
self.custom,
|
|
|
|
|
|
self.overrides,
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
self._default_desktop(),
|
[ADD] qemu: a graphical VM name carries its desktop
erplibre-ubuntu-2404-gnome, erplibre-ubuntu-2404-mint. A graphical VM is
recognisable from a "virsh list", and its hostname says so too --
deploy_qemu uses the name as hostname when --hostname is not given.
This is not only cosmetic. The name is the COLLISION KEY: a graphical VM
and its server twin carried the same one, so the second was reported as
"already exists" and silently skipped. They are now distinct.
The suffix is applied AFTER overrides, the only point where each
machine's type is known now that it is chosen VM by VM. In the form the
name therefore follows the choice live, both ways.
"mint" rather than "cinnamon": that is the name chosen for the fleet,
the installed package still being Cinnamon from the distribution's own
repositories. The suffix lives with the flavour, in _QEMU_DESKTOP, and
the CLI calls the same function as the TUI rather than writing a second.
--- FR ---
erplibre-ubuntu-2404-gnome, erplibre-ubuntu-2404-mint. Une VM graphique
se reconnaît d'un « virsh list », et son nom d'hôte le dit aussi —
deploy_qemu prend le nom pour hôte quand --hostname n'est pas donné.
Ce n'est pas que cosmétique. Le nom sert de CLÉ DE COLLISION : une VM
graphique et sa jumelle serveur portaient le même, donc la seconde était
signalée « existe déjà » et silencieusement ignorée. Elles se
distinguent maintenant.
Le suffixe est appliqué APRÈS les surcharges, seul moment où le type de
chaque machine est connu depuis qu'il se choisit VM par VM. Dans le
formulaire, le nom suit donc le choix en direct, dans les deux sens.
« mint » et non « cinnamon » : c'est le nom retenu pour le parc, le
paquet installé restant Cinnamon depuis les dépôts de la distribution.
Le suffixe vit avec la saveur, dans _QEMU_DESKTOP, et la CLI appelle la
même fonction que la TUI plutôt que d'en écrire une seconde.
Assisted-by: Claude Opus 5
2026-08-12 03:29:33 -04:00
|
|
|
|
desktop_suffixes,
|
2026-08-07 03:22:26 -04:00
|
|
|
|
)
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
|
# Ce qu'un système IMPOSE d'installer, posé sur le MODÈLE et pas
|
|
|
|
|
|
# seulement à l'écran : le déploiement lit « install_cmd » VM par
|
|
|
|
|
|
# VM, et une VM Proxmox laissée à vide recevait la commande
|
|
|
|
|
|
# commune — donc ERPLibre et Odoo 18 sur un hyperviseur.
|
|
|
|
|
|
for vm in self.vms:
|
|
|
|
|
|
impose = distro_profiles.get(vm["distro"])
|
|
|
|
|
|
if impose and not vm.get("install_cmd"):
|
|
|
|
|
|
vm["install_label"], vm["install_cmd"] = (
|
|
|
|
|
|
impose[0],
|
|
|
|
|
|
impose[1],
|
|
|
|
|
|
)
|
|
|
|
|
|
self.rows = plan_rows(self.vms, domains)
|
[ADD] tui qemu: graphical VMs, with a GNOME desktop
VMs could only be servers. A server-or-graphical choice joins both
interfaces, installing GNOME along with remote access to it.
Packages come from the remote command, not cloud-init: their 1 to 2 GB
would stretch an already long boot there while leaving no trace in the
monitoring, and group installs do not go through it. The desktop
therefore does not depend on ERPLibre -- a VM may be wanted graphical
and bare.
Each distribution has its own names, taken from the source: Arch has no
xrdp in its official repositories and takes TigerVNC. The SPICE display
is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but
nothing guarantees its kernel's DRM driver, whereas remote desktop works
everywhere.
--- FR ---
Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique
s'ajoute aux deux interfaces, et pose GNOME avec son accès distant.
Les paquets viennent de la commande distante, pas de cloud-init : leurs
1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace
dans le suivi, et les installations par groupe n'y passent pas. Le bureau
ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue.
Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp
dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est
posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien
ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui,
marche partout.
Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
|
|
|
|
# ERPLibre et GNOME pèsent chacun sur le disque, et se cumulent.
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
|
# Le supplément d'ERPLibre ne vaut que pour les VM qui l'auront
|
|
|
|
|
|
# VRAIMENT : l'ajouter à une VM Proxmox gonflait son disque de
|
|
|
|
|
|
# cinq gigaoctets pour un dépôt qu'elle ne clonera pas.
|
|
|
|
|
|
installe = self.query_one("#f_install", Checkbox).value
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# Le bureau pèse sur le disque de la VM QUI LE PORTE, et d'elle
|
|
|
|
|
|
# seule : un supplément commun mentait dès que les types
|
|
|
|
|
|
# différaient d'une machine à l'autre.
|
[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-18 00:50:08 -04:00
|
|
|
|
tools = self._vm_tools()
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
|
for i, row in enumerate(self.rows):
|
|
|
|
|
|
cmd_vm = (
|
|
|
|
|
|
row["vm"].get("install_cmd") or self._profile_cmd() or ""
|
|
|
|
|
|
)
|
|
|
|
|
|
if installe and cmd_vm.strip() not in no_erplibre:
|
|
|
|
|
|
row["disk_gb"] += extra_disk
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
# Le bureau et les outils ne pèsent que sur les VM qui
|
|
|
|
|
|
# les reçoivent RÉELLEMENT : Android Studio n'existe qu'en
|
|
|
|
|
|
# x86_64, les extensions GNOME n'ont de sens que sous GNOME.
|
|
|
|
|
|
row["disk_gb"] += self._extras_disk_gb(row["vm"], tools)
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
# Le plan doit MONTRER qu'une VM a été personnalisée : sans marque,
|
|
|
|
|
|
# deux lignes aux ressources différentes n'ont aucune explication à
|
|
|
|
|
|
# l'écran, et la surcharge est oubliée à la relecture. Le drapeau
|
|
|
|
|
|
# vit sur la ligne d'affichage, jamais sur la VM : celle-ci part
|
|
|
|
|
|
# telle quelle dans la spec, que la CLI produit à l'identique.
|
|
|
|
|
|
for row, entry in zip(self.rows, entries):
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
key = entry_key(entry)
|
|
|
|
|
|
row["custom"] = bool(self.overrides.get(key))
|
|
|
|
|
|
row["locked"] = key in self.locked
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self._render_plan()
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
self.render_extras()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# -- panneau droit : une rangée de widgets par VM ---------------- #
|
|
|
|
|
|
def _mount_rows(self) -> None:
|
|
|
|
|
|
"""(Re)construit le panneau droit.
|
|
|
|
|
|
|
|
|
|
|
|
Le verrou couvre TOUT le montage : poser « value= » sur un Select
|
|
|
|
|
|
fait émettre un Changed à Textual, que on_select_changed prenait
|
|
|
|
|
|
pour une saisie. Résultat mesuré — les trois champs de CHAQUE VM
|
|
|
|
|
|
recevaient une surcharge dès l'affichage, le profil x1..x4 ne
|
|
|
|
|
|
pouvait plus rien changer, et toutes les lignes portaient la
|
|
|
|
|
|
marque ✎. Il est relâché après le rafraîchissement, une fois ces
|
|
|
|
|
|
messages consommés."""
|
|
|
|
|
|
self._syncing = True
|
|
|
|
|
|
self._gen += 1
|
|
|
|
|
|
plan = self.query_one("#plan", VerticalScroll)
|
|
|
|
|
|
plan.remove_children()
|
|
|
|
|
|
widgets = []
|
|
|
|
|
|
for i, r in enumerate(self.rows):
|
2026-08-07 03:22:26 -04:00
|
|
|
|
vm = r["vm"]
|
[ADD] tui qemu: deploy several copies of one entry
A "+" per row adds a copy of the entry, a "−" removes it. The "−" only
appears on a copy: the original cannot be deleted by mistake, it is
unticked in the catalogue.
The name adapts on its own -- erplibre-ubuntu-2404, then -2, then -3.
The first keeps the catalogue name, so existing deployments keep theirs.
The real work is in identity. entry_key was (distro, version, arch): two
copies shared it, so setting the first set the second and their locks
fought each other. It now carries the instance number, and overrides as
well as locks follow the right machine. Each instance is a COPY of the
dictionary, never a shared reference.
Removing a copy forgets its settings: keeping them would resurrect old
values on the next copy, with nothing to explain it.
--- FR ---
Un « + » par rangée ajoute une copie de l'entrée, un « − » la retire. Le
« − » n'apparaît que sur une copie : l'original ne se supprime pas par
mégarde, il se décoche au catalogue.
Le nom s'adapte seul — erplibre-ubuntu-2404, puis -2, puis -3. Le
premier garde le nom du catalogue, pour que les déploiements existants
gardent le leur.
Le vrai travail est dans l'identité. entry_key valait
(distro, version, arch) : deux copies la partageaient, donc régler la
première réglait la seconde et leurs verrous se marchaient dessus. Elle
porte maintenant le numéro d'exemplaire, et surcharges comme verrous
suivent la bonne machine. Chaque exemplaire est une COPIE du
dictionnaire, jamais une référence partagée.
Retirer une copie oublie ses réglages : les garder ferait resurgir
d'anciennes valeurs à la copie suivante, sans que rien ne l'explique.
Assisted-by: Claude Opus 5
2026-08-13 00:37:45 -04:00
|
|
|
|
item = self._plan_entries()[i]
|
|
|
|
|
|
key = entry_key(item)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
row = Horizontal(
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
# « + » ajoute un exemplaire de CETTE entrée ; « - » ne
|
|
|
|
|
|
# s'affiche que sur une copie, pour qu'on ne puisse pas
|
|
|
|
|
|
# retirer l'original par mégarde.
|
|
|
|
|
|
Button("+", id=f"p{i}", classes="vmcopy"),
|
|
|
|
|
|
Button("✎", id=f"r{i}", classes="vmcopy"),
|
|
|
|
|
|
Button(
|
|
|
|
|
|
"🔒" if key in self.locked else "🔓",
|
|
|
|
|
|
id=f"l{i}",
|
|
|
|
|
|
variant="success" if key in self.locked else "default",
|
|
|
|
|
|
classes="vmlock",
|
|
|
|
|
|
),
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
# Le triplet vCPU / RAM / disque vient du socle
|
|
|
|
|
|
# commun : c'est la partie que le formulaire Proxmox pose
|
|
|
|
|
|
# à l'identique, et la seule que les deux dupliquaient.
|
|
|
|
|
|
*res_row_widgets(
|
|
|
|
|
|
i,
|
|
|
|
|
|
vm,
|
|
|
|
|
|
{
|
|
|
|
|
|
"vcpus": ctx["cpu_presets"],
|
|
|
|
|
|
"ram": ctx["ram_presets"],
|
|
|
|
|
|
"disk": ctx["disk_presets"],
|
|
|
|
|
|
},
|
|
|
|
|
|
labels={"vcpus": "vCPU"},
|
|
|
|
|
|
null=SELECT_NULL,
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
),
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
(
|
|
|
|
|
|
Button("−", id=f"m{i}", classes="vmcopy")
|
|
|
|
|
|
if item.get("instance")
|
|
|
|
|
|
else Static("", classes="vmcopy")
|
|
|
|
|
|
),
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
|
*self.install_row_widgets(i, SELECT_NULL),
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
classes="vmrow",
|
2026-08-07 03:22:26 -04:00
|
|
|
|
)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
widgets.append(
|
|
|
|
|
|
Vertical(
|
|
|
|
|
|
Static(self._row_head(i, r), id=f"h{i}"),
|
|
|
|
|
|
row,
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
# Pas d'id sur la carte : « remove_children() » est
|
|
|
|
|
|
# ASYNCHRONE, les anciennes sont encore là au montage
|
|
|
|
|
|
# et Textual refuse deux frères de même id. Les ids
|
|
|
|
|
|
# des champs vivent un niveau plus bas, dans un parent
|
|
|
|
|
|
# neuf — la collision ne les touche pas. On atteint
|
|
|
|
|
|
# donc la carte par son RANG.
|
|
|
|
|
|
classes=(
|
|
|
|
|
|
"vmcard locked" if key in self.locked else "vmcard"
|
|
|
|
|
|
),
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Marque de génération, posée sur CHAQUE widget de rangée.
|
|
|
|
|
|
# « walk_children() » ne voit RIEN avant le montage : les enfants
|
|
|
|
|
|
# passés au constructeur attendent dans « _pending_children ».
|
|
|
|
|
|
def mark(node):
|
|
|
|
|
|
node._el_gen = self._gen
|
|
|
|
|
|
for child in getattr(node, "_pending_children", None) or []:
|
|
|
|
|
|
mark(child)
|
|
|
|
|
|
|
|
|
|
|
|
for card in widgets:
|
|
|
|
|
|
mark(card)
|
|
|
|
|
|
# Le montage vient APRÈS le marquage, jamais avant : « mount_all »
|
|
|
|
|
|
# consomme « _pending_children » et le vide. Marquer ensuite était
|
|
|
|
|
|
# une COURSE — gagnée sur une machine, perdue sur une autre. Perdue,
|
|
|
|
|
|
# les listes des rangées n'avaient plus de génération, TOUS les
|
|
|
|
|
|
# changements par VM étaient rejetés en silence, et seuls les
|
|
|
|
|
|
# réglages globaux semblaient agir.
|
|
|
|
|
|
if widgets:
|
|
|
|
|
|
plan.mount_all(widgets)
|
|
|
|
|
|
self._shown_ids = self._row_ids()
|
|
|
|
|
|
# Les saisies libres ne se révèlent qu'après le montage : leur
|
|
|
|
|
|
# style ne peut pas être touché avant qu'elles existent.
|
|
|
|
|
|
self.call_after_refresh(self._after_mount_rows)
|
|
|
|
|
|
|
|
|
|
|
|
def _after_mount_rows(self) -> None:
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
self._sync_install_deps()
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
self._sync_free_inputs()
|
|
|
|
|
|
self._syncing = False
|
|
|
|
|
|
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
def _refresh_row_widgets(self) -> None:
|
|
|
|
|
|
"""Remet les listes de chaque rangée sur ce que la VM vaut MAINTENANT.
|
|
|
|
|
|
|
|
|
|
|
|
Sans cela, changer le profil x1..x4 mettait les totaux à jour mais
|
|
|
|
|
|
laissait les listes sur leurs anciennes valeurs : l'écran affichait
|
|
|
|
|
|
8192 pendant que la VM valait 4096. Les rangées ne sont remontées
|
|
|
|
|
|
que si le JEU de VM change — pour ne pas voler le focus — donc ce
|
|
|
|
|
|
rafraîchissement doit se faire à la main.
|
|
|
|
|
|
|
|
|
|
|
|
Aucun risque de boucle : on_select_changed ignore une valeur déjà
|
|
|
|
|
|
égale à celle du modèle, et le modèle vient précisément d'être
|
|
|
|
|
|
recalculé."""
|
|
|
|
|
|
for i, r in enumerate(self.rows):
|
|
|
|
|
|
vm = r["vm"]
|
|
|
|
|
|
for field, presets in (
|
|
|
|
|
|
("vcpus", ctx["cpu_presets"]),
|
|
|
|
|
|
("ram", ctx["ram_presets"]),
|
|
|
|
|
|
("disk", ctx["disk_presets"]),
|
|
|
|
|
|
):
|
|
|
|
|
|
try:
|
|
|
|
|
|
sel = self.query_one(f"#v{i}_{field}", Select)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
continue
|
|
|
|
|
|
# Une liste posée sur « libre… » ne doit PAS être remise
|
|
|
|
|
|
# sur une valeur : l'utilisateur vient de la choisir, et
|
|
|
|
|
|
# tant qu'il n'a rien tapé la VM vaut encore celle du
|
|
|
|
|
|
# profil — on la lui reprendrait sous les doigts.
|
|
|
|
|
|
if sel.value is FREE:
|
|
|
|
|
|
continue
|
|
|
|
|
|
# Une valeur libre déjà saisie n'est dans aucune liste :
|
|
|
|
|
|
# liste vide, la saisie à côté porte le nombre.
|
|
|
|
|
|
sel.value = (
|
|
|
|
|
|
vm[field] if vm[field] in presets else SELECT_NULL
|
|
|
|
|
|
)
|
|
|
|
|
|
for wid, value in (
|
|
|
|
|
|
(f"#v{i}_type", vm.get("desktop") or SERVER),
|
|
|
|
|
|
(f"#v{i}_branch", vm.get("branch") or self._branch()),
|
[ADD] tui qemu: an Odoo version per VM
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
2026-08-13 01:15:13 -04:00
|
|
|
|
(f"#v{i}_prof", self._row_profile_index(i)),
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
):
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.query_one(wid, Select).value = value
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._sync_free_inputs()
|
|
|
|
|
|
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
def _render_plan(self):
|
|
|
|
|
|
# Le JEU de VM a-t-il changé ? Si oui on remonte les widgets, sinon
|
|
|
|
|
|
# on se contente des titres : remonter à chaque frappe volerait le
|
|
|
|
|
|
# focus au champ en cours de saisie.
|
|
|
|
|
|
if self._row_ids() != self._shown_ids:
|
|
|
|
|
|
self._mount_rows()
|
|
|
|
|
|
else:
|
|
|
|
|
|
for i, r in enumerate(self.rows):
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.query_one(f"#h{i}", Static).update(
|
|
|
|
|
|
self._row_head(i, r)
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
self._refresh_row_widgets()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
if not self.rows:
|
|
|
|
|
|
# Rien de coché : un total à zéro n'apprend rien, on dit
|
|
|
|
|
|
# plutôt comment remplir la liste.
|
|
|
|
|
|
self.query_one("#totals", Static).update(
|
|
|
|
|
|
f" {t('Tick what to deploy')} — "
|
|
|
|
|
|
f"{t('F7 main versions · F6 all')}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
n, cpus, ram, disk = plan_totals(self.rows)
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
|
# Une liste et non un seul avertissement : la RAM, les cœurs et le
|
|
|
|
|
|
# disque sont trois limites distinctes, et n'en montrer qu'une
|
|
|
|
|
|
# cachait les autres — on corrigeait la première pour découvrir la
|
|
|
|
|
|
# suivante au déploiement.
|
|
|
|
|
|
alertes = []
|
2026-08-07 03:22:26 -04:00
|
|
|
|
if free_ram and ram > free_ram:
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
|
alertes.append(t("> host free RAM"))
|
|
|
|
|
|
if free_disk and disk > free_disk:
|
|
|
|
|
|
alertes.append(t("> host free disk"))
|
|
|
|
|
|
if cpus > host_cpu:
|
|
|
|
|
|
alertes.append(f"{t('> host cores')} ({host_cpu})")
|
|
|
|
|
|
warn = f" ⚠ {' · '.join(alertes)}" if alertes else ""
|
2026-08-07 03:22:26 -04:00
|
|
|
|
dupes = len({vm["name"] for vm in self.vms}) != len(self.vms)
|
|
|
|
|
|
dup_txt = (
|
|
|
|
|
|
f"\n ⚠ {t('Duplicate names detected; keeping as entered.')}"
|
|
|
|
|
|
if dupes
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# Une VM DÉJÀ DÉFINIE n'est pas recréée : elle ne consomme rien
|
|
|
|
|
|
# de neuf, donc plan_totals l'écarte. Mais un total à zéro sans
|
|
|
|
|
|
# explication se lit comme un bogue — on a cru que les réglages
|
|
|
|
|
|
# par VM n'avaient aucun effet, alors qu'ils portaient sur une
|
|
|
|
|
|
# machine qui ne sera pas créée.
|
|
|
|
|
|
skipped = sum(1 for r in self.rows if r["state"] == "exists")
|
|
|
|
|
|
skip_txt = (
|
|
|
|
|
|
f" ({skipped} {t('already defined, not counted')})"
|
|
|
|
|
|
if skipped
|
|
|
|
|
|
else ""
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self.query_one("#totals", Static).update(
|
[ADD] todo : dire la place qui reste sous le plan de déploiement
La ligne de totaux annonçait « ~126 G » sans dire sur quoi : la demande
seule ne dit pas si ça rentre, et on l'apprenait au déploiement. Elle dit
maintenant la demande, ce qui reste et la capacité — « ~126 G / 20 G libres
sur 270 G » — et prévient dès que le plan dépasse. Les trois limites (RAM,
disque, cœurs) s'affichent ensemble : n'en montrer qu'une cachait les
autres. Sur Proxmox, la place vient du stockage choisi, que « pvesm status »
donnait déjà.
Deux défauts trouvés en le faisant : la marque de génération, exigée de tous
les widgets, faisait taire chaque réglage commun de l'écran Proxmox, et
« [x1] » disparaissait, lu comme une balise Rich.
--- EN ---
The totals line said "~126 G" without saying out of what: the demand alone
does not tell whether it fits, and one found out at deploy time. It now says
the demand, what is left and the capacity — "~126 G / 20 G free of 270 G" —
and warns as soon as the plan exceeds it. The three limits (RAM, disk,
cores) show together: showing only one hid the others. On Proxmox the room
comes from the chosen storage, which "pvesm status" already gave.
Two defects found on the way: the generation mark, required of every widget,
silenced each common setting on the Proxmox screen, and "[x1]" vanished,
read as a Rich tag.
Assisted-by: Claude Opus 5
2026-08-24 01:11:09 -04:00
|
|
|
|
f" {n} {t('VMs')} · {cpus} vCPU · {ram} Mo · "
|
|
|
|
|
|
f"{disk_note(disk, free_disk, total_disk)}"
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
f"{skip_txt}{warn}{dup_txt}"
|
2026-08-07 03:22:26 -04:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# -- réactions aux champs -------------------------------------- #
|
|
|
|
|
|
def on_radio_set_changed(self, event) -> None:
|
|
|
|
|
|
if event.radio_set.id == "f_arch":
|
|
|
|
|
|
self.arch = arches[event.radio_set.pressed_index]
|
|
|
|
|
|
self._reload_catalog()
|
[ADD] tui qemu: graphical VMs, with a GNOME desktop
VMs could only be servers. A server-or-graphical choice joins both
interfaces, installing GNOME along with remote access to it.
Packages come from the remote command, not cloud-init: their 1 to 2 GB
would stretch an already long boot there while leaving no trace in the
monitoring, and group installs do not go through it. The desktop
therefore does not depend on ERPLibre -- a VM may be wanted graphical
and bare.
Each distribution has its own names, taken from the source: Arch has no
xrdp in its official repositories and takes TigerVNC. The SPICE display
is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but
nothing guarantees its kernel's DRM driver, whereas remote desktop works
everywhere.
--- FR ---
Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique
s'ajoute aux deux interfaces, et pose GNOME avec son accès distant.
Les paquets viennent de la commande distante, pas de cloud-init : leurs
1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace
dans le suivi, et les installations par groupe n'y passent pas. Le bureau
ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue.
Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp
dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est
posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien
ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui,
marche partout.
Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
|
|
|
|
elif event.radio_set.id == "f_type":
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
self._clear_overrides(("desktop",))
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
# Le type est l'autre moitié de la décision : un bureau seul
|
|
|
|
|
|
# garde le magasin d'applications et les outils utiles.
|
|
|
|
|
|
self._sync_install_deps()
|
[UPD] tui qemu: the plan shows each VM type
The server-or-desktop choice only showed on the left, in the form. The
table on the right, the one read before launching, said nothing about it:
two visually identical plans could produce different VMs.
The Status column, empty until now for a VM to create, therefore carries
the type, and the totals line repeats it. An already defined VM keeps its
collision message: it is not touched, showing a desktop there would
suggest one is about to be installed on it.
--- FR ---
Le choix serveur ou bureau ne se voyait qu'à gauche, dans le formulaire.
Le tableau de droite, celui qu'on relit avant de lancer, n'en disait
rien : deux plans identiques à l'écran pouvaient produire des VM
différentes.
La colonne Statut, vide jusqu'ici pour une VM à créer, porte donc le
type, et la ligne de totaux le rappelle. Une VM déjà définie garde son
message de collision : elle n'est pas retouchée, lui afficher un bureau
laisserait croire qu'on va lui en poser un.
Assisted-by: Claude Opus 5
2026-08-11 18:55:52 -04:00
|
|
|
|
# Recalcul : le disque annonce inclut le bureau, et la
|
|
|
|
|
|
# colonne Statut affiche le type de VM.
|
|
|
|
|
|
self._recompute()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
elif event.radio_set.id == "f_profile":
|
|
|
|
|
|
index = event.radio_set.pressed_index
|
|
|
|
|
|
self.profile = "custom" if index == 4 else str(index + 1)
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
# Un multiplicateur x1..x4 ne touche QUE les vCPU et la RAM —
|
|
|
|
|
|
# apply_profile y laisse le disque du catalogue. Y effacer une
|
|
|
|
|
|
# taille de disque réglée à la main la faisait disparaître sans
|
|
|
|
|
|
# rien mettre à la place : on revenait à 20G sans l'avoir
|
|
|
|
|
|
# demandé. Le disque n'est rendu au commun que par le profil
|
|
|
|
|
|
# « personnalisé », qui en porte un.
|
|
|
|
|
|
fields = ("vcpus", "ram")
|
|
|
|
|
|
if self.profile == "custom":
|
|
|
|
|
|
fields += ("disk",)
|
|
|
|
|
|
self._clear_overrides(fields)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
custom = self.profile == "custom"
|
|
|
|
|
|
for field, (sel, _inp) in RES_FIELDS.items():
|
|
|
|
|
|
self.query_one(sel, Select).disabled = not custom
|
|
|
|
|
|
self._show_free(field, custom and self._free.get(field))
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self._recompute()
|
|
|
|
|
|
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# -- rangées du panneau droit ------------------------------- #
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def on_selection_list_selected_changed(self, event) -> None:
|
|
|
|
|
|
self._recompute()
|
|
|
|
|
|
|
|
|
|
|
|
def on_select_changed(self, event) -> None:
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
if self._syncing:
|
|
|
|
|
|
return
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
wid = event.select.id or ""
|
|
|
|
|
|
row = re.match(r"v(\d+)_(vcpus|ram|disk|type|branch|prof)$", wid)
|
|
|
|
|
|
if row and not self._is_current(event.select):
|
|
|
|
|
|
# Widget d'une génération périmée : son rang ne désigne plus
|
|
|
|
|
|
# la même VM. L'appliquer écraserait le réglage d'une voisine.
|
|
|
|
|
|
return
|
|
|
|
|
|
if row:
|
|
|
|
|
|
index, field = int(row.group(1)), row.group(2)
|
|
|
|
|
|
if index >= len(self.rows):
|
|
|
|
|
|
return
|
|
|
|
|
|
# Poser « value= » au montage fait émettre un Changed que
|
|
|
|
|
|
# Textual délivre APRÈS coup : un verrou temporel ne l'attrape
|
|
|
|
|
|
# pas — mesuré, les trois champs de chaque VM se retrouvaient
|
|
|
|
|
|
# surchargés dès l'affichage et le profil x1..x4 devenait
|
|
|
|
|
|
# inopérant. On compare donc à ce que le modèle dit déjà : une
|
|
|
|
|
|
# valeur identique n'est pas une saisie, c'est l'écho.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Cas limite assumé : choisir explicitement la valeur que le
|
|
|
|
|
|
# profil donne déjà n'enregistre pas de surcharge. La VM
|
|
|
|
|
|
# suivra donc le profil s'il change — ce qui est aussi le plus
|
|
|
|
|
|
# attendu quand on n'a rien changé de visible.
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
|
if self.extras_on_row_select(event, index, field):
|
[ADD] tui qemu: an Odoo version per VM
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
2026-08-13 01:15:13 -04:00
|
|
|
|
return
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
|
# Ne restent ici que les RESSOURCES : elles n'ont pas de
|
|
|
|
|
|
# défaut à comparer, mais une saisie libre à révéler.
|
|
|
|
|
|
if event.value is FREE:
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
self._row_free(index, field, True)
|
|
|
|
|
|
self._set_override(
|
|
|
|
|
|
index, field, self._read_row_free(index, field)
|
|
|
|
|
|
)
|
|
|
|
|
|
elif event.value is not SELECT_NULL:
|
[REF] déploiement : la branche, le profil et le type se choisissent par VM
Sur Proxmox on déploie le plus souvent un parc MIXTE — un hyperviseur
imbriqué à côté de VM ERPLibre. C'est exactement le cas où un réglage par
machine sert, et c'est le seul écran qui ne l'offrait pas : ses rangées
n'avaient ni branche, ni profil, ni type.
Les trois choix et leur gestionnaire — quatre-vingts lignes — rejoignent le
socle. Les dupliquer aurait remis en place le mécanisme de dérive qu'on vient
d'enlever. L'écran QEMU/KVM perd encore 130 lignes sans qu'un widget, un
modèle ou une spec ne bouge : ancien et nouveau montés dans le même
processus, mêmes rangées, mêmes valeurs après avoir changé une branche, un
type et un profil.
Le déploiement suit : il lisait la seule valeur commune alors que le plan
portait déjà le choix par rangée. Une seule VM qui s'écarte suffit à rendre
la carte nécessaire — « len(set) > 1 » ne l'aurait pas vu.
Un défaut trouvé par un test, pas à l'usage : l'écho du montage se
reconnaissait à sa commande, or quand la commande imposée par le système
n'est pas dans la liste proposée, la liste retombe au rang 0 — et l'écho de
ce rang 0 effaçait l'imposition. Un Proxmox imbriqué reprenait ERPLibre et
Odoo 18. L'écho se reconnaît maintenant au RANG affiché.
--- EN ---
On Proxmox you usually deploy a MIXED fleet — a nested hypervisor next to
ERPLibre VMs. That is exactly where a per-machine setting earns its keep, and
it was the only screen without one: its rows had no branch, no profile, no
type.
The three choices and their handler — eighty lines — move into the shared
foundation. Duplicating them would have restored the very drift mechanism we
just removed. The QEMU/KVM screen loses another 130 lines with no widget,
model or spec moving: old and new mounted in one process, same rows, same
values after changing a branch, a type and a profile.
The deployment follows: it read the single common value while the plan
already carried the per-row choice. One VM that differs is enough to require
the map — "len(set) > 1" would not have seen it.
One defect found by a test, not by use: the mount echo was recognised by its
command, yet when the command imposed by the guest OS is absent from the
offered list, the list falls back to index 0 — and that index-0 echo erased
the imposition. A nested Proxmox took ERPLibre and Odoo 18 back. The echo is
now recognised by the DISPLAYED index.
Assisted-by: Claude Opus 5
2026-08-24 23:31:13 -04:00
|
|
|
|
if self._row_echo(index, field, event.value):
|
|
|
|
|
|
return
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
self._row_free(index, field, False)
|
|
|
|
|
|
self._set_override(index, field, event.value)
|
|
|
|
|
|
self._recompute()
|
|
|
|
|
|
return
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
# Les choix GLOBAUX de branche et de profil ne portent aucune
|
|
|
|
|
|
# valeur de ressource : ils tombaient donc dans le « return »
|
|
|
|
|
|
# ci-dessous sans rien recalculer, et les rangées restaient sur
|
|
|
|
|
|
# l'ancienne version. Elles n'en gardent pas de copie — « » y
|
|
|
|
|
|
# veut dire « celle du formulaire » — il suffit de redessiner.
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
if self.extras_on_select(event):
|
[ADD] tui qemu: pick the time zone from a list
The timezone was typed by hand. A misspelt IANA name is not rejected by
cloud-init: it is IGNORED. The VM stays on UTC, and you only notice from
the timestamps, once deployed.
A list of twenty-five zones, Québec first, then the rest of Canada and
the places one actually meets. The host's zone goes to the top, without
duplication: a machine outside this list must still see its own at a
glance.
NAMES, not offsets: "UTC-5" says nothing about daylight saving and
cloud-init will not take it. A name carries its own switching rules.
"free value…" keeps the door open to the other six hundred zones in the
database. The choice is copied into the field, which stays the only
value the spec reads -- one place holds the answer.
--- FR ---
Le fuseau se tapait à la main. Un nom IANA mal orthographié n'est pas
refusé par cloud-init : il est IGNORÉ. La VM reste en UTC, et on ne s'en
aperçoit qu'aux horodatages, une fois déployée.
Une liste de vingt-cinq fuseaux, le Québec d'abord, puis le reste du
Canada et les places qu'on rencontre en pratique. Le fuseau de l'hôte
passe en tête, sans doublon : une machine hors de cette liste doit voir
le sien en un coup d'œil.
Des NOMS, pas des décalages : « UTC-5 » ne dit rien de l'heure d'été et
cloud-init n'en veut pas. Un nom porte ses propres règles de bascule.
« libre… » garde la porte ouverte aux six cents autres fuseaux de la
base. Le choix est recopié dans le champ, qui reste la seule valeur lue
par la spec — un seul endroit porte la réponse.
Assisted-by: Claude Opus 5
2026-08-13 01:56:36 -04:00
|
|
|
|
return
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
if event.select.id in ("f_branch", "f_profile_install"):
|
|
|
|
|
|
self._clear_overrides(
|
|
|
|
|
|
("branch",)
|
|
|
|
|
|
if event.select.id == "f_branch"
|
|
|
|
|
|
else ("install_cmd",)
|
|
|
|
|
|
)
|
|
|
|
|
|
self._recompute()
|
|
|
|
|
|
return
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
field = SELECT_TO_FIELD.get(event.select.id)
|
|
|
|
|
|
if not field:
|
|
|
|
|
|
return
|
|
|
|
|
|
if event.value is FREE:
|
|
|
|
|
|
# La valeur retenue est celle de la saisie, pas ce choix-ci.
|
|
|
|
|
|
self._free[field] = True
|
|
|
|
|
|
self._show_free(field, True)
|
|
|
|
|
|
self.query_one(RES_FIELDS[field][1], Input).focus()
|
|
|
|
|
|
self._apply_free(field)
|
|
|
|
|
|
elif event.value is not SELECT_NULL:
|
|
|
|
|
|
self._free[field] = False
|
|
|
|
|
|
self._show_free(field, False)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
self.custom[field] = event.value
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
self._clear_overrides((field,))
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
self._recompute()
|
|
|
|
|
|
|
|
|
|
|
|
def on_input_changed(self, event) -> None:
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
if self._syncing:
|
|
|
|
|
|
return
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
wid = event.input.id or ""
|
|
|
|
|
|
row = re.match(r"c(\d+)_(vcpus|ram|disk)$", wid)
|
|
|
|
|
|
if row and not self._is_current(event.input):
|
|
|
|
|
|
return
|
|
|
|
|
|
if row:
|
|
|
|
|
|
index, field = int(row.group(1)), row.group(2)
|
[FIX] qemu : ne pas poser ERPLibre sur une VM Proxmox VE
Choisir « Proxmox VE » comme système, c'est demander qu'il soit installé.
L'invite en ligne le savait ; le formulaire posait « ERPLibre + Odoo 18 »,
lui ajoutait les 5 Go réservés au dépôt et annonçait une cible make dans le
guide de la VM. La règle vit maintenant en un seul endroit, les deux
chemins la lisent, et un choix explicite l'emporte toujours sur elle.
Trois défauts trouvés derrière. Une commande par VM n'était retenue que si
DEUX VM différaient : déployée seule, la VM Proxmox retombait sur la
commande commune. Le disque choisi à la main disparaissait quand rien
n'était installé — 60 G demandés, 20 G créés. Et une taille absente des
préréglages marquait la rangée ✎ dès son montage.
--- EN ---
Choosing "Proxmox VE" as the system means asking for it to be installed.
The command-line prompt knew that; the form set "ERPLibre + Odoo 18", added
the 5 GB meant for the repository and advertised a make target in the VM's
guide. The rule now lives in one place, both paths read it, and an explicit
choice always wins over it.
Three defects behind it. A per-VM command was only used when TWO VMs
differed: deployed alone, the Proxmox VM fell back to the common command.
A hand-picked disk size vanished when nothing was installed — 60 G asked,
20 G created. And a size absent from the presets marked its row ✎ on mount.
Assisted-by: Claude Opus 5
2026-08-24 01:34:10 -04:00
|
|
|
|
valeur = self._read_row_free(index, field)
|
|
|
|
|
|
# Même règle que pour les listes : poser « value= » au montage
|
|
|
|
|
|
# émet un Changed. L'écrire comme surcharge marquait ✎ une
|
|
|
|
|
|
# rangée que personne n'avait touchée — visible dès qu'une
|
|
|
|
|
|
# entrée du catalogue porte une taille absente des
|
|
|
|
|
|
# préréglages, comme les 32 G de Proxmox VE.
|
|
|
|
|
|
if self._row_echo(index, field, valeur):
|
|
|
|
|
|
return
|
|
|
|
|
|
self._set_override(index, field, valeur)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
self._recompute()
|
|
|
|
|
|
return
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
field = INPUT_TO_FIELD.get(event.input.id)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
if field:
|
[ADD] tui qemu: free values for vCPU, RAM and disk
The CLI already took a typed value -- a letter picks a suggestion, a
digit stands for itself. The form, though, locked the custom profile
into its three dropdowns.
Each now gets a final "free value..." entry revealing an input below it.
An invalid entry overwrites nothing: the resource falls back to the
catalog value instead of freezing the view.
The 3 vCPU preset was missing too, between 2 and 4; it comes from the
same constant, so both interfaces gain it together.
--- FR ---
La CLI acceptait déjà une valeur tapée — une lettre choisit une
suggestion, un chiffre vaut pour lui-même. Le formulaire, lui, enfermait
le profil personnalisé dans ses trois listes déroulantes.
Chacune reçoit donc un dernier choix, « valeur libre… », qui révèle une
saisie sous elle. Une entrée invalide n'écrase rien : la ressource
retombe sur celle du catalogue plutôt que de bloquer la vue.
Le préréglage 3 vCPU manquait aussi, entre 2 et 4 ; il vient de la même
constante, donc les deux interfaces l'offrent ensemble.
Assisted-by: Claude Opus 5
2026-08-11 06:57:49 -04:00
|
|
|
|
self._apply_free(field)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self._recompute()
|
|
|
|
|
|
|
[ADD] tui qemu: freeze one VM's resources, row in green
A 🔒 box at the head of each row. Ticked, the VM's four current values --
vCPU, RAM, disk, type -- are copied into its overrides: the x1..x4 profile
no longer reaches it. Unticked, they are removed and the VM falls back
under the profile. The lock shows on the WHOLE ROW, not in a box lost at
the end: that is what lets you scan the plan and see at once what escapes
the profile.
It rests on the override mechanism already proven, indexed by catalog
identity, so it survives a remount. Locked state stays distinct from
overrides: a VM can be edited without being frozen, and the lock covers
all four fields at once.
Four traps came with it. "remove_children()" is ASYNCHRONOUS, so giving
the card an id broke the remount, the old one still being there. Lists
kept stale values across a remount, and a refresh took back the free
entry. A frozen VM changed version all the same. And switching profile
wiped the disk size that had been set.
--- FR ---
Une case 🔒 en tête de chaque rangée. Cochée, les quatre valeurs courantes
de la VM — vCPU, RAM, disque, type — sont recopiées dans ses surcharges :
le profil x1..x4 ne l'atteint plus. Décochée, elles sont retirées et la VM
retombe sous le profil. Le verrou se voit à la LIGNE ENTIÈRE, pas à une
case perdue au bout : c'est ce qui permet de balayer le plan et de savoir
d'un coup ce qui échappe au profil.
Il s'appuie sur le mécanisme de surcharge déjà éprouvé, indexé par
identité de catalogue : il survit donc à un remontage. L'état verrouillé
reste distinct des surcharges : une VM peut être modifiée sans être figée,
et le verrou couvre les quatre champs d'un coup.
Quatre pièges l'ont accompagné. « remove_children() » est ASYNCHRONE :
donner un id à la carte faisait échouer le remontage, l'ancienne étant
encore là. Les listes gardaient des valeurs périmées au remontage, et un
rafraîchissement reprenait la saisie libre. Une VM figée changeait quand
même de version. Et changer de profil effaçait la taille de disque réglée.
Assisted-by: Claude Opus 5
2026-08-13 00:33:04 -04:00
|
|
|
|
def on_button_pressed(self, event) -> None:
|
|
|
|
|
|
match = re.match(r"([pm])(\d+)$", event.button.id or "")
|
|
|
|
|
|
if match:
|
|
|
|
|
|
self._add_copy(
|
|
|
|
|
|
int(match.group(2)), 1 if match.group(1) == "p" else -1
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
match = re.match(r"r(\d+)$", event.button.id or "")
|
|
|
|
|
|
if match:
|
|
|
|
|
|
self._rename(int(match.group(1)))
|
|
|
|
|
|
return
|
|
|
|
|
|
match = re.match(r"l(\d+)$", event.button.id or "")
|
|
|
|
|
|
if match:
|
|
|
|
|
|
index = int(match.group(1))
|
|
|
|
|
|
key = self._row_key(index)
|
|
|
|
|
|
if key is not None:
|
|
|
|
|
|
self._set_lock(index, key not in self.locked)
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def on_checkbox_changed(self, event) -> None:
|
|
|
|
|
|
if event.checkbox.id == "f_install":
|
[UPD] todo : nommer la case d'installation par ce qu'elle commande
« Installer ERPLibre » commandait TOUTE installation, l'hyperviseur Proxmox
VE compris : décochée, une VM Proxmox restait une Debian nue sans que rien
ne l'explique. Elle devient « Installer un logiciel dans la VM », passe sous
le type de VM — juste avant les sections qu'elle commande — et le suivi la
quitte, puisqu'il regarde la VM arriver même quand rien ne s'installe.
Ce qu'elle rend sans effet se grise, titre compris. Trois états et non deux :
sans installation mais AVEC un bureau, le magasin d'applications et les
outils de la phase « avant » servent encore ; seuls ceux qui vivent dans le
dépôt s'éteignent. Griser en bloc aurait menti autant que de tout laisser.
--- EN ---
"Install ERPLibre" commanded EVERY install, the Proxmox VE hypervisor
included: unticked, a Proxmox VM stayed a bare Debian with nothing to explain
it. It becomes "Install software in the VM", moves under the VM type — right
before the sections it commands — and the dashboard leaves it, since that
watches the VM arrive even when nothing is installed.
What it makes ineffective now greys out, title included. Three states, not
two: with no install but WITH a desktop, the application store and the
"before" tools still act; only those living in the repository go dark.
Greying the lot would have lied as much as greying nothing.
Assisted-by: Claude Opus 5
2026-08-24 02:32:09 -04:00
|
|
|
|
self._sync_install_deps()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
self._recompute() # le disque annoncé inclut le +5 G ERPLibre
|
2026-08-11 08:59:10 -04:00
|
|
|
|
elif event.checkbox.id == "f_par_all":
|
|
|
|
|
|
self.query_one("#f_par", Select).disabled = event.value
|
[ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.
Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.
--- EN ---
One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.
Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.
Assisted-by: Claude Opus 5
2026-09-03 04:12:14 -04:00
|
|
|
|
elif event.checkbox.id == "f_tool_aidev":
|
|
|
|
|
|
self._sync_ai()
|
|
|
|
|
|
self._recompute()
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
elif event.checkbox.id == "f_offline":
|
|
|
|
|
|
self._sync_offline()
|
[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-17 22:57:56 -04:00
|
|
|
|
elif str(event.checkbox.id or "").startswith("f_tool_"):
|
|
|
|
|
|
# Un IDE de plus, c'est un disque plus grand : le plan doit le
|
|
|
|
|
|
# montrer AVANT de déployer, pas après une heure d'installation.
|
|
|
|
|
|
self._recompute()
|
2026-08-07 03:22:26 -04:00
|
|
|
|
|
|
|
|
|
|
# -- actions ---------------------------------------------------- #
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
def action_clear_vm(self) -> None:
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
"""Rend au profil commun la VM dont un widget a le focus. Sans
|
|
|
|
|
|
cette sortie, un réglage posé par erreur ne se défaisait qu'en
|
|
|
|
|
|
rouvrant le formulaire."""
|
|
|
|
|
|
index = self._focused_row()
|
|
|
|
|
|
if index is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
key = self._row_key(index)
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
if key is None or key not in self.overrides:
|
|
|
|
|
|
return
|
|
|
|
|
|
self.overrides.pop(key)
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
# Les widgets de la rangée portent encore l'ancienne valeur : on
|
|
|
|
|
|
# les remonte pour qu'ils disent la vérité.
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
self._recompute()
|
[ADD] tui qemu: set every VM in place, type included
VM type was global: the whole fleet as servers, or all of them GNOME. It
now lives on the VM, all the way down -- the creation flag, and one remote
command per machine at install time, where a single one served them all.
The right pane is no longer a table but a row of widgets per VM: vCPU,
RAM, disk and type, each with its usual values and a free entry. The
left-hand fields become the shared default, which the screen now says. The
scope selector and the F2 modal go away: given two ways to do the same
thing, keep the visible one.
Three traps came out of it, and the tests lock them. Widget ids carry a
RANK, and the rank shifts when an entry is ticked, so an event from an
already destroyed widget applied to the VM that took its place -- rows now
carry a generation, marked BEFORE mounting, since mount_all empties the
pending children and marking after it was a race. The x1..x4 profile no
longer reached any VM. And a total of zero never said it had counted
nothing.
--- FR ---
Le type de VM était global : tout le parc en serveur, ou tout en GNOME. Il
vit désormais sur la VM, jusqu'au bout — le drapeau de création, et une
commande distante par machine à l'installation, là où une seule les
servait toutes.
Le panneau de droite n'est plus un tableau mais une rangée de widgets par
VM : vCPU, RAM, disque et type, chacun avec ses valeurs usuelles et une
saisie libre. Les champs de gauche deviennent le défaut commun, ce que
l'écran dit maintenant. Le sélecteur de portée et la modale F2 partent :
entre deux façons de faire la même chose, on garde la visible.
Trois pièges en sont sortis, et les tests les verrouillent. Les
identifiants de widgets portent un RANG, et le rang se décale quand on
coche une entrée : un événement émis par un widget déjà détruit
s'appliquait à la VM qui avait pris sa place — les rangées portent
maintenant une génération, marquée AVANT le montage, car mount_all vide
les enfants en attente et marquer après était une course. Le profil x1..x4
n'atteignait plus aucune VM. Et un total à zéro ne disait pas qu'il
n'avait rien compté.
Assisted-by: Claude Opus 5
2026-08-12 02:46:41 -04:00
|
|
|
|
self._mount_rows()
|
[ADD] tui qemu: customise one VM without touching the others
The three resource fields applied to the whole fleet. With a single
machine needing 16 G, the other eight got it too. A scope selector
settles it: all VMs, or the row targeted in the plan. The x1..x4 profile
stays global, multiplying what each image asks for; under "selected VM"
the fields are absolute, hence active even outside the custom profile.
The F2 modal already existed but was undiscoverable, and its cursor was
unusable: the table never held focus. It stays, the toggle now focuses
the table, F4 returns a VM to the shared profile, and the plan marks
customised rows with a ✎ -- without it, two rows with different
resources have no explanation on screen.
One defect found along the way: clear() reset the cursor to the top on
every redraw, and the plan recomputes on every keystroke. You picked
debian, typed the value, the table redrew, and the next entry landed on
ubuntu with nothing to show for it.
--- FR ---
Les trois champs de ressources s'appliquaient à tout le parc. Une seule
machine ayant besoin de 16 G, il fallait les donner aux huit autres. Un
sélecteur de portée tranche : toutes les VM, ou la ligne visée dans le
plan. Le profil x1..x4 reste global, il multiplie ce que demande chaque
image ; en portée « une seule » les champs valent en absolu, et sont donc
actifs même hors profil personnalisé.
La modale F2 existait déjà mais restait introuvable, et son curseur était
inutilisable : le tableau n'avait jamais le focus. Elle demeure, la
bascule lui donne le focus, F4 rend une VM au profil commun, et le plan
marque d'un ✎ ce qui a été personnalisé — sans marque, deux lignes aux
ressources différentes n'ont aucune explication à l'écran.
Un défaut au passage : « clear() » ramenait le curseur en tête à chaque
redessin, et le plan se recalcule à chaque frappe. On choisissait debian,
on tapait la valeur, le tableau se redessinait, et la saisie suivante
partait sur ubuntu sans que rien ne le montre.
Assisted-by: Claude Opus 5
2026-08-12 01:30:11 -04:00
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def _form_values(self):
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# La case n'existe que si le cache tourne : la chercher toujours
|
|
|
|
|
|
# ferait lever le formulaire là où il n'y a pas de cache,
|
|
|
|
|
|
# c'est-à-dire sur la plupart des hôtes.
|
|
|
|
|
|
offline = bool(
|
|
|
|
|
|
self.query("#f_offline") # type: ignore[union-attr]
|
|
|
|
|
|
and self.query_one("#f_offline", Checkbox).value
|
|
|
|
|
|
)
|
|
|
|
|
|
# Hors ligne, le suivi est exigé : seul son chemin confie la levée
|
|
|
|
|
|
# à une unité systemd. L'écran force déjà la case ; la valeur
|
|
|
|
|
|
# l'est aussi ici, pour qu'aucun état du widget ne mène à une
|
|
|
|
|
|
# coupure sans levée bornée.
|
|
|
|
|
|
monitor = offline or self.query_one("#f_monitor", Checkbox).value
|
2026-08-07 03:22:26 -04:00
|
|
|
|
install = None
|
|
|
|
|
|
if self.query_one("#f_install", Checkbox).value and profiles:
|
|
|
|
|
|
index = self.query_one("#f_profile_install", Select).value
|
|
|
|
|
|
label, cmd = profiles[index if isinstance(index, int) else 0]
|
|
|
|
|
|
install = {
|
|
|
|
|
|
"branch": self.query_one("#f_branch", Select).value,
|
|
|
|
|
|
"prod": self.query_one("#f_prod", Checkbox).value,
|
|
|
|
|
|
"label": label,
|
|
|
|
|
|
"cmd": cmd,
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
"monitor": monitor,
|
2026-08-07 03:22:26 -04:00
|
|
|
|
}
|
|
|
|
|
|
key = self.query_one("#f_key", Input).value.strip()
|
|
|
|
|
|
return {
|
[FIX] déploiement : suivre une VM même sans installation ERPLibre
Décocher l'installation d'ERPLibre faisait disparaître le tableau de bord. La
case « suivi » vivait DANS le groupe de l'installation, build_spec ne la
recopiait même pas dans la spec, et l'épilogue était gardé par « if install or
desktop » : sans rien à installer, il ne se passait rien.
Le suivi devient un choix du DÉPLOIEMENT. Et sans rien à installer, la
commande distante ne vaut plus « true » — journal vide, ✅ instantané : elle
regarde la VM ARRIVER, attend cloud-init, puis relève système, noyau, adresse,
disque et mémoire. Le journal cesse aussi d'annoncer une installation ERPLibre
qui n'a pas lieu.
--- EN ---
Unchecking the ERPLibre install made the dashboard vanish. The "monitoring"
checkbox lived INSIDE the install group, build_spec did not even copy it into
the spec, and the deploy epilogue was gated by "if install or desktop": with
nothing to install, nothing happened.
Monitoring is now a DEPLOYMENT-level choice. And with nothing to install, the
remote command is no longer "true" — empty log, instant ✅: it watches the VM
ARRIVE, waits for cloud-init, then reports system, kernel, address, disk and
memory. The log also stops announcing an ERPLibre install that never happens.
Assisted-by: Claude Opus 5
2026-08-23 03:50:57 -04:00
|
|
|
|
# Le suivi est demandé au NIVEAU DU DÉPLOIEMENT, pas de
|
|
|
|
|
|
# l'installation : décocher ERPLibre emportait la case avec
|
|
|
|
|
|
# elle, et le tableau de bord ne s'ouvrait plus du tout.
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
"monitor": monitor,
|
[ADD] qemu deploy : case 3D à la création, même sans écran virtuel
Une VM sans console peut vouloir un virtio-gpu accéléré — rendu hors
écran, ou émulateur qui tourne dedans — et « auto » ne l'accorde jamais :
il s'abstient sans écran, pour ne pas poser un périphérique vidéo que
personne n'a demandé. La case envoie donc « --gpu on », qui l'accorde
désormais. « --graphics none » est alors écarté : il dit « aucun
affichage », et egl-headless EST un affichage. Le repli le rend quand la
3D tombe, sinon la VM repartirait sur le défaut de virt-install.
Vérifié : 8 tests neufs et 2 sur le repli, rougis par trois mutations.
Le rendu de la case dans le terminal plein écran n'est pas testé.
--- EN ---
A VM without a console may want an accelerated virtio-gpu — offscreen
rendering, or an emulator running inside — and « auto » never grants it:
it abstains without a screen, so as not to add a video device nobody
asked for. The box therefore sends « --gpu on », which now grants it.
« --graphics none » is then dropped: it means « no display », and
egl-headless IS a display. The fallback gives it back when 3D fails,
otherwise the VM would restart on virt-install's default.
Checked: 8 new tests and 2 on the fallback, turned red by three
mutations. The box's rendering in the full-screen terminal is not tested.
Assisted-by: Claude Opus 5
2026-09-03 00:50:48 -04:00
|
|
|
|
"gpu3d": self.query_one("#f_gpu3d", Checkbox).value,
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# La case n'existe que si le cache tourne : la chercher
|
|
|
|
|
|
# toujours ferait lever le formulaire là où il n'y a pas de
|
|
|
|
|
|
# cache, c'est-à-dire sur la plupart des hôtes.
|
|
|
|
|
|
"cache_bypass": bool(
|
|
|
|
|
|
self.query("#f_cache_bypass") # type: ignore[union-attr]
|
|
|
|
|
|
and self.query_one("#f_cache_bypass", Checkbox).value
|
|
|
|
|
|
),
|
|
|
|
|
|
"offline": offline,
|
[ADD] qemu deploy : poser rtk, starship et un agent dans la VM
Une case de plus au catalogue des outils, donc une case dans les deux
écrans sans y toucher. Cochée, elle découvre le choix de l'agent — Claude
Code ou opencode — et l'identité git, pré-remplie avec celle de l'hôte :
c'est ce que la VM reçoit déjà, et un champ vide la ferait croire absente.
Ce qui est saisi prime, champ par champ.
Phase « before », où chaque outil se garde lui-même. Chaque pose est aussi
privée d'entrée standard et bornée dans le temps : « || true » couvre
l'échec, pas l'ATTENTE, et un installateur amont qui pose une question
resterait pendu sur un SSH sans terminal — d'où « -y » pour starship.
--- EN ---
One more entry in the tool catalogue, hence one more box on both screens
for free. Ticked, it reveals the agent choice — Claude Code or opencode —
and the git identity, prefilled from the host: that is what the VM
already gets, and an empty field would suggest none. What is typed wins,
field by field.
Phase « before », where each tool guards itself. Every install is also
denied stdin and time-bounded: « || true » covers failure, not WAITING,
and an upstream installer asking a question would hang on a terminal-less
SSH — hence « -y » for starship.
Assisted-by: Claude Opus 5
2026-09-03 04:12:14 -04:00
|
|
|
|
"ai_agent": self.query_one("#f_ai_agent", Select).value,
|
|
|
|
|
|
"git_name": self.query_one("#f_git_name", Input).value.strip(),
|
|
|
|
|
|
"git_email": self.query_one(
|
|
|
|
|
|
"#f_git_email", Input
|
|
|
|
|
|
).value.strip(),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
"res_label": (
|
|
|
|
|
|
t("custom")
|
|
|
|
|
|
if self.profile == "custom"
|
|
|
|
|
|
else f"x{self.profile}"
|
|
|
|
|
|
),
|
|
|
|
|
|
"ssh_key": os.path.expanduser(key) if key else "",
|
[REF] déploiement : un seul socle pour ce qui décrit le système invité
Type de VM, production, magasin d'applications, outils de développement,
fuseau horaire, interpréteur Python : six réglages qui décrivent l'invité, pas
la machine qui le porte. Ils valaient donc déjà sur Proxmox — l'écran n'en
offrait que trois. Une VM créée là-bas naissait serveur nu, sans outils et en
UTC, et rien ne le disait.
La duplication était le mécanisme de la dérive : chaque correctif se posait
sur un seul des deux écrans. Les six vivent maintenant dans un socle commun,
widgets ET logique, et le contexte qui les nourrit est écrit une fois. L'écran
QEMU/KVM perd 330 lignes sans qu'un widget ni une valeur de spec ne bouge —
vérifié en montant l'ancien et le nouveau dans le même processus.
Trois conséquences se sont propagées seules : le disque annoncé (bureau et
outils compris) atteint enfin « qm resize », le parallélisme suit les cœurs de
l'hôte au lieu d'un plafond de quatre, et le nom prend le suffixe du bureau —
sans lui, une VM graphique et sa jumelle serveur se disputaient le même.
Deux défauts nommés au passage. Le fuseau : « qm set » n'en pose pas, il part
maintenant par ssh avant l'installation. Et l'architecture d'une VM Proxmox
venait de « virsh », qui ne connaît que les domaines d'ici — une VM ARM prise
pour x86_64 recevait Android Studio, que Google ne publie pas pour elle.
Le test porte sur la PARITÉ, pas sur six comportements : ajouter un réglage à
un seul écran le fait échouer. Il a d'abord échoué à se lancer — hors des
préfixes du lanceur, douze tests n'ont jamais tourné et le total n'avait pas
bougé. La règle de nommage est maintenant dans son en-tête.
--- EN ---
VM type, production, app store, development tools, timezone, Python
interpreter: six settings that describe the guest, not the machine hosting it.
They already applied to Proxmox — the screen offered three. A VM created there
was born a bare server, no tools, in UTC, and nothing said so.
Duplication was the mechanism of the drift: each fix landed on one screen
only. The six now live in a shared foundation, widgets AND logic, and the
context feeding them is written once. The QEMU/KVM screen loses 330 lines with
no widget and no spec value moving — verified by mounting the old and the new
in one process.
Three consequences followed on their own: the announced disk (desktop and
tools included) finally reaches "qm resize", parallelism follows the host's
cores instead of a cap of four, and the name takes the desktop suffix —
without it a graphical VM and its server twin fought over the same one.
Two defects named along the way. The timezone: "qm set" sets none, it now goes
over ssh before the install. And a Proxmox VM's architecture came from
"virsh", which only knows local domains — an ARM VM taken for x86_64 got
Android Studio, which Google does not publish for it.
The test covers PARITY, not six behaviours: adding a setting to one screen
alone fails it. It first failed to run at all — outside the runner's prefixes,
twelve tests never ran and the total had not moved. The naming rule is now in
its header.
Assisted-by: Claude Opus 5
2026-08-24 22:45:50 -04:00
|
|
|
|
**self.extras_values(),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
"install": install,
|
|
|
|
|
|
"add_ssh_config": self.query_one("#f_sshcfg", Checkbox).value,
|
2026-08-11 08:59:10 -04:00
|
|
|
|
# Une exécution par installation : le nombre de VM retenues
|
|
|
|
|
|
# fait foi. Le déploiement le borne ensuite à ce même nombre,
|
|
|
|
|
|
# donc une valeur haute ne crée jamais de travailleur inutile.
|
|
|
|
|
|
"parallelism": (
|
|
|
|
|
|
max(1, len(self.vms))
|
|
|
|
|
|
if self.query_one("#f_par_all", Checkbox).value
|
|
|
|
|
|
else self.query_one("#f_par", Select).value
|
|
|
|
|
|
),
|
2026-08-07 03:22:26 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def action_preview(self) -> None:
|
|
|
|
|
|
spec = build_spec(self.vms, domains, self._form_values())
|
|
|
|
|
|
build = ctx.get("build_command")
|
|
|
|
|
|
if not build:
|
|
|
|
|
|
return
|
|
|
|
|
|
lines = [build(vm, spec, True) for vm in spec["vms"]]
|
[REF] todo : un fichier par sujet, un socle par formulaire
todo.py passait 13 000 lignes : plus personne n'y trouvait où une chose
vivait. Il en garde 4 400 — les menus et les aides générales — et six
fichiers portent chacun un sujet, assemblés en mixins sur la classe TODO.
Aucun membre perdu : 438 avant, 438 après, et treize sources modifiées
seulement là où un appel de classe devait changer de nom.
Les deux formulaires de déploiement posent le même travail : ils partagent
maintenant la logique pure, le CSS, la fabrique des rangées de ressources
et les gestes du plan (surcharges, verrous, exemplaires, renommage).
Vérifié en rendant le formulaire QEMU avant et après, sur neuf gestes :
même écran au SVG près, même état, même spec.
--- EN ---
todo.py had passed 13,000 lines: nobody could find where anything lived.
It keeps 4,400 — the menus and the general helpers — and six files each
own one subject, assembled as mixins on the TODO class. No member lost:
438 before, 438 after, and thirteen sources changed only where a
class-level call had to change name.
Both deployment forms do the same work: they now share the pure logic, the
CSS, the resource-row factory and the plan's gestures (overrides, locks,
copies, renaming). Verified by rendering the QEMU form before and after
across nine gestures: same screen down to the SVG, same state, same spec.
Assisted-by: Claude Opus 5
2026-08-23 18:01:09 -04:00
|
|
|
|
self.push_screen(
|
|
|
|
|
|
preview_screen()(lines or [t("Nothing selected.")])
|
|
|
|
|
|
)
|
2026-08-07 03:22:26 -04:00
|
|
|
|
|
[ADD] tui qemu: F9 dumps the form state, widgets and model
A per-VM setting appears not to be taken into account, and nothing on
screen tells which link failed: the gap may be between the widget and
the model, or between the model and the spec. A screenshot shows
neither.
F9 writes both side by side into ~/.erplibre/deploy-form-dump.txt:
profile, shared values, overrides, locks, row generation, then VM by VM
what the list DISPLAYS against what the model HOLDS, and finally the
spec that would go to deployment.
The file can be pasted into a message, unlike an image, and it answers
the question on its own: if the list shows 16384 while the model says
1024, the defect is in the intake; if they agree and the created VM
differs, it is downstream, in deploy_qemu.
--- FR ---
Un réglage par VM ne semble pas pris en compte, et rien dans ce que
l'écran montre ne permet de trancher : l'écart peut être entre le widget
et le modèle, ou entre le modèle et la spec. Une capture d'écran ne dit
ni l'un ni l'autre.
F9 écrit les deux côte à côte dans ~/.erplibre/deploy-form-dump.txt :
profil, valeurs communes, surcharges, verrous, génération des rangées,
puis VM par VM ce que la liste AFFICHE contre ce que le modèle CONTIENT,
et enfin la spec qui partirait au déploiement.
Le fichier se recopie dans un message, contrairement à une image, et il
répond seul à la question : si la liste montre 16384 et que le modèle
dit 1024, le défaut est dans la prise en compte ; s'ils s'accordent et
que la VM créée diffère, il est en aval, dans deploy_qemu.
Assisted-by: Claude Opus 5
2026-08-13 04:51:27 -04:00
|
|
|
|
def action_dump_state(self) -> None:
|
|
|
|
|
|
"""Écrit l'état COMPLET dans un fichier, widgets ET modèle.
|
|
|
|
|
|
|
|
|
|
|
|
Une capture d'écran ne dit pas si l'écart vient de ce qu'on voit
|
|
|
|
|
|
ou de ce qui sera déployé. Ce vidage met les deux côte à côte, VM
|
|
|
|
|
|
par VM : si la liste affiche 16384 et que le modèle dit 1024, le
|
|
|
|
|
|
défaut est dans la prise en compte ; s'ils s'accordent et que la
|
|
|
|
|
|
VM déployée diffère, il est en aval."""
|
|
|
|
|
|
path = os.path.expanduser("~/.erplibre/deploy-form-dump.txt")
|
|
|
|
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
|
|
out = [
|
|
|
|
|
|
"=== formulaire de deploiement ===",
|
|
|
|
|
|
f"profil={self.profile} custom={self.custom}",
|
|
|
|
|
|
f"verrous={sorted(self.locked)}",
|
|
|
|
|
|
f"surcharges={self.overrides}",
|
|
|
|
|
|
f"generation={self._gen} jeu_monte={self._shown_ids}",
|
|
|
|
|
|
f"branche_globale={self._branch()}",
|
|
|
|
|
|
f"profil_odoo_global={self._profile_cmd()}",
|
|
|
|
|
|
"",
|
|
|
|
|
|
" # widget -> modele, VM par VM",
|
|
|
|
|
|
]
|
|
|
|
|
|
for i, r in enumerate(self.rows):
|
|
|
|
|
|
vm = r["vm"]
|
|
|
|
|
|
shown = {}
|
|
|
|
|
|
for f in ("vcpus", "ram", "disk", "type", "branch", "prof"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
shown[f] = self.query_one(f"#v{i}_{f}", Select).value
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
shown[f] = "-"
|
|
|
|
|
|
try:
|
|
|
|
|
|
free = self.query_one(f"#c{i}_{f}", Input)
|
|
|
|
|
|
if free.display:
|
|
|
|
|
|
shown[f] = f"libre:{free.value!r}"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
out.append(f" [{i}] {vm['name']} etat={r['state']}")
|
|
|
|
|
|
out.append(f" widgets = {shown}")
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
f" modele = vcpus={vm['vcpus']} ram={vm['ram']} "
|
|
|
|
|
|
f"disk={vm['disk']} desktop={vm.get('desktop')!r} "
|
|
|
|
|
|
f"branch={vm.get('branch')!r} cmd={vm.get('install_cmd')!r}"
|
|
|
|
|
|
)
|
|
|
|
|
|
spec = build_spec(self.vms, domains, self._form_values())
|
|
|
|
|
|
out.append("")
|
|
|
|
|
|
out.append(" # spec qui partirait au deploiement")
|
|
|
|
|
|
for vm in spec["vms"]:
|
|
|
|
|
|
out.append(
|
|
|
|
|
|
f" {vm['name']}: ram={vm['ram']} vcpus={vm['vcpus']} "
|
|
|
|
|
|
f"disk={vm['disk']}"
|
|
|
|
|
|
)
|
|
|
|
|
|
out.append(f" ignorees (existent deja) = {spec['existing']}")
|
|
|
|
|
|
with open(path, "w", encoding="utf-8") as fh:
|
|
|
|
|
|
fh.write("\n".join(out) + "\n")
|
|
|
|
|
|
self.notify(f"{t('State written to')} {path}")
|
|
|
|
|
|
|
2026-08-07 03:22:26 -04:00
|
|
|
|
def action_deploy(self) -> None:
|
|
|
|
|
|
if not self.vms:
|
|
|
|
|
|
self.notify(t("Nothing selected."), severity="warning")
|
|
|
|
|
|
return
|
|
|
|
|
|
spec = build_spec(self.vms, domains, self._form_values())
|
|
|
|
|
|
if not spec["vms"]:
|
|
|
|
|
|
self.notify(
|
|
|
|
|
|
t("Nothing to create - every VM already exists."),
|
|
|
|
|
|
severity="warning",
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
[ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
VMs of one host pulled the same packages again and again, and an install
could not be proven to work without the network. A Go service intercepts the
bridge transparently: a package file is served from disk, an index is always
revalidated upstream and replayed only when upstream is mute, git is mirrored.
Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut
the network for a whole install. The TODO menu installs, diagnoses, fills,
cleans and copies the store; long_test/qemu_cache.py measures that a second VM
pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py.
--- FR ---
Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait
qu'une installation marche sans réseau. Un service Go intercepte le pont de
façon transparente : un fichier de paquet est servi du disque, un index est
toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git
est mis en miroir. Le déploiement pose l'autorité du cache dans les invités
QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le
menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ;
long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de
l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py.
Assisted-by: Claude Opus 5
2026-09-14 14:41:19 -04:00
|
|
|
|
# Hors ligne : ce que le cache ne détient pas, aucune VM ne
|
|
|
|
|
|
# pourra le lire. Dit MAINTENANT, et non après une heure
|
|
|
|
|
|
# d'installation qui échoue sur « Impossible de trouver le
|
|
|
|
|
|
# paquet ». F5 à nouveau vaut passage outre — le journal peut
|
|
|
|
|
|
# avoir tourné, ou le cache avoir été rempli autrement.
|
|
|
|
|
|
if spec.get("offline") and not getattr(
|
|
|
|
|
|
self, "_offline_ack", False
|
|
|
|
|
|
):
|
|
|
|
|
|
motifs = motifs_hors_ligne(spec["vms"])
|
|
|
|
|
|
if motifs:
|
|
|
|
|
|
self._offline_ack = True
|
|
|
|
|
|
self.notify(
|
|
|
|
|
|
" — ".join(motifs + [t("press F5 again to confirm")]),
|
|
|
|
|
|
severity="error",
|
|
|
|
|
|
timeout=20,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
2026-08-07 03:22:26 -04:00
|
|
|
|
orphans = [r for r in self.rows if r["state"] == "orphan"]
|
|
|
|
|
|
if orphans and not getattr(self, "_orphan_ack", False):
|
|
|
|
|
|
# Un qcow2 orphelin fait échouer deploy_qemu : on prévient une
|
|
|
|
|
|
# première fois, F5 à nouveau vaut confirmation.
|
|
|
|
|
|
self._orphan_ack = True
|
|
|
|
|
|
self.notify(
|
|
|
|
|
|
t("orphan disk - will FAIL")
|
|
|
|
|
|
+ f" ({len(orphans)}) — "
|
|
|
|
|
|
+ t("press F5 again to confirm"),
|
|
|
|
|
|
severity="error",
|
|
|
|
|
|
timeout=10,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
result["spec"] = spec
|
|
|
|
|
|
self.exit()
|
|
|
|
|
|
|
|
|
|
|
|
def action_cancel(self) -> None:
|
|
|
|
|
|
self.exit()
|
|
|
|
|
|
|
|
|
|
|
|
app = DeployForm()
|
|
|
|
|
|
# Exposé pour les tests headless (run_app=False), qui pilotent l'app
|
|
|
|
|
|
# eux-mêmes et ont besoin de lire la spec produite.
|
|
|
|
|
|
app._result = result
|
|
|
|
|
|
if not run_app:
|
|
|
|
|
|
return app
|
|
|
|
|
|
app.run()
|
|
|
|
|
|
return result["spec"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
# Vue de progression : un bloc repliable par VM
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|